decred.org/dcrdex@v1.0.5/client/asset/dcr/rpcwallet.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package dcr
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"math"
    14  	"os"
    15  	"strings"
    16  	"sync"
    17  	"sync/atomic"
    18  	"time"
    19  
    20  	"decred.org/dcrdex/client/asset"
    21  	"decred.org/dcrdex/dex"
    22  	"decred.org/dcrwallet/v5/rpc/client/dcrwallet"
    23  	walletjson "decred.org/dcrwallet/v5/rpc/jsonrpc/types"
    24  	"decred.org/dcrwallet/v5/wallet"
    25  	"github.com/decred/dcrd/chaincfg/chainhash"
    26  	"github.com/decred/dcrd/chaincfg/v3"
    27  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    28  	"github.com/decred/dcrd/dcrjson/v4"
    29  	"github.com/decred/dcrd/dcrutil/v4"
    30  	"github.com/decred/dcrd/gcs/v4"
    31  	"github.com/decred/dcrd/gcs/v4/blockcf2"
    32  	chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
    33  	"github.com/decred/dcrd/rpcclient/v8"
    34  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    35  	"github.com/decred/dcrd/wire"
    36  )
    37  
    38  var (
    39  	compatibleWalletRPCVersions = []dex.Semver{
    40  		{Major: 11, Minor: 0, Patch: 0}, // 2.1.0 release
    41  	}
    42  	compatibleNodeRPCVersions = []dex.Semver{
    43  		{Major: 8, Minor: 0, Patch: 0}, // 1.8-pre, just dropped unused ticket RPCs
    44  		{Major: 7, Minor: 0, Patch: 0}, // 1.7 release, new gettxout args
    45  	}
    46  	// From vspWithSPVWalletRPCVersion and later the wallet's current "vsp"
    47  	// is included in the walletinfo response and the wallet will no longer
    48  	// error on GetTickets with an spv wallet.
    49  	vspWithSPVWalletRPCVersion = dex.Semver{Major: 9, Minor: 2, Patch: 0}
    50  )
    51  
    52  // RawRequest RPC methods
    53  const (
    54  	methodGetCFilterV2       = "getcfilterv2"
    55  	methodListUnspent        = "listunspent"
    56  	methodListLockUnspent    = "listlockunspent"
    57  	methodSignRawTransaction = "signrawtransaction"
    58  	methodSyncStatus         = "syncstatus"
    59  	methodGetPeerInfo        = "getpeerinfo"
    60  	methodWalletInfo         = "walletinfo"
    61  )
    62  
    63  // rpcWallet implements Wallet functionality using an rpc client to communicate
    64  // with the json-rpc server of an external dcrwallet daemon.
    65  type rpcWallet struct {
    66  	chainParams *chaincfg.Params
    67  	log         dex.Logger
    68  	rpcCfg      *rpcclient.ConnConfig
    69  	accountsV   atomic.Value // XCWalletAccounts
    70  
    71  	hasSPVTicketFunctions bool
    72  
    73  	rpcMtx  sync.RWMutex
    74  	spvMode bool
    75  	// rpcConnector is a rpcclient.Client, does not need to be
    76  	// set for testing.
    77  	rpcConnector rpcConnector
    78  	// rpcClient is a combined rpcclient.Client+dcrwallet.Client,
    79  	// or a stub for testing.
    80  	rpcClient rpcClient
    81  
    82  	connectCount uint32 // atomic
    83  }
    84  
    85  // Ensure rpcWallet satisfies the Wallet interface.
    86  var _ Wallet = (*rpcWallet)(nil)
    87  var _ Mempooler = (*rpcWallet)(nil)
    88  var _ FeeRateEstimator = (*rpcWallet)(nil)
    89  
    90  type walletClient = dcrwallet.Client
    91  
    92  type combinedClient struct {
    93  	*rpcclient.Client
    94  	*walletClient
    95  }
    96  
    97  func newCombinedClient(nodeRPCClient *rpcclient.Client, chainParams *chaincfg.Params) *combinedClient {
    98  	return &combinedClient{
    99  		nodeRPCClient,
   100  		dcrwallet.NewClient(dcrwallet.RawRequestCaller(nodeRPCClient), chainParams),
   101  	}
   102  }
   103  
   104  // Ensure combinedClient satisfies the rpcClient interface.
   105  var _ rpcClient = (*combinedClient)(nil)
   106  
   107  // ValidateAddress disambiguates the node and wallet methods of the same name.
   108  func (cc *combinedClient) ValidateAddress(ctx context.Context, address stdaddr.Address) (*walletjson.ValidateAddressWalletResult, error) {
   109  	return cc.walletClient.ValidateAddress(ctx, address)
   110  }
   111  
   112  // rpcConnector defines methods required by *rpcWallet for connecting and
   113  // disconnecting the rpcClient to/from the json-rpc server.
   114  type rpcConnector interface {
   115  	Connect(ctx context.Context, retry bool) error
   116  	Version(ctx context.Context) (map[string]chainjson.VersionResult, error)
   117  	Disconnected() bool
   118  	Shutdown()
   119  	WaitForShutdown()
   120  }
   121  
   122  // rpcClient defines rpc request methods that are used by *rpcWallet.
   123  // This is a *combinedClient or a stub for testing.
   124  type rpcClient interface {
   125  	GetCurrentNet(ctx context.Context) (wire.CurrencyNet, error)
   126  	EstimateSmartFee(ctx context.Context, confirmations int64, mode chainjson.EstimateSmartFeeMode) (*chainjson.EstimateSmartFeeResult, error)
   127  	SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error)
   128  	GetTxOut(ctx context.Context, txHash *chainhash.Hash, index uint32, tree int8, mempool bool) (*chainjson.GetTxOutResult, error)
   129  	GetBalanceMinConf(ctx context.Context, account string, minConfirms int) (*walletjson.GetBalanceResult, error)
   130  	GetBestBlock(ctx context.Context) (*chainhash.Hash, int64, error)
   131  	GetBlockHash(ctx context.Context, blockHeight int64) (*chainhash.Hash, error)
   132  	GetBlock(ctx context.Context, blockHash *chainhash.Hash) (*wire.MsgBlock, error)
   133  	GetBlockHeaderVerbose(ctx context.Context, blockHash *chainhash.Hash) (*chainjson.GetBlockHeaderVerboseResult, error)
   134  	GetBlockHeader(ctx context.Context, blockHash *chainhash.Hash) (*wire.BlockHeader, error)
   135  	GetRawMempool(ctx context.Context, txType chainjson.GetRawMempoolTxTypeCmd) ([]*chainhash.Hash, error)
   136  	LockUnspent(ctx context.Context, unlock bool, ops []*wire.OutPoint) error
   137  	GetRawChangeAddress(ctx context.Context, account string, net stdaddr.AddressParams) (stdaddr.Address, error)
   138  	GetNewAddressGapPolicy(ctx context.Context, account string, gap dcrwallet.GapPolicy) (stdaddr.Address, error)
   139  	DumpPrivKey(ctx context.Context, address stdaddr.Address) (*dcrutil.WIF, error)
   140  	GetTransaction(ctx context.Context, txHash *chainhash.Hash) (*walletjson.GetTransactionResult, error) // Should return asset.CoinNotFoundError if tx is not found.
   141  	WalletLock(ctx context.Context) error
   142  	WalletPassphrase(ctx context.Context, passphrase string, timeoutSecs int64) error
   143  	AccountUnlocked(ctx context.Context, account string) (*walletjson.AccountUnlockedResult, error)
   144  	LockAccount(ctx context.Context, account string) error
   145  	UnlockAccount(ctx context.Context, account, passphrase string) error
   146  	RawRequest(ctx context.Context, method string, params []json.RawMessage) (json.RawMessage, error)
   147  	WalletInfo(ctx context.Context) (*walletjson.WalletInfoResult, error)
   148  	ValidateAddress(ctx context.Context, address stdaddr.Address) (*walletjson.ValidateAddressWalletResult, error)
   149  	GetStakeInfo(ctx context.Context) (*walletjson.GetStakeInfoResult, error)
   150  	PurchaseTicket(ctx context.Context, fromAccount string, minConf *int,
   151  		numTickets *int,
   152  		expiry *int, ticketChange *bool, ticketFee *dcrutil.Amount) ([]*chainhash.Hash, error)
   153  	GetTickets(ctx context.Context, includeImmature bool) ([]*chainhash.Hash, error)
   154  	GetVoteChoices(ctx context.Context) (*walletjson.GetVoteChoicesResult, error)
   155  	SetVoteChoice(ctx context.Context, agendaID, choiceID string) error
   156  	SetTxFee(ctx context.Context, fee dcrutil.Amount) error
   157  	ListSinceBlock(ctx context.Context, hash *chainhash.Hash) (*walletjson.ListSinceBlockResult, error)
   158  	GetReceivedByAddressMinConf(ctx context.Context, address stdaddr.Address, minConfs int) (dcrutil.Amount, error)
   159  }
   160  
   161  // newRPCWallet creates an rpcClient and uses it to construct a new instance
   162  // of the rpcWallet. The rpcClient isn't connected to the server yet, use the
   163  // Connect method of the returned *rpcWallet to connect the rpcClient to the
   164  // server.
   165  func newRPCWallet(settings map[string]string, logger dex.Logger, net dex.Network) (*rpcWallet, error) {
   166  	cfg, chainParams, err := loadRPCConfig(settings, net)
   167  	if err != nil {
   168  		return nil, fmt.Errorf("error parsing config: %w", err)
   169  	}
   170  
   171  	// Check rpc connection config values
   172  	missing := ""
   173  	if cfg.RPCUser == "" {
   174  		missing += " username"
   175  	}
   176  	if cfg.RPCPass == "" {
   177  		missing += " password"
   178  	}
   179  	if missing != "" {
   180  		return nil, fmt.Errorf("missing dcrwallet rpc credentials:%s", missing)
   181  	}
   182  
   183  	log := logger.SubLogger("RPC")
   184  	rpcw := &rpcWallet{
   185  		chainParams: chainParams,
   186  		log:         log,
   187  	}
   188  
   189  	certs, err := os.ReadFile(cfg.RPCCert)
   190  	if err != nil {
   191  		return nil, fmt.Errorf("TLS certificate read error: %w", err)
   192  	}
   193  
   194  	log.Infof("Setting up rpc client to communicate with dcrwallet at %s with TLS certificate %q.",
   195  		cfg.RPCListen, cfg.RPCCert)
   196  	rpcw.rpcCfg = &rpcclient.ConnConfig{
   197  		Host:                cfg.RPCListen,
   198  		Endpoint:            "ws",
   199  		User:                cfg.RPCUser,
   200  		Pass:                cfg.RPCPass,
   201  		Certificates:        certs,
   202  		DisableConnectOnNew: true, // don't start until Connect
   203  	}
   204  	// Validate the RPC client config, and create a placeholder (non-nil) RPC
   205  	// connector and client that will be replaced on Connect. Any method calls
   206  	// prior to Connect will be met with rpcclient.ErrClientNotConnected rather
   207  	// than a panic.
   208  	nodeRPCClient, err := rpcclient.New(rpcw.rpcCfg, nil)
   209  	if err != nil {
   210  		return nil, fmt.Errorf("error setting up rpc client: %w", err)
   211  	}
   212  	rpcw.rpcConnector = nodeRPCClient
   213  	rpcw.rpcClient = newCombinedClient(nodeRPCClient, chainParams)
   214  
   215  	rpcw.accountsV.Store(XCWalletAccounts{
   216  		PrimaryAccount: cfg.PrimaryAccount,
   217  		UnmixedAccount: cfg.UnmixedAccount,
   218  		TradingAccount: cfg.TradingAccount,
   219  	})
   220  
   221  	return rpcw, nil
   222  }
   223  
   224  // Accounts returns the names of the accounts for use by the exchange wallet.
   225  func (w *rpcWallet) Accounts() XCWalletAccounts {
   226  	return w.accountsV.Load().(XCWalletAccounts)
   227  }
   228  
   229  // Reconfigure updates the wallet to user a new configuration.
   230  func (w *rpcWallet) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, net dex.Network, currentAddress string) (restart bool, err error) {
   231  	if !(cfg.Type == walletTypeDcrwRPC || cfg.Type == walletTypeLegacy) {
   232  		return true, nil
   233  	}
   234  
   235  	rpcCfg, chainParams, err := loadRPCConfig(cfg.Settings, net)
   236  	if err != nil {
   237  		return false, fmt.Errorf("error parsing config: %w", err)
   238  	}
   239  
   240  	walletCfg := new(walletConfig)
   241  	_, err = loadConfig(cfg.Settings, net, walletCfg)
   242  	if err != nil {
   243  		return false, err
   244  	}
   245  
   246  	if chainParams.Net != w.chainParams.Net {
   247  		return false, errors.New("cannot reconfigure to use different network")
   248  	}
   249  
   250  	certs, err := os.ReadFile(rpcCfg.RPCCert)
   251  	if err != nil {
   252  		return false, fmt.Errorf("TLS certificate read error: %w", err)
   253  	}
   254  
   255  	var allOk bool
   256  	defer func() {
   257  		if allOk { // update the account names as the last step
   258  			w.accountsV.Store(XCWalletAccounts{
   259  				PrimaryAccount: rpcCfg.PrimaryAccount,
   260  				UnmixedAccount: rpcCfg.UnmixedAccount,
   261  				TradingAccount: rpcCfg.TradingAccount,
   262  			})
   263  		}
   264  	}()
   265  
   266  	currentAccts := w.accountsV.Load().(XCWalletAccounts)
   267  
   268  	if rpcCfg.RPCUser == w.rpcCfg.User &&
   269  		rpcCfg.RPCPass == w.rpcCfg.Pass &&
   270  		bytes.Equal(certs, w.rpcCfg.Certificates) &&
   271  		rpcCfg.RPCListen == w.rpcCfg.Host &&
   272  		rpcCfg.PrimaryAccount == currentAccts.PrimaryAccount &&
   273  		rpcCfg.UnmixedAccount == currentAccts.UnmixedAccount &&
   274  		rpcCfg.TradingAccount == currentAccts.TradingAccount {
   275  		allOk = true
   276  		return false, nil
   277  	}
   278  
   279  	newWallet, err := newRPCWallet(cfg.Settings, w.log, net)
   280  	if err != nil {
   281  		return false, err
   282  	}
   283  
   284  	err = newWallet.Connect(ctx)
   285  	if err != nil {
   286  		return false, fmt.Errorf("error connecting new wallet")
   287  	}
   288  
   289  	defer func() {
   290  		if !allOk {
   291  			newWallet.Disconnect()
   292  		}
   293  	}()
   294  
   295  	for _, acctName := range []string{rpcCfg.PrimaryAccount, rpcCfg.TradingAccount, rpcCfg.UnmixedAccount} {
   296  		if acctName == "" {
   297  			continue
   298  		}
   299  		if _, err := newWallet.AccountUnlocked(ctx, acctName); err != nil {
   300  			return false, fmt.Errorf("error checking lock status on account %q: %v", acctName, err)
   301  		}
   302  	}
   303  
   304  	a, err := stdaddr.DecodeAddress(currentAddress, w.chainParams)
   305  	if err != nil {
   306  		return false, err
   307  	}
   308  	var depositAccount string
   309  	if rpcCfg.UnmixedAccount != "" {
   310  		depositAccount = rpcCfg.UnmixedAccount
   311  	} else {
   312  		depositAccount = rpcCfg.PrimaryAccount
   313  	}
   314  	owns, err := newWallet.AccountOwnsAddress(ctx, a, depositAccount)
   315  	if err != nil {
   316  		return false, err
   317  	}
   318  	if !owns {
   319  		if walletCfg.ActivelyUsed {
   320  			return false, errors.New("cannot reconfigure to different wallet while there are active trades")
   321  		}
   322  		return true, nil
   323  	}
   324  
   325  	w.rpcMtx.Lock()
   326  	defer w.rpcMtx.Unlock()
   327  	w.chainParams = newWallet.chainParams
   328  	w.rpcCfg = newWallet.rpcCfg
   329  	w.spvMode = newWallet.spvMode
   330  	w.rpcConnector = newWallet.rpcConnector
   331  	w.rpcClient = newWallet.rpcClient
   332  
   333  	allOk = true
   334  	return false, nil
   335  }
   336  
   337  func (w *rpcWallet) handleRPCClientReconnection(ctx context.Context) {
   338  	connectCount := atomic.AddUint32(&w.connectCount, 1)
   339  	if connectCount == 1 {
   340  		// first connection, below check will be performed
   341  		// by *rpcWallet.Connect.
   342  		return
   343  	}
   344  
   345  	w.log.Debugf("dcrwallet reconnected (%d)", connectCount-1)
   346  	w.rpcMtx.RLock()
   347  	defer w.rpcMtx.RUnlock()
   348  	spv, hasSPVTicketFunctions, err := checkRPCConnection(ctx, w.rpcConnector, w.rpcClient, w.log)
   349  	if err != nil {
   350  		w.log.Errorf("dcrwallet reconnect handler error: %v", err)
   351  	}
   352  	w.spvMode = spv
   353  	w.hasSPVTicketFunctions = hasSPVTicketFunctions
   354  }
   355  
   356  // checkRPCConnection verifies the dcrwallet connection with the walletinfo RPC
   357  // and sets the spvMode flag accordingly. The spvMode flag is only set after a
   358  // successful check. This method is not safe for concurrent access, and the
   359  // rpcMtx must be at least read locked.
   360  func checkRPCConnection(ctx context.Context, connector rpcConnector, client rpcClient, log dex.Logger) (bool, bool, error) {
   361  	// Check the required API versions.
   362  	versions, err := connector.Version(ctx)
   363  	if err != nil {
   364  		return false, false, fmt.Errorf("dcrwallet version fetch error: %w", err)
   365  	}
   366  
   367  	ver, exists := versions["dcrwalletjsonrpcapi"]
   368  	if !exists {
   369  		return false, false, fmt.Errorf("dcrwallet.Version response missing 'dcrwalletjsonrpcapi'")
   370  	}
   371  	walletSemver := dex.NewSemver(ver.Major, ver.Minor, ver.Patch)
   372  	if !dex.SemverCompatibleAny(compatibleWalletRPCVersions, walletSemver) {
   373  		return false, false, fmt.Errorf("advertised dcrwallet JSON-RPC version %v incompatible with %v",
   374  			walletSemver, compatibleWalletRPCVersions)
   375  	}
   376  
   377  	hasSPVTicketFunctions := walletSemver.Major >= vspWithSPVWalletRPCVersion.Major &&
   378  		walletSemver.Minor >= vspWithSPVWalletRPCVersion.Minor
   379  
   380  	ver, exists = versions["dcrdjsonrpcapi"]
   381  	if exists {
   382  		nodeSemver := dex.NewSemver(ver.Major, ver.Minor, ver.Patch)
   383  		if !dex.SemverCompatibleAny(compatibleNodeRPCVersions, nodeSemver) {
   384  			return false, false, fmt.Errorf("advertised dcrd JSON-RPC version %v incompatible with %v",
   385  				nodeSemver, compatibleNodeRPCVersions)
   386  		}
   387  		log.Infof("Connected to dcrwallet (JSON-RPC API v%s) proxying dcrd (JSON-RPC API v%s)",
   388  			walletSemver, nodeSemver)
   389  		return false, false, nil
   390  	}
   391  
   392  	// SPV maybe?
   393  	walletInfo, err := client.WalletInfo(ctx)
   394  	if err != nil {
   395  		return false, false, fmt.Errorf("walletinfo rpc error: %w", translateRPCCancelErr(err))
   396  	}
   397  	if !walletInfo.SPV {
   398  		return false, false, fmt.Errorf("dcrwallet.Version response missing 'dcrdjsonrpcapi' for non-spv wallet")
   399  	}
   400  	log.Infof("Connected to dcrwallet (JSON-RPC API v%s) in SPV mode", walletSemver)
   401  	return true, hasSPVTicketFunctions, nil
   402  }
   403  
   404  // Connect establishes a connection to the previously created rpc client. The
   405  // wallet must not already be connected.
   406  func (w *rpcWallet) Connect(ctx context.Context) error {
   407  	w.rpcMtx.Lock()
   408  	defer w.rpcMtx.Unlock()
   409  
   410  	// NOTE: rpcclient.(*Client).Disconnected() returns false prior to connect,
   411  	// so we cannot block incorrect Connect calls on that basis. However, it is
   412  	// always safe to call Shutdown, so do it just in case.
   413  	w.rpcConnector.Shutdown()
   414  
   415  	// Prepare a fresh RPC client.
   416  	ntfnHandlers := &rpcclient.NotificationHandlers{
   417  		// Setup an on-connect handler for logging (re)connects.
   418  		OnClientConnected: func() { w.handleRPCClientReconnection(ctx) },
   419  	}
   420  	nodeRPCClient, err := rpcclient.New(w.rpcCfg, ntfnHandlers)
   421  	if err != nil { // should never fail since we validated the config in newRPCWallet
   422  		return fmt.Errorf("failed to create dcrwallet RPC client: %w", err)
   423  	}
   424  
   425  	atomic.StoreUint32(&w.connectCount, 0) // handleRPCClientReconnection should skip checkRPCConnection on first Connect
   426  
   427  	w.rpcConnector = nodeRPCClient
   428  	w.rpcClient = newCombinedClient(nodeRPCClient, w.chainParams)
   429  
   430  	err = nodeRPCClient.Connect(ctx, false) // no retry
   431  	if err != nil {
   432  		return fmt.Errorf("dcrwallet connect error: %w", err)
   433  	}
   434  
   435  	net, err := w.rpcClient.GetCurrentNet(ctx)
   436  	if err != nil {
   437  		return translateRPCCancelErr(err)
   438  	}
   439  	if net != w.chainParams.Net {
   440  		return fmt.Errorf("unexpected wallet network %s, expected %s", net, w.chainParams.Net)
   441  	}
   442  
   443  	// The websocket client is connected now, so if the following check
   444  	// fails and we return with a non-nil error, we must shutdown the
   445  	// rpc client otherwise subsequent reconnect attempts will be met
   446  	// with "websocket client has already connected".
   447  	spv, hasSPVTicketFunctions, err := checkRPCConnection(ctx, w.rpcConnector, w.rpcClient, w.log)
   448  	if err != nil {
   449  		// The client should still be connected, but if not, do not try to
   450  		// shutdown and wait as it could hang.
   451  		if !errors.Is(err, rpcclient.ErrClientShutdown) {
   452  			// Using w.Disconnect would deadlock with rpcMtx already locked.
   453  			w.rpcConnector.Shutdown()
   454  			w.rpcConnector.WaitForShutdown()
   455  		}
   456  		return err
   457  	}
   458  
   459  	w.spvMode = spv
   460  	w.hasSPVTicketFunctions = hasSPVTicketFunctions
   461  
   462  	return nil
   463  }
   464  
   465  // Disconnect shuts down access to the wallet by disconnecting the rpc client.
   466  // Part of the Wallet interface.
   467  func (w *rpcWallet) Disconnect() {
   468  	w.rpcMtx.Lock()
   469  	defer w.rpcMtx.Unlock()
   470  
   471  	w.rpcConnector.Shutdown() // rpcclient.(*Client).Disconnect is a no-op with auto-reconnect
   472  	w.rpcConnector.WaitForShutdown()
   473  
   474  	// NOTE: After rpcclient shutdown, the rpcConnector and rpcClient are dead
   475  	// and cannot be started again. Connect must recreate them.
   476  }
   477  
   478  // client is a thread-safe accessor to the wallet's rpcClient.
   479  func (w *rpcWallet) client() rpcClient {
   480  	w.rpcMtx.RLock()
   481  	defer w.rpcMtx.RUnlock()
   482  	return w.rpcClient
   483  }
   484  
   485  // Network returns the network of the connected wallet.
   486  // Part of the Wallet interface.
   487  func (w *rpcWallet) Network(ctx context.Context) (wire.CurrencyNet, error) {
   488  	net, err := w.client().GetCurrentNet(ctx)
   489  	return net, translateRPCCancelErr(err)
   490  }
   491  
   492  // SpvMode returns true if the wallet is connected to the Decred
   493  // network via SPV peers.
   494  // Part of the Wallet interface.
   495  func (w *rpcWallet) SpvMode() bool {
   496  	w.rpcMtx.RLock()
   497  	defer w.rpcMtx.RUnlock()
   498  	return w.spvMode
   499  }
   500  
   501  // AddressInfo returns information for the provided address. It is an error
   502  // if the address is not owned by the wallet.
   503  // Part of the Wallet interface.
   504  func (w *rpcWallet) AddressInfo(ctx context.Context, address string) (*AddressInfo, error) {
   505  	a, err := stdaddr.DecodeAddress(address, w.chainParams)
   506  	if err != nil {
   507  		return nil, err
   508  	}
   509  	res, err := w.client().ValidateAddress(ctx, a)
   510  	if err != nil {
   511  		return nil, translateRPCCancelErr(err)
   512  	}
   513  	if !res.IsValid {
   514  		return nil, fmt.Errorf("address is invalid")
   515  	}
   516  	if !res.IsMine {
   517  		return nil, fmt.Errorf("address does not belong to this wallet")
   518  	}
   519  	if res.Branch == nil {
   520  		return nil, fmt.Errorf("no account branch info for address")
   521  	}
   522  	return &AddressInfo{Account: res.Account, Branch: *res.Branch}, nil
   523  }
   524  
   525  // AccountOwnsAddress checks if the provided address belongs to the specified
   526  // account.
   527  // Part of the Wallet interface.
   528  func (w *rpcWallet) AccountOwnsAddress(ctx context.Context, addr stdaddr.Address, acctName string) (bool, error) {
   529  	va, err := w.rpcClient.ValidateAddress(ctx, addr)
   530  	if err != nil {
   531  		return false, translateRPCCancelErr(err)
   532  	}
   533  
   534  	return va.IsMine && va.Account == acctName, nil
   535  }
   536  
   537  // WalletOwnsAddress returns whether any of the account controlled by this
   538  // wallet owns the specified address.
   539  func (w *rpcWallet) WalletOwnsAddress(ctx context.Context, addr stdaddr.Address) (bool, error) {
   540  	va, err := w.rpcClient.ValidateAddress(ctx, addr)
   541  	if err != nil {
   542  		return false, translateRPCCancelErr(err)
   543  	}
   544  
   545  	return va.IsMine, nil
   546  }
   547  
   548  // AccountBalance returns the balance breakdown for the specified account.
   549  // Part of the Wallet interface.
   550  func (w *rpcWallet) AccountBalance(ctx context.Context, confirms int32, acctName string) (*walletjson.GetAccountBalanceResult, error) {
   551  	balances, err := w.rpcClient.GetBalanceMinConf(ctx, acctName, int(confirms))
   552  	if err != nil {
   553  		return nil, translateRPCCancelErr(err)
   554  	}
   555  
   556  	for i := range balances.Balances {
   557  		ab := &balances.Balances[i]
   558  		if ab.AccountName == acctName {
   559  			return ab, nil
   560  		}
   561  	}
   562  
   563  	return nil, fmt.Errorf("account not found: %q", acctName)
   564  }
   565  
   566  // LockedOutputs fetches locked outputs for the specified account using rpc
   567  // RawRequest.
   568  // Part of the Wallet interface.
   569  func (w *rpcWallet) LockedOutputs(ctx context.Context, acctName string) ([]chainjson.TransactionInput, error) {
   570  	var locked []chainjson.TransactionInput
   571  	err := w.rpcClientRawRequest(ctx, methodListLockUnspent, anylist{acctName}, &locked)
   572  	return locked, translateRPCCancelErr(err)
   573  }
   574  
   575  // EstimateSmartFeeRate returns a smart feerate estimate using the
   576  // estimatesmartfee rpc.
   577  // Part of the Wallet interface.
   578  func (w *rpcWallet) EstimateSmartFeeRate(ctx context.Context, confTarget int64, mode chainjson.EstimateSmartFeeMode) (float64, error) {
   579  	// estimatesmartfee 1 returns extremely high rates (e.g. 0.00817644).
   580  	if confTarget < 2 {
   581  		confTarget = 2
   582  	}
   583  	estimateFeeResult, err := w.client().EstimateSmartFee(ctx, confTarget, mode)
   584  	if err != nil {
   585  		return 0, translateRPCCancelErr(err)
   586  	}
   587  	return estimateFeeResult.FeeRate, nil
   588  }
   589  
   590  // Unspents fetches unspent outputs for the specified account using rpc
   591  // RawRequest.
   592  // Part of the Wallet interface.
   593  func (w *rpcWallet) Unspents(ctx context.Context, acctName string) ([]*walletjson.ListUnspentResult, error) {
   594  	var unspents []*walletjson.ListUnspentResult
   595  	// minconf, maxconf (rpcdefault=9999999), [address], account
   596  	params := anylist{0, 9999999, nil, acctName}
   597  	err := w.rpcClientRawRequest(ctx, methodListUnspent, params, &unspents)
   598  	return unspents, err
   599  }
   600  
   601  // InternalAddress returns a change address from the specified account.
   602  // Part of the Wallet interface.
   603  func (w *rpcWallet) InternalAddress(ctx context.Context, acctName string) (stdaddr.Address, error) {
   604  	addr, err := w.rpcClient.GetRawChangeAddress(ctx, acctName, w.chainParams)
   605  	return addr, translateRPCCancelErr(err)
   606  }
   607  
   608  // ExternalAddress returns an external address from the specified account using
   609  // GapPolicyWrap. The dcrwallet user should set --gaplimit= as needed to prevent
   610  // address reused depending on their needs. Part of the Wallet interface.
   611  func (w *rpcWallet) ExternalAddress(ctx context.Context, acctName string) (stdaddr.Address, error) {
   612  	addr, err := w.rpcClient.GetNewAddressGapPolicy(ctx, acctName, dcrwallet.GapPolicyWrap)
   613  	if err != nil {
   614  		return nil, translateRPCCancelErr(err)
   615  	}
   616  	return addr, nil
   617  }
   618  
   619  // LockUnspent locks or unlocks the specified outpoint.
   620  // Part of the Wallet interface.
   621  func (w *rpcWallet) LockUnspent(ctx context.Context, unlock bool, ops []*wire.OutPoint) error {
   622  	return translateRPCCancelErr(w.client().LockUnspent(ctx, unlock, ops))
   623  }
   624  
   625  // UnspentOutput returns information about an unspent tx output, if found
   626  // and unspent. Use wire.TxTreeUnknown if the output tree is unknown, the
   627  // correct tree will be returned if the unspent output is found.
   628  // This method is only guaranteed to return results for outputs that pay to
   629  // the wallet. Returns asset.CoinNotFoundError if the unspent output cannot
   630  // be located.
   631  // Part of the Wallet interface.
   632  func (w *rpcWallet) UnspentOutput(ctx context.Context, txHash *chainhash.Hash, index uint32, tree int8) (*TxOutput, error) {
   633  	var checkTrees []int8
   634  	switch {
   635  	case tree == wire.TxTreeUnknown:
   636  		checkTrees = []int8{wire.TxTreeRegular, wire.TxTreeStake}
   637  	case tree == wire.TxTreeRegular || tree == wire.TxTreeStake:
   638  		checkTrees = []int8{tree}
   639  	default:
   640  		return nil, fmt.Errorf("invalid tx tree %d", tree)
   641  	}
   642  
   643  	for _, tree := range checkTrees {
   644  		txOut, err := w.client().GetTxOut(ctx, txHash, index, tree, true)
   645  		if err != nil {
   646  			return nil, translateRPCCancelErr(err)
   647  		}
   648  		if txOut == nil {
   649  			continue
   650  		}
   651  
   652  		amount, err := dcrutil.NewAmount(txOut.Value)
   653  		if err != nil {
   654  			return nil, fmt.Errorf("invalid amount %f: %v", txOut.Value, err)
   655  		}
   656  		pkScript, err := hex.DecodeString(txOut.ScriptPubKey.Hex)
   657  		if err != nil {
   658  			return nil, fmt.Errorf("invalid ScriptPubKey %s: %v", txOut.ScriptPubKey.Hex, err)
   659  		}
   660  		output := &TxOutput{
   661  			TxOut:         newTxOut(int64(amount), txOut.ScriptPubKey.Version, pkScript),
   662  			Tree:          tree,
   663  			Addresses:     txOut.ScriptPubKey.Addresses,
   664  			Confirmations: uint32(txOut.Confirmations),
   665  		}
   666  		return output, nil
   667  	}
   668  
   669  	return nil, asset.CoinNotFoundError
   670  }
   671  
   672  // SignRawTransaction signs the provided transaction using rpc RawRequest.
   673  // Part of the Wallet interface.
   674  func (w *rpcWallet) SignRawTransaction(ctx context.Context, inTx *wire.MsgTx) (*wire.MsgTx, error) {
   675  	baseTx := inTx.Copy()
   676  	txHex, err := msgTxToHex(baseTx)
   677  	if err != nil {
   678  		return nil, fmt.Errorf("failed to encode MsgTx: %w", err)
   679  	}
   680  	var res walletjson.SignRawTransactionResult
   681  	err = w.rpcClientRawRequest(ctx, methodSignRawTransaction, anylist{txHex}, &res)
   682  	if err != nil {
   683  		return nil, err
   684  	}
   685  
   686  	for i := range res.Errors {
   687  		sigErr := &res.Errors[i]
   688  		return nil, fmt.Errorf("signing %v:%d, seq = %d, sigScript = %v, failed: %v (is wallet locked?)",
   689  			sigErr.TxID, sigErr.Vout, sigErr.Sequence, sigErr.ScriptSig, sigErr.Error)
   690  		// Will be incomplete below, so log each SignRawTransactionError and move on.
   691  	}
   692  
   693  	if !res.Complete {
   694  		baseTxB, _ := baseTx.Bytes()
   695  		w.log.Errorf("Incomplete raw transaction signatures (input tx: %x / incomplete signed tx: %s)",
   696  			baseTxB, res.Hex)
   697  		return nil, fmt.Errorf("incomplete raw tx signatures (is wallet locked?)")
   698  	}
   699  
   700  	return msgTxFromHex(res.Hex)
   701  }
   702  
   703  // SendRawTransaction broadcasts the provided transaction to the Decred network.
   704  // Part of the Wallet interface.
   705  func (w *rpcWallet) SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) {
   706  	hash, err := w.client().SendRawTransaction(ctx, tx, allowHighFees)
   707  	return hash, translateRPCCancelErr(err)
   708  }
   709  
   710  // GetBlockHeader generates a *BlockHeader for the specified block hash. The
   711  // returned block header is a wire.BlockHeader with the addition of the block's
   712  // median time.
   713  func (w *rpcWallet) GetBlockHeader(ctx context.Context, blockHash *chainhash.Hash) (*BlockHeader, error) {
   714  	hdr, err := w.rpcClient.GetBlockHeader(ctx, blockHash)
   715  	if err != nil {
   716  		return nil, err
   717  	}
   718  	verboseHdr, err := w.rpcClient.GetBlockHeaderVerbose(ctx, blockHash)
   719  	if err != nil {
   720  		return nil, err
   721  	}
   722  	var nextHash *chainhash.Hash
   723  	if verboseHdr.NextHash != "" {
   724  		nextHash, err = chainhash.NewHashFromStr(verboseHdr.NextHash)
   725  		if err != nil {
   726  			return nil, fmt.Errorf("invalid next block hash %v: %w",
   727  				verboseHdr.NextHash, err)
   728  		}
   729  	}
   730  	return &BlockHeader{
   731  		BlockHeader:   hdr,
   732  		MedianTime:    verboseHdr.MedianTime,
   733  		Confirmations: verboseHdr.Confirmations,
   734  		NextHash:      nextHash,
   735  	}, nil
   736  }
   737  
   738  // GetBlock returns the MsgBlock.
   739  // Part of the Wallet interface.
   740  func (w *rpcWallet) GetBlock(ctx context.Context, blockHash *chainhash.Hash) (*wire.MsgBlock, error) {
   741  	blk, err := w.rpcClient.GetBlock(ctx, blockHash)
   742  	return blk, translateRPCCancelErr(err)
   743  }
   744  
   745  // GetTransaction returns the details of a wallet tx, if the wallet contains a
   746  // tx with the provided hash. Returns asset.CoinNotFoundError if the tx is not
   747  // found in the wallet.
   748  // Part of the Wallet interface.
   749  func (w *rpcWallet) GetTransaction(ctx context.Context, txHash *chainhash.Hash) (*WalletTransaction, error) {
   750  	tx, err := w.client().GetTransaction(ctx, txHash)
   751  	if err != nil {
   752  		if isTxNotFoundErr(err) {
   753  			return nil, asset.CoinNotFoundError
   754  		}
   755  		return nil, fmt.Errorf("error finding transaction %s in wallet: %w", txHash, translateRPCCancelErr(err))
   756  	}
   757  	msgTx, err := msgTxFromHex(tx.Hex)
   758  	if err != nil {
   759  		return nil, fmt.Errorf("invalid tx hex %s: %w", tx.Hex, err)
   760  	}
   761  	return &WalletTransaction{
   762  		Confirmations: tx.Confirmations,
   763  		BlockHash:     tx.BlockHash,
   764  		Details:       tx.Details,
   765  		MsgTx:         msgTx,
   766  	}, nil
   767  }
   768  
   769  func (w *rpcWallet) ListSinceBlock(ctx context.Context, start int32) ([]ListTransactionsResult, error) {
   770  	hash, err := w.GetBlockHash(ctx, int64(start))
   771  	if err != nil {
   772  		return nil, err
   773  	}
   774  
   775  	res, err := w.client().ListSinceBlock(ctx, hash)
   776  	if err != nil {
   777  		return nil, err
   778  	}
   779  
   780  	toReturn := make([]ListTransactionsResult, 0, len(res.Transactions))
   781  	for _, tx := range res.Transactions {
   782  		toReturn = append(toReturn, ListTransactionsResult{
   783  			TxID:       tx.TxID,
   784  			BlockIndex: tx.BlockIndex,
   785  			BlockTime:  tx.BlockTime,
   786  			Send:       tx.Category == "send",
   787  			TxType:     tx.TxType,
   788  			Fee:        tx.Fee,
   789  		})
   790  	}
   791  
   792  	return toReturn, nil
   793  }
   794  
   795  // GetRawMempool returns hashes for all txs of the specified type in the node's
   796  // mempool.
   797  // Part of the Wallet interface.
   798  func (w *rpcWallet) GetRawMempool(ctx context.Context) ([]*chainhash.Hash, error) {
   799  	mempoolTxs, err := w.rpcClient.GetRawMempool(ctx, chainjson.GRMAll)
   800  	return mempoolTxs, translateRPCCancelErr(err)
   801  }
   802  
   803  // GetBestBlock returns the hash and height of the wallet's best block.
   804  // Part of the Wallet interface.
   805  func (w *rpcWallet) GetBestBlock(ctx context.Context) (*chainhash.Hash, int64, error) {
   806  	hash, height, err := w.client().GetBestBlock(ctx)
   807  	return hash, height, translateRPCCancelErr(err)
   808  }
   809  
   810  // GetBlockHash returns the hash of the mainchain block at the specified height.
   811  // Part of the Wallet interface.
   812  func (w *rpcWallet) GetBlockHash(ctx context.Context, blockHeight int64) (*chainhash.Hash, error) {
   813  	bh, err := w.client().GetBlockHash(ctx, blockHeight)
   814  	return bh, translateRPCCancelErr(err)
   815  }
   816  
   817  // MatchAnyScript looks for any of the provided scripts in the block specified.
   818  // Part of the Wallet interface.
   819  func (w *rpcWallet) MatchAnyScript(ctx context.Context, blockHash *chainhash.Hash, scripts [][]byte) (bool, error) {
   820  	var cfRes walletjson.GetCFilterV2Result
   821  	err := w.rpcClientRawRequest(ctx, methodGetCFilterV2, anylist{blockHash.String()}, &cfRes)
   822  	if err != nil {
   823  		return false, err
   824  	}
   825  
   826  	bf, key := cfRes.Filter, cfRes.Key
   827  	filterB, err := hex.DecodeString(bf)
   828  	if err != nil {
   829  		return false, fmt.Errorf("error decoding block filter: %w", err)
   830  	}
   831  	keyB, err := hex.DecodeString(key)
   832  	if err != nil {
   833  		return false, fmt.Errorf("error decoding block filter key: %w", err)
   834  	}
   835  	filter, err := gcs.FromBytesV2(blockcf2.B, blockcf2.M, filterB)
   836  	if err != nil {
   837  		return false, fmt.Errorf("error deserializing block filter: %w", err)
   838  	}
   839  
   840  	var bcf2Key [gcs.KeySize]byte
   841  	copy(bcf2Key[:], keyB)
   842  
   843  	return filter.MatchAny(bcf2Key, scripts), nil
   844  
   845  }
   846  
   847  // lockWallet locks the wallet.
   848  func (w *rpcWallet) lockWallet(ctx context.Context) error {
   849  	return translateRPCCancelErr(w.rpcClient.WalletLock(ctx))
   850  }
   851  
   852  // unlockWallet unlocks the wallet.
   853  func (w *rpcWallet) unlockWallet(ctx context.Context, passphrase string, timeoutSecs int64) error {
   854  	return translateRPCCancelErr(w.rpcClient.WalletPassphrase(ctx, passphrase, timeoutSecs))
   855  }
   856  
   857  // AccountUnlocked returns true if the account is unlocked.
   858  // Part of the Wallet interface.
   859  func (w *rpcWallet) AccountUnlocked(ctx context.Context, acctName string) (bool, error) {
   860  	// First return locked status of the account, falling back to walletinfo if
   861  	// the account is not individually password protected.
   862  	res, err := w.rpcClient.AccountUnlocked(ctx, acctName)
   863  	if err != nil {
   864  		return false, err
   865  	}
   866  	if res.Encrypted {
   867  		return *res.Unlocked, nil
   868  	}
   869  	// The account is not individually encrypted, so check wallet lock status.
   870  	walletInfo, err := w.rpcClient.WalletInfo(ctx)
   871  	if err != nil {
   872  		return false, fmt.Errorf("walletinfo error: %w", err)
   873  	}
   874  	return walletInfo.Unlocked, nil
   875  }
   876  
   877  // LockAccount locks the specified account.
   878  // Part of the Wallet interface.
   879  func (w *rpcWallet) LockAccount(ctx context.Context, acctName string) error {
   880  	if w.rpcConnector.Disconnected() {
   881  		return asset.ErrConnectionDown
   882  	}
   883  
   884  	// Since hung calls to Lock() may block shutdown of the consumer and thus
   885  	// cancellation of the ExchangeWallet subsystem's Context, dcr.ctx, give
   886  	// this a timeout in case the connection goes down or the RPC hangs for
   887  	// other reasons.
   888  	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
   889  	defer cancel()
   890  
   891  	res, err := w.rpcClient.AccountUnlocked(ctx, acctName)
   892  	if err != nil {
   893  		return err
   894  	}
   895  	if !res.Encrypted {
   896  		return w.lockWallet(ctx)
   897  	}
   898  	if res.Unlocked != nil && !*res.Unlocked {
   899  		return nil
   900  	}
   901  
   902  	err = w.rpcClient.LockAccount(ctx, acctName)
   903  	if isAccountLockedErr(err) {
   904  		return nil // it's already locked
   905  	}
   906  	return translateRPCCancelErr(err)
   907  }
   908  
   909  // UnlockAccount unlocks the specified account or the wallet if account is not
   910  // encrypted. Part of the Wallet interface.
   911  func (w *rpcWallet) UnlockAccount(ctx context.Context, pw []byte, acctName string) error {
   912  	res, err := w.rpcClient.AccountUnlocked(ctx, acctName)
   913  	if err != nil {
   914  		return err
   915  	}
   916  	if res.Encrypted {
   917  		return translateRPCCancelErr(w.rpcClient.UnlockAccount(ctx, acctName, string(pw)))
   918  	}
   919  	return w.unlockWallet(ctx, string(pw), 0)
   920  
   921  }
   922  
   923  // SyncStatus returns the wallet's sync status.
   924  // Part of the Wallet interface.
   925  func (w *rpcWallet) SyncStatus(ctx context.Context) (*asset.SyncStatus, error) {
   926  	syncStatus := new(walletjson.SyncStatusResult)
   927  	if err := w.rpcClientRawRequest(ctx, methodSyncStatus, nil, syncStatus); err != nil {
   928  		return nil, fmt.Errorf("rawrequest error: %w", err)
   929  	}
   930  	peers, err := w.PeerInfo(ctx)
   931  	if err != nil {
   932  		return nil, fmt.Errorf("error getting peer info: %w", err)
   933  	}
   934  	if syncStatus.Synced {
   935  		_, targetHeight, err := w.client().GetBestBlock(ctx)
   936  		if err != nil {
   937  			return nil, fmt.Errorf("error getting block count: %w", err)
   938  		}
   939  		return &asset.SyncStatus{
   940  			Synced:       len(peers) > 0 && !syncStatus.InitialBlockDownload,
   941  			TargetHeight: uint64(targetHeight),
   942  			Blocks:       uint64(targetHeight),
   943  		}, nil
   944  	}
   945  	var targetHeight int64
   946  	for _, p := range peers {
   947  		if p.StartingHeight > targetHeight {
   948  			targetHeight = p.StartingHeight
   949  		}
   950  	}
   951  	if targetHeight == 0 {
   952  		return new(asset.SyncStatus), nil
   953  	}
   954  	return &asset.SyncStatus{
   955  		Synced:       syncStatus.Synced && !syncStatus.InitialBlockDownload,
   956  		TargetHeight: uint64(targetHeight),
   957  		Blocks:       uint64(math.Round(float64(syncStatus.HeadersFetchProgress) * float64(targetHeight))),
   958  	}, nil
   959  }
   960  
   961  func (w *rpcWallet) PeerInfo(ctx context.Context) (peerInfo []*walletjson.GetPeerInfoResult, _ error) {
   962  	return peerInfo, w.rpcClientRawRequest(ctx, methodGetPeerInfo, nil, &peerInfo)
   963  }
   964  
   965  // PeerCount returns the number of network peers to which the wallet or its
   966  // backing node are connected.
   967  func (w *rpcWallet) PeerCount(ctx context.Context) (uint32, error) {
   968  	peerInfo, err := w.PeerInfo(ctx)
   969  	if err != nil {
   970  		return 0, err
   971  	}
   972  	return uint32(len(peerInfo)), err
   973  }
   974  
   975  // AddressPrivKey fetches the privkey for the specified address.
   976  // Part of the Wallet interface.
   977  func (w *rpcWallet) AddressPrivKey(ctx context.Context, address stdaddr.Address) (*secp256k1.PrivateKey, error) {
   978  	wif, err := w.rpcClient.DumpPrivKey(ctx, address)
   979  	if err != nil {
   980  		return nil, translateRPCCancelErr(err)
   981  	}
   982  	var priv secp256k1.PrivateKey
   983  	if overflow := priv.Key.SetByteSlice(wif.PrivKey()); overflow || priv.Key.IsZero() {
   984  		return nil, errors.New("invalid private key")
   985  	}
   986  	return &priv, nil
   987  }
   988  
   989  // StakeInfo returns the current gestakeinfo results.
   990  func (w *rpcWallet) StakeInfo(ctx context.Context) (*wallet.StakeInfoData, error) {
   991  	res, err := w.rpcClient.GetStakeInfo(ctx)
   992  	if err != nil {
   993  		return nil, err
   994  	}
   995  	sdiff, err := dcrutil.NewAmount(res.Difficulty)
   996  	if err != nil {
   997  		return nil, err
   998  	}
   999  	totalSubsidy, err := dcrutil.NewAmount(res.TotalSubsidy)
  1000  	if err != nil {
  1001  		return nil, err
  1002  	}
  1003  	return &wallet.StakeInfoData{
  1004  		BlockHeight:    res.BlockHeight,
  1005  		TotalSubsidy:   totalSubsidy,
  1006  		Sdiff:          sdiff,
  1007  		OwnMempoolTix:  res.OwnMempoolTix,
  1008  		Unspent:        res.Unspent,
  1009  		Voted:          res.Voted,
  1010  		Revoked:        res.Revoked,
  1011  		UnspentExpired: res.UnspentExpired,
  1012  		PoolSize:       res.PoolSize,
  1013  		AllMempoolTix:  res.AllMempoolTix,
  1014  		Immature:       res.Immature,
  1015  		Live:           res.Live,
  1016  		Missed:         res.Missed,
  1017  		Expired:        res.Expired,
  1018  	}, nil
  1019  }
  1020  
  1021  // PurchaseTickets purchases n amount of tickets. Returns the purchased ticket
  1022  // hashes if successful.
  1023  func (w *rpcWallet) PurchaseTickets(ctx context.Context, n int, _, _ string, _ bool) ([]*asset.Ticket, error) {
  1024  	hashes, err := w.rpcClient.PurchaseTicket(
  1025  		ctx,
  1026  		"default",
  1027  		nil, // minConf
  1028  		&n,  // numTickets
  1029  		nil, // expiry
  1030  		nil, // ticketChange
  1031  		nil, // ticketFee
  1032  	)
  1033  	if err != nil {
  1034  		return nil, err
  1035  	}
  1036  
  1037  	now := uint64(time.Now().Unix())
  1038  	tickets := make([]*asset.Ticket, len(hashes))
  1039  	for i, h := range hashes {
  1040  		// Need to get the ticket price
  1041  		tx, err := w.rpcClient.GetTransaction(ctx, h)
  1042  		if err != nil {
  1043  			return nil, fmt.Errorf("error getting transaction for new ticket %s: %w", h, err)
  1044  		}
  1045  		msgTx, err := msgTxFromHex(tx.Hex)
  1046  		if err != nil {
  1047  			return nil, fmt.Errorf("error decoding ticket %s tx hex: %v", h, err)
  1048  		}
  1049  		if len(msgTx.TxOut) == 0 {
  1050  			return nil, fmt.Errorf("malformed ticket transaction %s", h)
  1051  		}
  1052  		var fees uint64
  1053  		for _, txIn := range msgTx.TxIn {
  1054  			fees += uint64(txIn.ValueIn)
  1055  		}
  1056  		for _, txOut := range msgTx.TxOut {
  1057  			fees -= uint64(txOut.Value)
  1058  		}
  1059  		tickets[i] = &asset.Ticket{
  1060  			Tx: asset.TicketTransaction{
  1061  				Hash:        h.String(),
  1062  				TicketPrice: uint64(msgTx.TxOut[0].Value),
  1063  				Fees:        fees,
  1064  				Stamp:       now,
  1065  				BlockHeight: -1,
  1066  			},
  1067  			Status: asset.TicketStatusUnmined,
  1068  		}
  1069  	}
  1070  	return tickets, nil
  1071  }
  1072  
  1073  var oldSPVWalletErr = errors.New("wallet is an older spv wallet")
  1074  
  1075  // Tickets returns active tickets.
  1076  func (w *rpcWallet) Tickets(ctx context.Context) ([]*asset.Ticket, error) {
  1077  	return w.tickets(ctx, true)
  1078  }
  1079  
  1080  func (w *rpcWallet) tickets(ctx context.Context, includeImmature bool) ([]*asset.Ticket, error) {
  1081  	// GetTickets only works for spv clients after version 9.2.0
  1082  	if w.spvMode && !w.hasSPVTicketFunctions {
  1083  		return nil, oldSPVWalletErr
  1084  	}
  1085  	hashes, err := w.rpcClient.GetTickets(ctx, includeImmature)
  1086  	if err != nil {
  1087  		return nil, err
  1088  	}
  1089  	tickets := make([]*asset.Ticket, 0, len(hashes))
  1090  	for _, h := range hashes {
  1091  		tx, err := w.client().GetTransaction(ctx, h)
  1092  		if err != nil {
  1093  			w.log.Errorf("GetTransaction error for ticket %s: %v", h, err)
  1094  			continue
  1095  		}
  1096  		blockHeight := int64(-1)
  1097  		// If the transaction is not yet mined we do not know the block hash.
  1098  		if tx.BlockHash != "" {
  1099  			blkHash, err := chainhash.NewHashFromStr(tx.BlockHash)
  1100  			if err != nil {
  1101  				w.log.Errorf("Invalid block hash %v for ticket %v: %w", tx.BlockHash, h, err)
  1102  				continue
  1103  			}
  1104  			// dcrwallet returns do not include the block height.
  1105  			hdr, err := w.client().GetBlockHeader(ctx, blkHash)
  1106  			if err != nil {
  1107  				w.log.Errorf("GetBlockHeader error for ticket %s: %v", h, err)
  1108  				continue
  1109  			}
  1110  			blockHeight = int64(hdr.Height)
  1111  		}
  1112  		msgTx, err := msgTxFromHex(tx.Hex)
  1113  		if err != nil {
  1114  			w.log.Errorf("Error decoding ticket %s tx hex: %v", h, err)
  1115  			continue
  1116  		}
  1117  		if len(msgTx.TxOut) < 1 {
  1118  			w.log.Errorf("No outputs for ticket %s", h)
  1119  			continue
  1120  		}
  1121  		// Fee is always negative.
  1122  		feeAmt, _ := dcrutil.NewAmount(-tx.Fee)
  1123  
  1124  		tickets = append(tickets, &asset.Ticket{
  1125  			Tx: asset.TicketTransaction{
  1126  				Hash:        h.String(),
  1127  				TicketPrice: uint64(msgTx.TxOut[0].Value),
  1128  				Fees:        uint64(feeAmt),
  1129  				Stamp:       uint64(tx.Time),
  1130  				BlockHeight: blockHeight,
  1131  			},
  1132  			// The walletjson.GetTransactionResult returned from GetTransaction
  1133  			// actually has a TicketStatus string field, but it doesn't appear
  1134  			// to ever be populated by dcrwallet.
  1135  			// Status:  somehowConvertFromString(tx.TicketStatus),
  1136  
  1137  			// Not sure how to get the spender through RPC.
  1138  			// Spender: ?,
  1139  		})
  1140  	}
  1141  	return tickets, nil
  1142  }
  1143  
  1144  // VotingPreferences returns current wallet voting preferences.
  1145  func (w *rpcWallet) VotingPreferences(ctx context.Context) ([]*walletjson.VoteChoice, []*asset.TBTreasurySpend, []*walletjson.TreasuryPolicyResult, error) {
  1146  	// Get consensus vote choices.
  1147  	choices, err := w.rpcClient.GetVoteChoices(ctx)
  1148  	if err != nil {
  1149  		return nil, nil, nil, fmt.Errorf("unable to get vote choices: %v", err)
  1150  	}
  1151  	voteChoices := make([]*walletjson.VoteChoice, len(choices.Choices))
  1152  	for i, v := range choices.Choices {
  1153  		vc := v
  1154  		voteChoices[i] = &vc
  1155  	}
  1156  	// Get tspend voting policy.
  1157  	const tSpendPolicyMethod = "tspendpolicy"
  1158  	var tSpendRes []walletjson.TSpendPolicyResult
  1159  	err = w.rpcClientRawRequest(ctx, tSpendPolicyMethod, nil, &tSpendRes)
  1160  	if err != nil {
  1161  		return nil, nil, nil, fmt.Errorf("unable to get treasury spend policy: %v", err)
  1162  	}
  1163  	tSpendPolicy := make([]*asset.TBTreasurySpend, len(tSpendRes))
  1164  	for i, tp := range tSpendRes {
  1165  		// TODO: Find a way to get the tspend total value? Probably only
  1166  		// possible with a full node and txindex.
  1167  		tSpendPolicy[i] = &asset.TBTreasurySpend{
  1168  			Hash:          tp.Hash,
  1169  			CurrentPolicy: tp.Policy,
  1170  		}
  1171  	}
  1172  	// Get treasury voting policy.
  1173  	const treasuryPolicyMethod = "treasurypolicy"
  1174  	var treasuryRes []walletjson.TreasuryPolicyResult
  1175  	err = w.rpcClientRawRequest(ctx, treasuryPolicyMethod, nil, &treasuryRes)
  1176  	if err != nil {
  1177  		return nil, nil, nil, fmt.Errorf("unable to get treasury policy: %v", err)
  1178  	}
  1179  	treasuryPolicy := make([]*walletjson.TreasuryPolicyResult, len(treasuryRes))
  1180  	for i, v := range treasuryRes {
  1181  		tp := v
  1182  		treasuryPolicy[i] = &tp
  1183  	}
  1184  	return voteChoices, tSpendPolicy, treasuryPolicy, nil
  1185  }
  1186  
  1187  // SetVotingPreferences sets voting preferences.
  1188  //
  1189  // NOTE: Will fail for communication problems with VSPs unlike internal wallets.
  1190  func (w *rpcWallet) SetVotingPreferences(ctx context.Context, choices, tSpendPolicy,
  1191  	treasuryPolicy map[string]string) error {
  1192  	for k, v := range choices {
  1193  		if err := w.rpcClient.SetVoteChoice(ctx, k, v); err != nil {
  1194  			return fmt.Errorf("unable to set vote choice: %v", err)
  1195  		}
  1196  	}
  1197  	const setTSpendPolicyMethod = "settspendpolicy"
  1198  	for k, v := range tSpendPolicy {
  1199  		if err := w.rpcClientRawRequest(ctx, setTSpendPolicyMethod, anylist{k, v}, nil); err != nil {
  1200  			return fmt.Errorf("unable to set tspend policy: %v", err)
  1201  		}
  1202  	}
  1203  	const setTreasuryPolicyMethod = "settreasurypolicy"
  1204  	for k, v := range treasuryPolicy {
  1205  		if err := w.rpcClientRawRequest(ctx, setTreasuryPolicyMethod, anylist{k, v}, nil); err != nil {
  1206  			return fmt.Errorf("unable to set treasury policy: %v", err)
  1207  		}
  1208  	}
  1209  	return nil
  1210  }
  1211  
  1212  func (w *rpcWallet) SetTxFee(ctx context.Context, feePerKB dcrutil.Amount) error {
  1213  	return w.rpcClient.SetTxFee(ctx, feePerKB)
  1214  }
  1215  
  1216  func (w *rpcWallet) AddressUsed(ctx context.Context, addrStr string) (bool, error) {
  1217  	addr, err := stdaddr.DecodeAddress(addrStr, w.chainParams)
  1218  	if err != nil {
  1219  		return false, err
  1220  	}
  1221  	const minConf = 0
  1222  	recv, err := w.rpcClient.GetReceivedByAddressMinConf(ctx, addr, minConf)
  1223  	if err != nil {
  1224  		return false, err
  1225  	}
  1226  	return recv != 0, nil
  1227  }
  1228  
  1229  // anylist is a list of RPC parameters to be converted to []json.RawMessage and
  1230  // sent via nodeRawRequest.
  1231  type anylist []any
  1232  
  1233  // rpcClientRawRequest is used to marshal parameters and send requests to the
  1234  // RPC server via rpcClient.RawRequest. If `thing` is non-nil, the result will
  1235  // be marshaled into `thing`.
  1236  func (w *rpcWallet) rpcClientRawRequest(ctx context.Context, method string, args anylist, thing any) error {
  1237  	params := make([]json.RawMessage, 0, len(args))
  1238  	for i := range args {
  1239  		p, err := json.Marshal(args[i])
  1240  		if err != nil {
  1241  			return err
  1242  		}
  1243  		params = append(params, p)
  1244  	}
  1245  	b, err := w.client().RawRequest(ctx, method, params)
  1246  	if err != nil {
  1247  		return fmt.Errorf("rawrequest error: %w", translateRPCCancelErr(err))
  1248  	}
  1249  	if thing != nil {
  1250  		return json.Unmarshal(b, thing)
  1251  	}
  1252  	return nil
  1253  }
  1254  
  1255  // The rpcclient package functions will return a rpcclient.ErrRequestCanceled
  1256  // error if the context is canceled. Translate these to asset.ErrRequestTimeout.
  1257  func translateRPCCancelErr(err error) error {
  1258  	if err == nil {
  1259  		return nil
  1260  	}
  1261  	if errors.Is(err, rpcclient.ErrRequestCanceled) {
  1262  		err = asset.ErrRequestTimeout
  1263  	}
  1264  	return err
  1265  }
  1266  
  1267  // isTxNotFoundErr will return true if the error indicates that the requested
  1268  // transaction is not known.
  1269  func isTxNotFoundErr(err error) bool {
  1270  	var rpcErr *dcrjson.RPCError
  1271  	return errors.As(err, &rpcErr) && rpcErr.Code == dcrjson.ErrRPCNoTxInfo
  1272  }
  1273  
  1274  func isAccountLockedErr(err error) bool {
  1275  	var rpcErr *dcrjson.RPCError
  1276  	return errors.As(err, &rpcErr) && rpcErr.Code == dcrjson.ErrRPCWalletUnlockNeeded &&
  1277  		strings.Contains(rpcErr.Message, "account is already locked")
  1278  }
  1279  
  1280  func (w *rpcWallet) walletInfo(ctx context.Context) (*walletjson.WalletInfoResult, error) {
  1281  	var walletInfo walletjson.WalletInfoResult
  1282  	err := w.rpcClientRawRequest(ctx, methodWalletInfo, nil, &walletInfo)
  1283  	return &walletInfo, translateRPCCancelErr(err)
  1284  }
  1285  
  1286  var _ ticketPager = (*rpcWallet)(nil)
  1287  
  1288  func (w *rpcWallet) TicketPage(ctx context.Context, scanStart int32, n, skipN int) ([]*asset.Ticket, error) {
  1289  	return make([]*asset.Ticket, 0), nil
  1290  }