Skip to content

Commit

Permalink
client/asset,rpcserver: Add WalletHistorian interface and RPC method
Browse files Browse the repository at this point in the history
This adds a `WalletHistorian` interface which exposes a `TxHistory`
function. This will allow wallets to display the history of their
transactions. Also adds a method to the RPC server to retrieve the
wallet history.
  • Loading branch information
martonp committed Sep 7, 2023
1 parent 883c935 commit 42bc5dc
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 0 deletions.
51 changes: 51 additions & 0 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
WalletTraitTokenApprover // The wallet is a TokenApprover
WalletTraitAccountLocker // The wallet must have enough balance for redemptions before a trade.
WalletTraitTicketBuyer // The wallet can participate in decred staking.
WalletTraitHistorian // This wallet can return its transaction history
)

// IsRescanner tests if the WalletTrait has the WalletTraitRescanner bit set.
Expand Down Expand Up @@ -131,6 +132,12 @@ func (wt WalletTrait) IsTicketBuyer() bool {
return wt&WalletTraitTicketBuyer != 0
}

// IsHistorian tests if the WalletTrait has the WalletTraitHistorian bit set,
// which indicates the wallet implements the WalletHistorian interface.
func (wt WalletTrait) IsHistorian() bool {
return wt&WalletTraitHistorian != 0
}

// DetermineWalletTraits returns the WalletTrait bitset for the provided Wallet.
func DetermineWalletTraits(w Wallet) (t WalletTrait) {
if _, is := w.(Rescanner); is {
Expand Down Expand Up @@ -181,6 +188,9 @@ func DetermineWalletTraits(w Wallet) (t WalletTrait) {
if _, is := w.(TicketBuyer); is {
t |= WalletTraitTicketBuyer
}
if _, is := w.(TicketBuyer); is {
t |= WalletTraitHistorian
}
return t
}

Expand Down Expand Up @@ -1017,6 +1027,47 @@ type TicketBuyer interface {
TicketPage(scanStart int32, n, skipN int) ([]*Ticket, error)
}

// TransactionType is type type of transaction made by a wallet.
type TransactionType uint16

const (
Send TransactionType = iota
Receive
Swap
Redeem
Refund
CreateBond
RedeemBond
ApproveToken
Acceleration
)

// WalletTransaction represents a transaction that was made by a wallet.
type WalletTransaction struct {
Type TransactionType `json:"type"`
ID dex.Bytes `json:"id"`
AmtIn uint64 `json:"amtIn"`
AmtOut uint64 `json:"amtOut"`
Fees uint64 `json:"fees"`
BlockNumber uint64 `json:"blockNumber"`
UpdatedBalance uint64 `json:"updatedBalance"`
// AdditionalData contains asset specific information, i.e. nonce
// for ETH.
AdditionalData map[string]string `json:"additionalData"`
}

// WalletHistorian is a wallet that is able to retrieve the history of all
// transactions it has made.
type WalletHistorian interface {
// TxHistory returns all the transactions a wallet has made. If refID
// is nil, then transactions starting from the most recent are returned
// (past is ignored). If past is true, the transactions prior to the
// refID are returned, otherwise the transactions after the refID are
// returned. n is the number of transactions to return. If n is <= 0,
// all the transactions will be returned.
TxHistory(n int, refID *dex.Bytes, past bool) ([]*WalletTransaction, error)
}

// Bond is the fidelity bond info generated for a certain account ID, amount,
// and lock time. These data are intended for the "post bond" request, in which
// the server pre-validates the unsigned transaction, the client then publishes
Expand Down
15 changes: 15 additions & 0 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -5769,6 +5769,21 @@ func (c *Core) MultiTrade(pw []byte, form *MultiTradeForm) ([]*Order, error) {
return orders, nil
}

// TxHistory returns all the transactions a wallet has made. If refID
// is nil, then transactions starting from the most recent are returned
// (past is ignored). If past is true, the transactions prior to the
// refID are returned, otherwise the transactions after the refID are
// returned. n is the number of transactions to return. If n is <= 0,
// all the transactions will be returned
func (c *Core) TxHistory(assetID uint32, n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) {
wallet, found := c.wallet(assetID)
if !found {
return nil, newError(missingWalletErr, "no wallet found for %s", unbip(assetID))
}

return wallet.TxHistory(n, refID, past)
}

// Trade is used to place a market or limit order.
func (c *Core) Trade(pw []byte, form *TradeForm) (*Order, error) {
req, err := c.prepareTradeRequest(pw, form)
Expand Down
19 changes: 19 additions & 0 deletions client/core/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,25 @@ func (w *xcWallet) swapConfirmations(ctx context.Context, coinID []byte, contrac
return w.Wallet.SwapConfirmations(ctx, coinID, contract, time.UnixMilli(int64(matchTime)))
}

// TxHistory returns all the transactions a wallet has made. If refID
// is nil, then transactions starting from the most recent are returned
// (past is ignored). If past is true, the transactions prior to the
// refID are returned, otherwise the transactions after the refID are
// returned. n is the number of transactions to return. If n is <= 0,
// all the transactions will be returned.
func (w *xcWallet) TxHistory(n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) {
if !w.connected() {
return nil, errWalletNotConnected
}

historian, ok := w.Wallet.(asset.WalletHistorian)
if !ok {
return nil, fmt.Errorf("wallet does not support transaction history")
}

return historian.TxHistory(n, refID, past)
}

// MakeBondTx authors a DEX time-locked fidelity bond transaction if the
// asset.Wallet implementation is a Bonder.
func (w *xcWallet) MakeBondTx(ver uint16, amt, feeRate uint64, lockTime time.Time, priv *secp256k1.PrivateKey, acctID []byte) (*asset.Bond, func(), error) {
Expand Down
29 changes: 29 additions & 0 deletions client/rpcserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const (
setVSPRoute = "setvsp"
purchaseTicketsRoute = "purchasetickets"
setVotingPreferencesRoute = "setvotingprefs"
txHistoryRoute = "txhistory"
)

const (
Expand Down Expand Up @@ -130,6 +131,7 @@ var routes = map[string]func(s *RPCServer, params *RawParams) *msgjson.ResponseP
setVSPRoute: handleSetVSP,
purchaseTicketsRoute: handlePurchaseTickets,
setVotingPreferencesRoute: handleSetVotingPreferences,
txHistoryRoute: handleTxHistory,
}

// handleHelp handles requests for help. Returns general help for all commands
Expand Down Expand Up @@ -1033,6 +1035,22 @@ func handleSetVotingPreferences(s *RPCServer, params *RawParams) *msgjson.Respon
return createResponse(setVotingPreferencesRoute, "vote preferences set", nil)
}

func handleTxHistory(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
form, err := parseTxHistoryArgs(params)
if err != nil {
return usage(txHistoryRoute, err)
}

txs, err := s.core.TxHistory(form.assetID, form.num, form.refID, form.past)
if err != nil {
errMsg := fmt.Sprintf("unable to get tx history: %v", err)
resErr := msgjson.NewError(msgjson.RPCTxHistoryError, errMsg)
return createResponse(txHistoryRoute, nil, resErr)
}

return createResponse(txHistoryRoute, txs, nil)
}

// format concatenates thing and tail. If thing is empty, returns an empty
// string.
func format(thing, tail string) string {
Expand Down Expand Up @@ -1752,4 +1770,15 @@ an spv wallet and enables options to view and set the vsp.
returns: `Returns:
string: The message "` + setVotePrefsStr + `"`,
},
txHistoryRoute: {
argsShort: `assetID (n) (refTxID) (past)`,
cmdSummary: `Get transaction history for a wallet`,
argsLong: `Args:
assetID (int): The asset's BIP-44 registered coin index.
n (int): Optional. The number of transactions to return. If <= 0 or unset, all transactions are returned.
refTxID (string): Optional. If set, the transactions before or after this tx (depending on the past argument)
will be returned.
past (bool): If true, the transactions before the reference tx will be returned. If false, the
transactions after the reference tx will be returned.`,
},
}
1 change: 1 addition & 0 deletions client/rpcserver/rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type clientCore interface {
RemoveWalletPeer(assetID uint32, host string) error
Notifications(int) ([]*db.Notification, error)
MultiTrade(pw []byte, form *core.MultiTradeForm) ([]*core.Order, error)
TxHistory(assetID uint32, n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error)

// These are core's ticket buying interface.
StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error)
Expand Down
54 changes: 54 additions & 0 deletions client/rpcserver/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ type setVotingPreferencesForm struct {
voteChoices, tSpendPolicy, treasuryPolicy map[string]string
}

type txHistoryForm struct {
assetID uint32
num int
refID *dex.Bytes
past bool
}

// checkNArgs checks that args and pwArgs are the correct length.
func checkNArgs(params *RawParams, nPWArgs, nArgs []int) error {
// For want, one integer indicates an exact match, two are the min and max.
Expand Down Expand Up @@ -940,3 +947,50 @@ func parseSetVotingPreferencesArgs(params *RawParams) (*setVotingPreferencesForm
}
return form, nil
}

func parseTxHistoryArgs(params *RawParams) (*txHistoryForm, error) {
err := checkNArgs(params, []int{0}, []int{1, 4})
if err != nil {
return nil, err
}

assetID, err := checkUIntArg(params.Args[0], "assetID", 32)
if err != nil {
return nil, fmt.Errorf("invalid assetID: %v", err)
}

var num int64
if len(params.Args) > 1 {
num, err = checkIntArg(params.Args[1], "num", 64)
if err != nil {
return nil, fmt.Errorf("invalid num: %v", err)
}
}

var refID *dex.Bytes
var past bool
if len(params.Args) > 2 {
if len(params.Args) != 4 {
return nil, fmt.Errorf("refID provided without past")
}

id, err := hex.DecodeString(params.Args[2])
if err != nil {
return nil, fmt.Errorf("invalid refID: %v", err)
}
idDB := dex.Bytes(id)
refID = &idDB

past, err = checkBoolArg(params.Args[3], "past")
if err != nil {
return nil, err
}
}

return &txHistoryForm{
assetID: uint32(assetID),
num: int(num),
refID: refID,
past: past,
}, nil
}
1 change: 1 addition & 0 deletions dex/msgjson/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const (
RPCPurchaseTicketsError // 75
RPCStakeStatusError // 76
RPCSetVotingPreferencesError // 77
RPCTxHistoryError // 78
)

// Routes are destinations for a "payload" of data. The type of data being
Expand Down

0 comments on commit 42bc5dc

Please sign in to comment.