Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client/asset: Add WalletHistorian interface and RPC method #2500

Merged
merged 2 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
buck54321 marked this conversation as resolved.
Show resolved Hide resolved
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.
buck54321 marked this conversation as resolved.
Show resolved Hide resolved
type TransactionType uint16

const (
Send TransactionType = iota
Receive
Swap
Redeem
Refund
CreateBond
RedeemBond
ApproveToken
Acceleration
)
buck54321 marked this conversation as resolved.
Show resolved Hide resolved

// 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"`
buck54321 marked this conversation as resolved.
Show resolved Hide resolved
Fees uint64 `json:"fees"`
BlockNumber uint64 `json:"blockNumber"`
buck54321 marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Comment on lines +1062 to +1072
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are a lot of transactions, this could be pretty big. Maybe add a hard limit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the front ends should handle that. I'll update the RPC server to have a default of 10. Would something go wrong if too many are requested?


// 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
3 changes: 3 additions & 0 deletions client/rpcserver/rpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ func (c *TCore) StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error)
func (c *TCore) SetVotingPreferences(assetID uint32, choices, tSpendPolicy, treasuryPolicy map[string]string) error {
return c.setVotingPrefErr
}
func (c *TCore) TxHistory(assetID uint32, n int, refID *dex.Bytes, past bool) ([]*asset.WalletTransaction, error) {
return nil, nil
}

type tBookFeed struct{}

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
Loading