Skip to content

Commit

Permalink
client/dcr: Add rpc staking.
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeGruffins committed Aug 8, 2023
1 parent ae95dca commit 25a52a4
Show file tree
Hide file tree
Showing 10 changed files with 590 additions and 12 deletions.
18 changes: 14 additions & 4 deletions client/asset/dcr/rpcwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -912,26 +912,36 @@ func (w *rpcWallet) Tickets(ctx context.Context) ([]*asset.Ticket, error) {
w.log.Errorf("GetTransaction error for ticket %s: %v", h, err)
continue
}
blkHash, err := chainhash.NewHashFromStr(tx.BlockHash)
if err != nil {
w.log.Errorf("Invalid block hash %v for ticket %v: %w", tx.BlockHash, h, err)
continue
}
// dcrwallet returns do not include the block height.
hdr, err := w.client().GetBlockHeader(ctx, blkHash)
if err != nil {
w.log.Errorf("GetBlockHeader error for ticket %s: %v", h, err)
continue
}
msgTx, err := msgTxFromHex(tx.Hex)
if err != nil {
w.log.Errorf("Error decoding ticket %s tx hex: %v", h, err)
continue
}

if len(msgTx.TxOut) < 1 {
w.log.Errorf("No outputs for ticket %s", h)
continue
}

feeAmt, _ := dcrutil.NewAmount(tx.Fee)
// Fee is always negative.
feeAmt, _ := dcrutil.NewAmount(-tx.Fee)

tickets = append(tickets, &asset.Ticket{
Ticket: asset.TicketTransaction{
Hash: h.String(),
TicketPrice: uint64(msgTx.TxOut[0].Value),
Fees: uint64(feeAmt),
Stamp: uint64(tx.Time),
BlockHeight: tx.BlockIndex,
BlockHeight: int64(hdr.Height),
},
// The walletjson.GetTransactionResult returned from GetTransaction
// actually has a TicketStatus string field, but it doesn't appear
Expand Down
1 change: 1 addition & 0 deletions client/cmd/dexcctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ var promptPasswords = map[string][]string{
"appseed": {"App password:"},
"startmarketmaking": {"App password:"},
"multitrade": {"App password:"},
"purchasetickets": {"App password:"},
}

// optionalTextFiles is a map of routes to arg index for routes that should read
Expand Down
72 changes: 72 additions & 0 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -10498,3 +10498,75 @@ func (c *Core) SendShielded(appPW []byte, assetID uint32, toAddr string, amt uin

return coinID, nil
}

// dcrStakingWallet fetches the decred wallet and returns its asset.TicketBuyer
// interface. Errors if no decred wallet is currently loaded. Used for decred
// ticket purchasing.
func (c *Core) dcrStakingWallet() (*xcWallet, asset.TicketBuyer, error) {
dcrAssetID := uint32(42)
wallet, exists := c.wallet(dcrAssetID)
if !exists {
return nil, nil, newError(missingWalletErr, "no configured wallet found for dcr")
}
ticketBuyer, is := wallet.Wallet.(asset.TicketBuyer)
if !is {
return nil, nil, fmt.Errorf("dcr wallet is not a TicketBuyer")
}
return wallet, ticketBuyer, nil
}

// StakeStatus returns current staking statuses such as currently owned
// tickets, ticket price, and current voting preferences. Used for
// decred ticket purchasing.
func (c *Core) StakeStatus() (*asset.TicketStakingStatus, error) {
_, tb, err := c.dcrStakingWallet()
if err != nil {
return nil, err
}
return tb.StakeStatus()
}

// SetVSP sets the VSP provider. Used for decred ticket purchasing.
func (c *Core) SetVSP(addr string) error {
_, tb, err := c.dcrStakingWallet()
if err != nil {
return err
}
return tb.SetVSP(addr)
}

// PurchaseTickets purchases n amout of tickets. Returns the purchased
// ticket hashes if successful.Used for decred ticket purchasing.
func (c *Core) PurchaseTickets(pw []byte, n int) ([]string, error) {
wallet, tb, err := c.dcrStakingWallet()
if err != nil {
return nil, err
}
crypter, err := c.encryptionKey(pw)
if err != nil {
return nil, fmt.Errorf("password error: %w", err)
}
defer crypter.Close()
err = c.connectAndUnlock(crypter, wallet)
if err != nil {
return nil, err
}

// TODO: Send tickets bought notification.
//subject, details := c.formatDetails(TopicSendSuccess, sentValue, unbip(assetID), address, coin)
//c.notify(newSendNote(TopicSendSuccess, subject, details, db.Success))

dcrAssetID := uint32(42)
c.updateAssetBalance(dcrAssetID)
return tb.PurchaseTickets(n)
}

// SetVotingPreferences sets default voting settings for all active
// tickets and future tickets. Nil maps can be provided for no change.
func (c *Core) SetVotingPreferences(choices, tSpendPolicy, treasuryPolicy map[string]string) error {
_, tb, err := c.dcrStakingWallet()
if err != nil {
return err
}
return tb.SetVotingPreferences(choices, tSpendPolicy, treasuryPolicy)
}
170 changes: 162 additions & 8 deletions client/rpcserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ const (
startMarketMakingRoute = "startmarketmaking"
stopMarketMakingRoute = "stopmarketmaking"
multiTradeRoute = "multitrade"
stakeStatusRoute = "stakestatus"
setVSPRoute = "setvsp"
purchaseTicketsRoute = "purchasetickets"
setVotingPreferencesRoute = "setvotingprefs"
)

const (
Expand All @@ -65,6 +69,8 @@ const (
canceledOrderStr = "canceled order %s"
logoutStr = "goodbye"
walletStatusStr = "%s wallet has been %s"
setVotePrefsStr = "vote preferences set"
setVSPStr = "vsp set to %s"
)

// createResponse creates a msgjson response payload.
Expand Down Expand Up @@ -116,10 +122,14 @@ var routes = map[string]func(s *RPCServer, params *RawParams) *msgjson.ResponseP
walletPeersRoute: handleWalletPeers,
addWalletPeerRoute: handleAddWalletPeer,
removeWalletPeerRoute: handleRemoveWalletPeer,
notificationsRoute: handleNotificationsRoute,
startMarketMakingRoute: handleStartMarketMakingRoute,
stopMarketMakingRoute: handleStopMarketMakingRoute,
notificationsRoute: handleNotifications,
startMarketMakingRoute: handleStartMarketMaking,
stopMarketMakingRoute: handleStopMarketMaking,
multiTradeRoute: handleMultiTrade,
stakeStatusRoute: handleStakeStatus,
setVSPRoute: handleSetVSP,
purchaseTicketsRoute: handlePurchaseTickets,
setVotingPreferencesRoute: handleSetVotingPreferences,
}

// handleHelp handles requests for help. Returns general help for all commands
Expand Down Expand Up @@ -888,15 +898,15 @@ func handleRemoveWalletPeer(s *RPCServer, params *RawParams) *msgjson.ResponsePa
return createResponse(removeWalletPeerRoute, "successfully removed peer", nil)
}

func handleNotificationsRoute(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
func handleNotifications(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
numNotes, err := parseNotificationsArgs(params)
if err != nil {
return usage(notificationsRoute, err)
}

notes, err := s.core.Notifications(numNotes)
if err != nil {
errMsg := fmt.Sprintf("unable to remove wallet peer: %v", err)
errMsg := fmt.Sprintf("unable to handle notification: %v", err)
resErr := msgjson.NewError(msgjson.RPCNotificationsError, errMsg)
return createResponse(notificationsRoute, nil, resErr)
}
Expand All @@ -921,7 +931,7 @@ func parseMarketMakingConfig(path string) ([]*mm.BotConfig, error) {
return configs, nil
}

func handleStartMarketMakingRoute(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
func handleStartMarketMaking(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
form, err := parseStartMarketMakingArgs(params)
if err != nil {
return usage(startMarketMakingRoute, err)
Expand All @@ -944,7 +954,7 @@ func handleStartMarketMakingRoute(s *RPCServer, params *RawParams) *msgjson.Resp
return createResponse(startMarketMakingRoute, "started market making", nil)
}

func handleStopMarketMakingRoute(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
func handleStopMarketMaking(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
if !s.mm.Running() {
errMsg := "market making is not running"
resErr := msgjson.NewError(msgjson.RPCStopMarketMakingError, errMsg)
Expand All @@ -954,6 +964,66 @@ func handleStopMarketMakingRoute(s *RPCServer, params *RawParams) *msgjson.Respo
return createResponse(stopMarketMakingRoute, "stopped market making", nil)
}

func handleSetVSP(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
addr, err := parseSetVSPArgs(params)
if err != nil {
return usage(setVSPRoute, err)
}

err = s.core.SetVSP(addr)
if err != nil {
errMsg := fmt.Sprintf("unable to set vsp: %v", err)
resErr := msgjson.NewError(msgjson.RPCSetVSPError, errMsg)
return createResponse(setVSPRoute, nil, resErr)
}

return createResponse(setVSPRoute, fmt.Sprintf(setVSPStr, addr), nil)
}

func handlePurchaseTickets(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
form, err := parsePurchaseTicketsArgs(params)
if err != nil {
return usage(purchaseTicketsRoute, err)
}
defer form.appPass.Clear()

hashes, err := s.core.PurchaseTickets(form.appPass, form.num)
if err != nil {
errMsg := fmt.Sprintf("unable to purchase tickets: %v", err)
resErr := msgjson.NewError(msgjson.RPCPurchaseTicketsError, errMsg)
return createResponse(purchaseTicketsRoute, nil, resErr)
}

return createResponse(purchaseTicketsRoute, hashes, nil)
}

func handleStakeStatus(s *RPCServer, _ *RawParams) *msgjson.ResponsePayload {
stakeStatus, err := s.core.StakeStatus()
if err != nil {
errMsg := fmt.Sprintf("unable to get staking status: %v", err)
resErr := msgjson.NewError(msgjson.RPCStakeStatusError, errMsg)
return createResponse(stakeStatusRoute, nil, resErr)
}

return createResponse(stakeStatusRoute, &stakeStatus, nil)
}

func handleSetVotingPreferences(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
form, err := parseSetVotingPreferencesArgs(params)
if err != nil {
return usage(setVotingPreferencesRoute, err)
}

err = s.core.SetVotingPreferences(form.voteChoices, form.tSpendPolicy, form.treasuryPolicy)
if err != nil {
errMsg := fmt.Sprintf("unable to set voting preferences: %v", err)
resErr := msgjson.NewError(msgjson.RPCSetVotingPreferencesError, errMsg)
return createResponse(setVotingPreferencesRoute, nil, resErr)
}

return createResponse(setVotingPreferencesRoute, "vote preferences set", nil)
}

// format concatenates thing and tail. If thing is empty, returns an empty
// string.
func format(thing, tail string) string {
Expand Down Expand Up @@ -1385,7 +1455,7 @@ Registration is complete after the fee transaction has been confirmed.`,
cmdSummary: `Initiate a rescan of an asset's wallet. This is only supported for certain
wallet types. Wallet resynchronization may be asynchronous, and the wallet
state should be consulted for progress.
WARNING: It is ill-advised to initiate a wallet rescan with active orders
unless as a last ditch effort to get the wallet to recognize a transaction
needed to complete a swap.`,
Expand Down Expand Up @@ -1584,4 +1654,88 @@ needed to complete a swap.`,
stopMarketMakingRoute: {
cmdSummary: `Stop market making.`,
},
stakeStatusRoute: {
cmdSummary: `Get stake status. `,
returns: `Returns:
obj: The staking status.
{
ticketprice (uint64): The current ticket price in atoms.
vsp (string): The url of the currently set vsp (voting service provider).
isrpc (bool): Whether the wallet is an RPC wallet. False indicates
an spv wallet and allows options that can view and set the vsp.
tickets (array): An array of ticket objects.
[
{
ticket (obj): Ticket transaction data.
{
hash (string): The ticket hash as hex.
ticketPrice (int): The amount paid for the ticket in atoms.
fees (int): The ticket transaction's tx fee.
stamp (int): The UNIX time the ticket was purchased.
blockheight (int): The block number the ticket was mined.
},
status: (int) The ticket status. 0 is TicketStatusUnknown. 1 is TicketStatusUnmined.
2 is TicketStatusImmature. 3 is TicketStatusLive. 4 is TicketStatusVoted.
5 is TicketStatusMissed. 6 is TicketStatusExpired. 7 is TicketStatusUnspent.
8 is TicketStatusRevoked.
spender (string): The transaction that votes on or revokes the ticket if available.
},
],...
stances (obj): Voting policies.
{
votechoices (array): An array of consensus vote choices.
[
{
agendaid (string): The agenda ID,
agendadescription (string): A description of the agenda being voted on.
choiceid (string): The current choice.
choicedescription (string): A description of the chosen choice.
},
],...
tspendpolicy (array): An array of TSpend policies.
[
{
hash (string): The TSpend txid.,
policy (string): The policy.
},
],...
treasurypolicy (array): An array of treasury policies.
[
{
key (string): The pubkey of the tspend creator.
policy (string): The policy.
},
],...
}
}`,
},
setVSPRoute: {
argsShort: `"addr"`,
cmdSummary: `Set a vsp by url.`,
argsLong: `Args:
addr (string): The vsp's url.`,
returns: `Returns:
string: The message "` + fmt.Sprintf(setVSPStr, "[vsp url]") + `"`,
},
purchaseTicketsRoute: {
pwArgsShort: `"appPass"`,
argsShort: `num`,
cmdSummary: `Purchase some tickets.`,
pwArgsLong: `Password Args:
appPass (string): The DEX client password.`,
argsLong: `Args:
num (int): The number of tickets to purchase`,
returns: `Returns:
array: An array of ticket hashes.`,
},
setVotingPreferencesRoute: {
argsShort: `(choicesMap) (tSpendPolicyMap) (treasuryPolicyMap)`,
cmdSummary: `Cancel an order.`,
argsLong: `Args:
choicesMap (map[string]string): A map of choices IDs to choice policies.
tSpendPolicyMap (map[string]string): A map of tSpend txids to tSpend policies.
treasuryPolicyMap (map[string]string): A map of treasury spender public keys to tSpend policies.`,
returns: `Returns:
string: The message "` + setVotePrefsStr + `"`,
},
}
Loading

0 comments on commit 25a52a4

Please sign in to comment.