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

multi: check for used addresses #2690

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions client/asset/bch/spv.go
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,18 @@ func (w *bchSPVWallet) RemovePeer(addr string) error {
return w.peerManager.RemovePeer(addr)
}

func (w *bchSPVWallet) TotalReceivedForAddr(btcAddr btcutil.Address, minConf int32) (btcutil.Amount, error) {
bchAddr, err := dexbch.BTCAddrToBCHAddr(btcAddr, w.btcParams)
if err != nil {
return 0, err
}
amt, err := w.Wallet.TotalReceivedForAddr(bchAddr, 0)
if err != nil {
return 0, err
}
return btcutil.Amount(amt), nil
}

// secretSource is used to locate keys and redemption scripts while signing a
// transaction. secretSource satisfies the txauthor.SecretsSource interface.
type secretSource struct {
Expand Down
8 changes: 7 additions & 1 deletion client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,7 @@ var _ asset.Authenticator = (*ExchangeWalletFullNode)(nil)
var _ asset.Authenticator = (*ExchangeWalletAccelerator)(nil)
var _ asset.AddressReturner = (*baseWallet)(nil)
var _ asset.WalletHistorian = (*ExchangeWalletSPV)(nil)
var _ asset.NewAddresser = (*baseWallet)(nil)

// RecoveryCfg is the information that is transferred from the old wallet
// to the new one when the wallet is recovered.
Expand Down Expand Up @@ -4257,6 +4258,11 @@ func (btc *baseWallet) NewAddress() (string, error) {
return btc.DepositAddress()
}

// AddressUsed checks if a wallet address has been used.
func (btc *baseWallet) AddressUsed(addrStr string) (bool, error) {
return btc.node.addressUsed(addrStr)
}

// EstimateRegistrationTxFee returns an estimate for the tx fee needed to
// pay the registration fee using the provided feeRate.
func (btc *baseWallet) EstimateRegistrationTxFee(feeRate uint64) uint64 {
Expand Down Expand Up @@ -5398,7 +5404,7 @@ func (btc *intermediaryWallet) checkPendingTxs(tip uint64) {
}
}

btc.addTxToHistory(asset.Receive, txHash, toSatoshi(tx.Amount), fee, nil, nil, true)
btc.addTxToHistory(asset.Receive, txHash, toSatoshi(tx.Amount), fee, nil, &tx.Address, true)
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions client/asset/btc/electrum_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1159,3 +1159,11 @@ func (ew *electrumWallet) findOutputSpender(ctx context.Context, txHash *chainha

return nil, 0, nil // caller should check msgTx (internal method)
}

func (ew *electrumWallet) addressUsed(addrStr string) (bool, error) {
txs, err := ew.wallet.GetAddressHistory(ew.ctx, addrStr)
if err != nil {
return false, fmt.Errorf("error getting address history: %w", err)
}
return len(txs) > 0, nil
}
72 changes: 41 additions & 31 deletions client/asset/btc/rpcclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,37 +29,38 @@ import (
)

const (
methodGetBalances = "getbalances"
methodGetBalance = "getbalance"
methodListUnspent = "listunspent"
methodLockUnspent = "lockunspent"
methodListLockUnspent = "listlockunspent"
methodChangeAddress = "getrawchangeaddress"
methodNewAddress = "getnewaddress"
methodSignTx = "signrawtransactionwithwallet"
methodSignTxLegacy = "signrawtransaction"
methodUnlock = "walletpassphrase"
methodLock = "walletlock"
methodPrivKeyForAddress = "dumpprivkey"
methodGetTransaction = "gettransaction"
methodSendToAddress = "sendtoaddress"
methodSetTxFee = "settxfee"
methodGetWalletInfo = "getwalletinfo"
methodGetAddressInfo = "getaddressinfo"
methodListDescriptors = "listdescriptors"
methodValidateAddress = "validateaddress"
methodEstimateSmartFee = "estimatesmartfee"
methodSendRawTransaction = "sendrawtransaction"
methodGetTxOut = "gettxout"
methodGetBlock = "getblock"
methodGetBlockHash = "getblockhash"
methodGetBestBlockHash = "getbestblockhash"
methodGetRawMempool = "getrawmempool"
methodGetRawTransaction = "getrawtransaction"
methodGetBlockHeader = "getblockheader"
methodGetNetworkInfo = "getnetworkinfo"
methodGetBlockchainInfo = "getblockchaininfo"
methodFundRawTransaction = "fundrawtransaction"
methodGetBalances = "getbalances"
methodGetBalance = "getbalance"
methodListUnspent = "listunspent"
methodLockUnspent = "lockunspent"
methodListLockUnspent = "listlockunspent"
methodChangeAddress = "getrawchangeaddress"
methodNewAddress = "getnewaddress"
methodSignTx = "signrawtransactionwithwallet"
methodSignTxLegacy = "signrawtransaction"
methodUnlock = "walletpassphrase"
methodLock = "walletlock"
methodPrivKeyForAddress = "dumpprivkey"
methodGetTransaction = "gettransaction"
methodSendToAddress = "sendtoaddress"
methodSetTxFee = "settxfee"
methodGetWalletInfo = "getwalletinfo"
methodGetAddressInfo = "getaddressinfo"
methodListDescriptors = "listdescriptors"
methodValidateAddress = "validateaddress"
methodEstimateSmartFee = "estimatesmartfee"
methodSendRawTransaction = "sendrawtransaction"
methodGetTxOut = "gettxout"
methodGetBlock = "getblock"
methodGetBlockHash = "getblockhash"
methodGetBestBlockHash = "getbestblockhash"
methodGetRawMempool = "getrawmempool"
methodGetRawTransaction = "getrawtransaction"
methodGetBlockHeader = "getblockheader"
methodGetNetworkInfo = "getnetworkinfo"
methodGetBlockchainInfo = "getblockchaininfo"
methodFundRawTransaction = "fundrawtransaction"
methodGetReceivedByAddress = "getreceivedbyaddress"
)

// IsTxNotFoundErr will return true if the error indicates that the requested
Expand Down Expand Up @@ -1133,6 +1134,15 @@ func SearchBlockForRedemptions(
return
}

func (wc *rpcClient) addressUsed(addr string) (bool, error) {
var recv float64
const minConf = 0
if err := wc.call(methodGetReceivedByAddress, []any{addr, minConf}, &recv); err != nil {
return false, err
}
return recv != 0, nil
}

// call is used internally to marshal parameters and send requests to the RPC
// server via (*rpcclient.Client).RawRequest. If thing is non-nil, the result
// will be marshaled into thing.
Expand Down
4 changes: 4 additions & 0 deletions client/asset/btc/spv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ func (c *tBtcWallet) RemovePeer(string) error {
return nil
}

func (c *tBtcWallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcutil.Amount, error) {
return 0, nil
}

type tNeutrinoClient struct {
*testData
}
Expand Down
15 changes: 15 additions & 0 deletions client/asset/btc/spv_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ type BTCWallet interface {
AddPeer(string) error
RemovePeer(string) error
ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error)
TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcutil.Amount, error)
}

type XCWalletAccount struct {
Expand Down Expand Up @@ -1653,6 +1654,20 @@ func (w *spvWallet) getWalletTransaction(txHash *chainhash.Hash) (*GetTransactio
*/
}

func (w *spvWallet) addressUsed(addrStr string) (bool, error) {
addr, err := w.decodeAddr(addrStr, w.chainParams)
if err != nil {
return false, fmt.Errorf("error decoding address: %w", err)
}

const minConfs = 0
amt, err := w.wallet.TotalReceivedForAddr(addr, minConfs)
if err != nil {
return false, fmt.Errorf("error getting address received: %v", err)
}
return amt != 0, nil
}

func confirms(txHeight, curHeight int32) int32 {
switch {
case txHeight == -1, txHeight > curHeight:
Expand Down
1 change: 1 addition & 0 deletions client/asset/btc/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Wallet interface {
ownsAddress(addr btcutil.Address) (bool, error) // this should probably just take a string
getWalletTransaction(txHash *chainhash.Hash) (*GetTransactionResult, error)
reconfigure(walletCfg *asset.WalletConfig, currentAddress string) (restartRequired bool, err error)
addressUsed(addr string) (bool, error)
}

type txLister interface {
Expand Down
20 changes: 18 additions & 2 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,7 @@ var _ asset.TxFeeEstimator = (*ExchangeWallet)(nil)
var _ asset.Bonder = (*ExchangeWallet)(nil)
var _ asset.Authenticator = (*ExchangeWallet)(nil)
var _ asset.TicketBuyer = (*ExchangeWallet)(nil)
var _ asset.NewAddresser = (*ExchangeWallet)(nil)

type block struct {
height int64
Expand Down Expand Up @@ -4008,6 +4009,11 @@ func (dcr *ExchangeWallet) NewAddress() (string, error) {
return dcr.DepositAddress()
}

// AddressUsed checks if a wallet address has been used.
func (dcr *ExchangeWallet) AddressUsed(addrStr string) (bool, error) {
return dcr.wallet.AddressUsed(dcr.ctx, addrStr)
}

// Unlock unlocks the exchange wallet.
func (dcr *ExchangeWallet) Unlock(pw []byte) error {
// Older SPV wallet potentially need an upgrade while we have a password.
Expand Down Expand Up @@ -5704,7 +5710,8 @@ func (dcr *ExchangeWallet) checkPendingTxs(ctx context.Context, tip uint64) {
blockToQuery = tip - blockQueryBuffer
}

recentTxs, err := dcr.wallet.ListSinceBlock(ctx, int32(blockToQuery), int32(tip), int32(tip))
const rangeEndMempool = -1
recentTxs, err := dcr.wallet.ListSinceBlock(ctx, int32(blockToQuery), rangeEndMempool, int32(tip))
if err != nil {
dcr.log.Errorf("Error listing transactions since block %d: %v", blockToQuery, err)
recentTxs = nil
Expand Down Expand Up @@ -5742,7 +5749,12 @@ func (dcr *ExchangeWallet) checkPendingTxs(ctx context.Context, tip uint64) {
}
}

dcr.addTxToHistory(txType, txHash, toAtoms(tx.Amount), fee, nil, nil, true)
var addr *string
if txType == asset.Receive {
addr = &tx.Address
}

dcr.addTxToHistory(txType, txHash, toAtoms(tx.Amount), fee, nil, addr, true)
}

for _, tx := range recentTxs {
Expand Down Expand Up @@ -6065,6 +6077,10 @@ func (dcr *ExchangeWallet) monitorBlocks(ctx context.Context) {
if walletTip == nil {
// Mempool tx seen.
dcr.emitBalance()
dcr.tipMtx.RLock()
tipHeight := uint64(dcr.currentTip.height)
dcr.tipMtx.RUnlock()
go dcr.checkPendingTxs(ctx, tipHeight)
continue
}
if queuedBlock != nil && walletTip.height >= queuedBlock.height {
Expand Down
4 changes: 4 additions & 0 deletions client/asset/dcr/dcr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,10 @@ func (c *tRPCClient) SetTxFee(ctx context.Context, fee dcrutil.Amount) error {
return nil
}

func (c *tRPCClient) GetReceivedByAddressMinConf(ctx context.Context, address stdaddr.Address, minConfs int) (dcrutil.Amount, error) {
return 0, nil
}

func (c *tRPCClient) ListSinceBlock(ctx context.Context, hash *chainhash.Hash) (*walletjson.ListSinceBlockResult, error) {
return nil, nil
}
Expand Down
14 changes: 14 additions & 0 deletions client/asset/dcr/rpcwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ type rpcClient interface {
SetVoteChoice(ctx context.Context, agendaID, choiceID string) error
SetTxFee(ctx context.Context, fee dcrutil.Amount) error
ListSinceBlock(ctx context.Context, hash *chainhash.Hash) (*walletjson.ListSinceBlockResult, error)
GetReceivedByAddressMinConf(ctx context.Context, address stdaddr.Address, minConfs int) (dcrutil.Amount, error)
}

// newRPCWallet creates an rpcClient and uses it to construct a new instance
Expand Down Expand Up @@ -1149,6 +1150,19 @@ func (w *rpcWallet) SetTxFee(ctx context.Context, feePerKB dcrutil.Amount) error
return w.rpcClient.SetTxFee(ctx, feePerKB)
}

func (w *rpcWallet) AddressUsed(ctx context.Context, addrStr string) (bool, error) {
addr, err := stdaddr.DecodeAddress(addrStr, w.chainParams)
if err != nil {
return false, err
}
const minConf = 0
recv, err := w.rpcClient.GetReceivedByAddressMinConf(ctx, addr, minConf)
if err != nil {
return false, err
}
return recv != 0, nil
}

// anylist is a list of RPC parameters to be converted to []json.RawMessage and
// sent via nodeRawRequest.
type anylist []any
Expand Down
14 changes: 14 additions & 0 deletions client/asset/dcr/spv.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ type dcrWallet interface {
SetRelayFee(relayFee dcrutil.Amount)
GetTicketInfo(ctx context.Context, hash *chainhash.Hash) (*wallet.TicketSummary, *wire.BlockHeader, error)
ListSinceBlock(ctx context.Context, start, end, syncHeight int32) ([]walletjson.ListTransactionsResult, error)
TotalReceivedForAddr(ctx context.Context, addr stdaddr.Address, minConf int32) (dcrutil.Amount, error)
vspclient.Wallet
// TODO: Rescan and DiscoverActiveAddresses can be used for a Rescanner.
}
Expand Down Expand Up @@ -1304,6 +1305,19 @@ func (w *spvWallet) SetTxFee(_ context.Context, feePerKB dcrutil.Amount) error {
return nil
}

func (w *spvWallet) AddressUsed(ctx context.Context, addrStr string) (bool, error) {
addr, err := stdaddr.DecodeAddress(addrStr, w.chainParams)
if err != nil {
return false, err
}
const minConf = 0
recv, err := w.TotalReceivedForAddr(ctx, addr, minConf)
if err != nil {
return false, err
}
return recv != 0, nil
}

// cacheBlock caches a block for future use. The block has a lastAccess stamp
// added, and will be discarded if not accessed again within 2 hours.
func (w *spvWallet) cacheBlock(block *wire.MsgBlock) {
Expand Down
4 changes: 4 additions & 0 deletions client/asset/dcr/spv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,10 @@ func (w *tDcrWallet) ListSinceBlock(ctx context.Context, start, end, syncHeight
return nil, nil
}

func (w *tDcrWallet) TotalReceivedForAddr(ctx context.Context, addr stdaddr.Address, minConf int32) (dcrutil.Amount, error) {
return 0, nil
}

func tNewSpvWallet() (*spvWallet, *tDcrWallet) {
dcrw := &tDcrWallet{
blockHeader: make(map[chainhash.Hash]*wire.BlockHeader),
Expand Down
1 change: 1 addition & 0 deletions client/asset/dcr/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ type Wallet interface {
SetTxFee(ctx context.Context, feePerKB dcrutil.Amount) error
StakeInfo(ctx context.Context) (*wallet.StakeInfoData, error)
Reconfigure(ctx context.Context, cfg *asset.WalletConfig, net dex.Network, currentAddress string) (restart bool, err error)
AddressUsed(ctx context.Context, addrStr string) (bool, error)
}

// WalletTransaction is a pared down version of walletjson.GetTransactionResult.
Expand Down
1 change: 1 addition & 0 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ type Sweeper interface {
// NewAddresser is a wallet that can generate new deposit addresses.
type NewAddresser interface {
NewAddress() (string, error)
AddressUsed(string) (bool, error)
}

// AddressReturner is a wallet that allows recycling of unused redemption or refund
Expand Down
12 changes: 12 additions & 0 deletions client/asset/ltc/spv.go
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,18 @@ func (w *ltcSPVWallet) RemovePeer(addr string) error {
return w.peerManager.RemovePeer(addr)
}

func (w *ltcSPVWallet) TotalReceivedForAddr(btcAddr btcutil.Address, minConf int32) (btcutil.Amount, error) {
ltcAddr, err := w.addrBTC2LTC(btcAddr)
if err != nil {
return 0, err
}
amt, err := w.Wallet.TotalReceivedForAddr(ltcAddr, 0)
if err != nil {
return 0, err
}
return btcutil.Amount(amt), nil
}

// secretSource is used to locate keys and redemption scripts while signing a
// transaction. secretSource satisfies the txauthor.SecretsSource interface.
type secretSource struct {
Expand Down
6 changes: 6 additions & 0 deletions client/asset/zec/transparent_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,9 @@ func syncStatus(c rpcCaller) (*btc.SyncStatus, error) {
Syncing: chainInfo.Syncing(),
}, nil
}

func getReceivedByAddress(c rpcCaller, addrStr string) (recv uint64, _ error) {
const minConf = 0
const inZats = true
return recv, c.CallRPC("getreceivedbyaddress", []any{addrStr, minConf, inZats}, &recv)
}
8 changes: 8 additions & 0 deletions client/asset/zec/zec.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ type zecWallet struct {

var _ asset.FeeRater = (*zecWallet)(nil)
var _ asset.Wallet = (*zecWallet)(nil)
var _ asset.NewAddresser = (*zecWallet)(nil)

// DRAFT TODO: Implement LiveReconfigurer
// var _ asset.LiveReconfigurer = (*zecWallet)(nil)
Expand Down Expand Up @@ -1573,6 +1574,13 @@ func (w *zecWallet) NewAddress() (string, error) {
return w.DepositAddress()
}

// AddressUsed checks if a wallet address has been used.
func (w *zecWallet) AddressUsed(addrStr string) (bool, error) {
// TODO: Resolve with new unified address encoding in https://github.com/decred/dcrdex/pull/2675
recv, err := getReceivedByAddress(w, addrStr)
return recv != 0, err
}

// DEPRECATED
func (w *zecWallet) EstimateRegistrationTxFee(feeRate uint64) uint64 {
return math.MaxUint64
Expand Down