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 16, 2023
1 parent e166742 commit ea84ec5
Show file tree
Hide file tree
Showing 11 changed files with 703 additions and 12 deletions.
23 changes: 19 additions & 4 deletions client/asset/dcr/rpcwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -912,26 +912,41 @@ func (w *rpcWallet) Tickets(ctx context.Context) ([]*asset.Ticket, error) {
w.log.Errorf("GetTransaction error for ticket %s: %v", h, err)
continue
}
blockHeight := int64(-1)
// If the transaction is not yet mined we do not know the block hash.
if tx.BlockHash != "" {
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
}
blockHeight = int64(hdr.Height)
}
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: blockHeight,
},
// The walletjson.GetTransactionResult returned from GetTransaction
// actually has a TicketStatus string field, but it doesn't appear
Expand Down
3 changes: 3 additions & 0 deletions client/asset/dcr/spv.go
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,9 @@ func (w *spvWallet) ticketsInRange(ctx context.Context, fromHeight, toHeight int

tickets := make([]*asset.Ticket, 0)

// TODO: This does not seem to return tickets withought confirmations.
// Investigate this and return unconfirmed tickets with block height
// set to -1 if possible.
processTicket := func(ticketSummaries []*wallet.TicketSummary, hdr *wire.BlockHeader) (bool, error) {
for _, ticketSummary := range ticketSummaries {
spender := ""
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
74 changes: 74 additions & 0 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -10498,3 +10498,77 @@ func (c *Core) SendShielded(appPW []byte, assetID uint32, toAddr string, amt uin

return coinID, nil
}

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

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

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

// PurchaseTickets purchases n tickets. Returns the purchased ticket hashes if
// successful. Used for ticket purchasing.
func (c *Core) PurchaseTickets(assetID uint32, pw []byte, n int) ([]string, error) {
wallet, tb, err := c.stakingWallet(assetID)
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
}
hashes, err := tb.PurchaseTickets(n)
if err != nil {
return nil, err
}
c.updateAssetBalance(assetID)
// TODO: Send tickets bought notification.
//subject, details := c.formatDetails(TopicSendSuccess, sentValue, unbip(assetID), address, coin)
//c.notify(newSendNote(TopicSendSuccess, subject, details, db.Success))
return hashes, nil
}

// SetVotingPreferences sets default voting settings for all active tickets and
// future tickets. Nil maps can be provided for no change. Used for ticket
// purchasing.
func (c *Core) SetVotingPreferences(assetID uint32, choices, tSpendPolicy,
treasuryPolicy map[string]string) error {
_, tb, err := c.stakingWallet(assetID)
if err != nil {
return err
}
return tb.SetVotingPreferences(choices, tSpendPolicy, treasuryPolicy)
}
178 changes: 170 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,70 @@ func handleStopMarketMakingRoute(s *RPCServer, params *RawParams) *msgjson.Respo
return createResponse(stopMarketMakingRoute, "stopped market making", nil)
}

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

err = s.core.SetVSP(form.assetID, form.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, form.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.assetID, 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, params *RawParams) *msgjson.ResponsePayload {
assetID, err := parseStakeStatusArgs(params)
if err != nil {
return usage(stakeStatusRoute, err)
}
stakeStatus, err := s.core.StakeStatus(assetID)
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.assetID, 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 +1459,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 +1658,92 @@ needed to complete a swap.`,
stopMarketMakingRoute: {
cmdSummary: `Stop market making.`,
},
stakeStatusRoute: {
cmdSummary: `Get stake status. `,
argsShort: `assetID`,
argsLong: `Args:
assetID (int): The asset's BIP-44 registered coin index.`,
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 enables options to 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: unknown, 1: unmined, 2: immature, 3: live,
4: voted, 5: missed, 6:expired, 7: unspent, 8: revoked.
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: `assetID "addr"`,
cmdSummary: `Set a vsp by url.`,
argsLong: `Args:
assetID (int): The asset's BIP-44 registered coin index.
addr (string): The vsp's url.`,
returns: `Returns:
string: The message "` + fmt.Sprintf(setVSPStr, "[vsp url]") + `"`,
},
purchaseTicketsRoute: {
pwArgsShort: `"appPass"`,
argsShort: `assetID num`,
cmdSummary: `Purchase some tickets.`,
pwArgsLong: `Password Args:
appPass (string): The DEX client password.`,
argsLong: `Args:
assetID (int): The asset's BIP-44 registered coin index.
num (int): The number of tickets to purchase`,
returns: `Returns:
array: An array of ticket hashes.`,
},
setVotingPreferencesRoute: {
argsShort: `assetID (choicesMap) (tSpendPolicyMap) (treasuryPolicyMap)`,
cmdSummary: `Cancel an order.`,
argsLong: `Args:
assetID (int): The asset's BIP-44 registered coin index.
choicesMap ({"agendaid": "choiceid", ...}): A map of choices IDs to choice policies.
tSpendPolicyMap ({"hash": "policy", ...}): A map of tSpend txids to tSpend policies.
treasuryPolicyMap ({"key": "policy", ...}): A map of treasury spender public keys to tSpend policies.`,
returns: `Returns:
string: The message "` + setVotePrefsStr + `"`,
},
}
Loading

0 comments on commit ea84ec5

Please sign in to comment.