decred.org/dcrdex@v1.0.5/client/core/wallet.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 core
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"strings"
    11  	"sync"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"decred.org/dcrdex/client/asset"
    16  	"decred.org/dcrdex/dex"
    17  	"decred.org/dcrdex/dex/encode"
    18  	"decred.org/dcrdex/dex/encrypt"
    19  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    20  )
    21  
    22  var errWalletNotConnected = errors.New("wallet not connected")
    23  
    24  // runWithTimeout runs the provided function, returning either the error from
    25  // the function or errTimeout if the function fails to return within the
    26  // timeout. This function is for wallet methods that may not have a context or
    27  // timeout of their own, or we simply cannot rely on third party packages to
    28  // respect context cancellation or deadlines.
    29  func runWithTimeout(f func() error, timeout time.Duration) error {
    30  	errChan := make(chan error, 1)
    31  	go func() {
    32  		defer close(errChan)
    33  		errChan <- f()
    34  	}()
    35  
    36  	select {
    37  	case err := <-errChan:
    38  		return err
    39  	case <-time.After(timeout):
    40  		return errTimeout
    41  	}
    42  }
    43  
    44  // xcWallet is a wallet. Use (*Core).loadWallet to construct a xcWallet.
    45  type xcWallet struct {
    46  	asset.Wallet
    47  	log               dex.Logger
    48  	connector         *dex.ConnectionMaster
    49  	AssetID           uint32
    50  	Symbol            string
    51  	supportedVersions []uint32
    52  	dbID              []byte
    53  	walletType        string
    54  	traits            asset.WalletTrait
    55  	parent            *xcWallet
    56  	feeState          atomic.Value // *FeeState
    57  	connectMtx        sync.Mutex
    58  
    59  	mtx        sync.RWMutex
    60  	encPass    []byte // empty means wallet not password protected
    61  	balance    *WalletBalance
    62  	pw         encode.PassBytes
    63  	address    string
    64  	peerCount  int32  // -1 means no count yet
    65  	monitored  uint32 // startWalletSyncMonitor goroutines monitoring sync status
    66  	hookedUp   bool
    67  	syncStatus *asset.SyncStatus
    68  	disabled   bool
    69  
    70  	// When wallets are being reconfigured and especially when the wallet type
    71  	// or host is being changed, we want to suppress "walletstate" notes to
    72  	// prevent subscribers from prematurely adopting the new WalletState before
    73  	// the new wallet is fully validated and added to the Core wallets map.
    74  	// WalletState notes during reconfiguration can come from the sync loop or
    75  	// from the PeersChange callback.
    76  	broadcasting *uint32
    77  }
    78  
    79  // encPW returns xcWallet's encrypted password.
    80  func (w *xcWallet) encPW() []byte {
    81  	w.mtx.RLock()
    82  	defer w.mtx.RUnlock()
    83  	return w.encPass
    84  }
    85  
    86  // setEncPW sets xcWallet's encrypted password.
    87  func (w *xcWallet) setEncPW(encPW []byte) {
    88  	w.mtx.Lock()
    89  	w.encPass = encPW
    90  	w.mtx.Unlock()
    91  }
    92  
    93  func (w *xcWallet) supportsVer(ver uint32) bool {
    94  	for _, v := range w.supportedVersions {
    95  		if v == ver {
    96  			return true
    97  		}
    98  	}
    99  	return false
   100  }
   101  
   102  // Unlock unlocks the wallet backend and caches the decrypted wallet password so
   103  // the wallet may be unlocked without user interaction using refreshUnlock.
   104  func (w *xcWallet) Unlock(crypter encrypt.Crypter) error {
   105  	if w.isDisabled() { // cannot unlock disabled wallet.
   106  		return fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID)))
   107  	}
   108  	if w.parent != nil {
   109  		return w.parent.Unlock(crypter)
   110  	}
   111  	if !w.connected() {
   112  		return errWalletNotConnected
   113  	}
   114  
   115  	a, is := w.Wallet.(asset.Authenticator)
   116  	if !is {
   117  		return nil
   118  	}
   119  
   120  	if len(w.encPW()) == 0 {
   121  		if a.Locked() {
   122  			return fmt.Errorf("wallet reporting as locked, but no password has been set")
   123  		}
   124  		return nil
   125  	}
   126  	pw, err := crypter.Decrypt(w.encPW())
   127  	if err != nil {
   128  		return fmt.Errorf("%s unlockWallet decryption error: %w", unbip(w.AssetID), err)
   129  	}
   130  
   131  	err = a.Unlock(pw) // can be slow - no timeout and NOT in the critical section!
   132  	if err != nil {
   133  		return err
   134  	}
   135  	w.mtx.Lock()
   136  	w.pw = pw
   137  	w.mtx.Unlock()
   138  	return nil
   139  }
   140  
   141  // refreshUnlock is used to ensure the wallet is unlocked. If the wallet backend
   142  // reports as already unlocked, which includes a wallet with no password
   143  // protection, no further action is taken and a nil error is returned. If the
   144  // wallet is reporting as locked, and the wallet is not known to be password
   145  // protected (no encPW set) or the decrypted password is not cached, a non-nil
   146  // error is returned. If no encrypted password is set, the xcWallet is
   147  // misconfigured and should be recreated. If the decrypted password is not
   148  // stored, the Unlock method should be used to decrypt the password. Finally, a
   149  // non-nil error will be returned if the cached password fails to unlock the
   150  // wallet, in which case unlockAttempted will also be true.
   151  func (w *xcWallet) refreshUnlock() (unlockAttempted bool, err error) {
   152  	if w.isDisabled() { // disabled wallet cannot be unlocked.
   153  		return false, fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID)))
   154  	}
   155  	if w.parent != nil {
   156  		return w.parent.refreshUnlock()
   157  	}
   158  	if !w.connected() {
   159  		return false, errWalletNotConnected
   160  	}
   161  
   162  	a, is := w.Wallet.(asset.Authenticator)
   163  	if !is {
   164  		return false, nil
   165  	}
   166  
   167  	// Check if the wallet backend is already unlocked.
   168  	if !a.Locked() {
   169  		return false, nil // unlocked
   170  	}
   171  
   172  	// Locked backend requires both encrypted and decrypted passwords.
   173  	w.mtx.RLock()
   174  	pwUnset := len(w.encPass) == 0
   175  	locked := len(w.pw) == 0
   176  	w.mtx.RUnlock()
   177  	if pwUnset {
   178  		return false, fmt.Errorf("%s wallet reporting as locked but no password"+
   179  			" has been set", unbip(w.AssetID))
   180  	}
   181  	if locked {
   182  		return false, fmt.Errorf("cannot refresh unlock on a locked %s wallet",
   183  			unbip(w.AssetID))
   184  	}
   185  
   186  	return true, a.Unlock(w.pw)
   187  }
   188  
   189  // Lock the wallet. For encrypted wallets (encPW set), this clears the cached
   190  // decrypted password and attempts to lock the wallet backend.
   191  func (w *xcWallet) Lock(timeout time.Duration) error {
   192  	a, is := w.Wallet.(asset.Authenticator)
   193  	if w.isDisabled() || !is { // wallet is disabled and is locked or it's not an authenticator.
   194  		return nil
   195  	}
   196  	w.mtx.Lock()
   197  	defer w.mtx.Unlock()
   198  	if w.parent != nil {
   199  		return w.parent.Lock(timeout)
   200  	}
   201  	if !w.hookedUp {
   202  		return errWalletNotConnected
   203  	}
   204  	if len(w.encPass) == 0 {
   205  		return nil
   206  	}
   207  	err := runWithTimeout(a.Lock, timeout)
   208  	if err == nil {
   209  		w.pw.Clear()
   210  		w.pw = nil
   211  	}
   212  	return err
   213  }
   214  
   215  // unlocked will only return true if both the wallet backend is unlocked and we
   216  // have cached the decrypted wallet password. The wallet backend may be queried
   217  // directly, likely involving an RPC call. Use locallyUnlocked to determine if
   218  // the wallet is automatically unlockable rather than actually unlocked.
   219  func (w *xcWallet) unlocked() bool {
   220  	if w.isDisabled() {
   221  		return false
   222  	}
   223  	a, is := w.Wallet.(asset.Authenticator)
   224  	if !is {
   225  		return w.locallyUnlocked()
   226  	}
   227  	if w.parent != nil {
   228  		return w.parent.unlocked()
   229  	}
   230  	if !w.connected() {
   231  		return false
   232  	}
   233  	return w.locallyUnlocked() && !a.Locked()
   234  }
   235  
   236  // locallyUnlocked checks whether we think the wallet is unlocked, but without
   237  // asking the wallet itself. More precisely, for encrypted wallets (encPW set)
   238  // this is true only if the decrypted password is cached. Use this to determine
   239  // if the wallet may be unlocked without user interaction (via refreshUnlock).
   240  func (w *xcWallet) locallyUnlocked() bool {
   241  	if w.isDisabled() {
   242  		return false
   243  	}
   244  	if w.parent != nil {
   245  		return w.parent.locallyUnlocked()
   246  	}
   247  	w.mtx.RLock()
   248  	defer w.mtx.RUnlock()
   249  	if len(w.encPass) == 0 {
   250  		return true // unencrypted wallet
   251  	}
   252  	return len(w.pw) > 0 // cached password for encrypted wallet
   253  }
   254  
   255  func (w *xcWallet) unitInfo() dex.UnitInfo {
   256  	return w.Info().UnitInfo
   257  }
   258  
   259  func (w *xcWallet) amtString(amt uint64) string {
   260  	ui := w.unitInfo()
   261  	return fmt.Sprintf("%s %s", ui.ConventionalString(amt), ui.Conventional.Unit)
   262  }
   263  
   264  func (w *xcWallet) amtStringSigned(amt int64) string {
   265  	if amt >= 0 {
   266  		return w.amtString(uint64(amt))
   267  	}
   268  	return "-" + w.amtString(uint64(-amt))
   269  }
   270  
   271  // state returns the current WalletState.
   272  func (w *xcWallet) state() *WalletState {
   273  	winfo := w.Info()
   274  
   275  	w.mtx.RLock()
   276  	var peerCount uint32
   277  	if w.peerCount > 0 { // initialized to -1 initially, means no count yet
   278  		peerCount = uint32(w.peerCount)
   279  	}
   280  
   281  	var tokenApprovals map[uint32]asset.ApprovalStatus
   282  	if w.connector.On() {
   283  		tokenApprovals = w.ApprovalStatus()
   284  	}
   285  
   286  	var feeState *FeeState
   287  	if feeStateI := w.feeState.Load(); feeStateI != nil {
   288  		feeState = feeStateI.(*FeeState)
   289  	}
   290  
   291  	state := &WalletState{
   292  		Symbol:       unbip(w.AssetID),
   293  		AssetID:      w.AssetID,
   294  		Open:         len(w.encPass) == 0 || len(w.pw) > 0,
   295  		Running:      w.connector.On(),
   296  		Balance:      w.balance,
   297  		Address:      w.address,
   298  		Units:        winfo.UnitInfo.AtomicUnit,
   299  		Encrypted:    len(w.encPass) > 0,
   300  		PeerCount:    peerCount,
   301  		Synced:       w.syncStatus.Synced,
   302  		SyncProgress: w.syncStatus.BlockProgress(),
   303  		SyncStatus:   w.syncStatus,
   304  		WalletType:   w.walletType,
   305  		Traits:       w.traits,
   306  		Disabled:     w.disabled,
   307  		Approved:     tokenApprovals,
   308  		FeeState:     feeState,
   309  	}
   310  	w.mtx.RUnlock()
   311  
   312  	if w.parent != nil {
   313  		w.parent.mtx.RLock()
   314  		state.Open = len(w.parent.encPass) == 0 || len(w.parent.pw) > 0
   315  		w.parent.mtx.RUnlock()
   316  	}
   317  
   318  	return state
   319  }
   320  
   321  // setBalance sets the wallet balance.
   322  func (w *xcWallet) setBalance(bal *WalletBalance) {
   323  	w.mtx.Lock()
   324  	w.balance = bal
   325  	w.mtx.Unlock()
   326  }
   327  
   328  // setDisabled sets the wallet disabled field.
   329  func (w *xcWallet) setDisabled(status bool) {
   330  	w.mtx.Lock()
   331  	w.disabled = status
   332  	w.mtx.Unlock()
   333  }
   334  
   335  func (w *xcWallet) isDisabled() bool {
   336  	w.mtx.RLock()
   337  	defer w.mtx.RUnlock()
   338  	return w.disabled
   339  }
   340  
   341  func (w *xcWallet) currentDepositAddress() string {
   342  	w.mtx.RLock()
   343  	defer w.mtx.RUnlock()
   344  	return w.address
   345  }
   346  
   347  func (w *xcWallet) refreshDepositAddress() (string, error) {
   348  	if !w.connected() {
   349  		return "", fmt.Errorf("cannot get address from unconnected %s wallet",
   350  			unbip(w.AssetID))
   351  	}
   352  
   353  	na, is := w.Wallet.(asset.NewAddresser)
   354  	if !is {
   355  		return "", fmt.Errorf("wallet does not generate new addresses")
   356  	}
   357  
   358  	addr, err := na.NewAddress()
   359  	if err != nil {
   360  		return "", fmt.Errorf("%s Wallet.Address error: %w", unbip(w.AssetID), err)
   361  	}
   362  
   363  	w.mtx.Lock()
   364  	w.address = addr
   365  	w.mtx.Unlock()
   366  
   367  	return addr, nil
   368  }
   369  
   370  // connected is true if the wallet has already been connected.
   371  func (w *xcWallet) connected() bool {
   372  	w.mtx.RLock()
   373  	defer w.mtx.RUnlock()
   374  	return w.hookedUp
   375  }
   376  
   377  // checkPeersAndSyncStatus checks that the wallet is synced, and has peers
   378  // otherwise we might double spend if the wallet keys were used elsewhere. This
   379  // should be checked before attempting to send funds but does not replace any
   380  // other checks that may be required.
   381  func (w *xcWallet) checkPeersAndSyncStatus() error {
   382  	w.mtx.RLock()
   383  	defer w.mtx.RUnlock()
   384  	if w.peerCount < 1 {
   385  		return fmt.Errorf("%s wallet has no connected peers", unbip(w.AssetID))
   386  	}
   387  	if !w.syncStatus.Synced {
   388  		return fmt.Errorf("%s wallet is not synchronized", unbip(w.AssetID))
   389  	}
   390  	return nil
   391  }
   392  
   393  // Connect calls the dex.Connector's Connect method, sets the xcWallet.hookedUp
   394  // flag to true, and validates the deposit address. Use Disconnect to cleanly
   395  // shutdown the wallet.
   396  func (w *xcWallet) Connect() error {
   397  	w.connectMtx.Lock()
   398  	defer w.connectMtx.Unlock()
   399  
   400  	// Disabled wallet cannot be connected to unless it is enabled.
   401  	if w.isDisabled() {
   402  		return fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID)))
   403  	}
   404  
   405  	if w.connected() {
   406  		return nil
   407  	}
   408  
   409  	// No parent context; use Disconnect instead. Also note that there's no
   410  	// reconnect loop for wallet like with the server Connectors, so we use
   411  	// ConnectOnce so that the ConnectionMaster's On method will report false.
   412  	err := w.connector.ConnectOnce(context.Background())
   413  	if err != nil {
   414  		return fmt.Errorf("ConnectOnce error: %w", err)
   415  	}
   416  
   417  	var ready bool
   418  	defer func() {
   419  		// Now that we are connected, we must Disconnect if any calls fail below
   420  		// since we are considering this wallet not "hookedUp".
   421  		if !ready {
   422  			w.connector.Disconnect()
   423  		}
   424  	}()
   425  
   426  	ss, err := w.SyncStatus()
   427  	if err != nil {
   428  		return fmt.Errorf("SyncStatus error: %w", err)
   429  	}
   430  
   431  	w.mtx.Lock()
   432  	defer w.mtx.Unlock()
   433  	haveAddress := w.address != ""
   434  	if haveAddress {
   435  		if haveAddress, err = w.OwnsDepositAddress(w.address); err != nil {
   436  			return fmt.Errorf("OwnsDepositAddress error: %w", err)
   437  		}
   438  	}
   439  	if !haveAddress {
   440  		if w.address, err = w.DepositAddress(); err != nil {
   441  			return fmt.Errorf("DepositAddress error: %w", err)
   442  		}
   443  	}
   444  	w.feeRate() // prime the feeState
   445  	w.hookedUp = true
   446  	w.syncStatus = ss
   447  	ready = true
   448  
   449  	return nil
   450  }
   451  
   452  // Disconnect calls the dex.Connector's Disconnect method and sets the
   453  // xcWallet.hookedUp flag to false.
   454  func (w *xcWallet) Disconnect() {
   455  	// Disabled wallet is already disconnected.
   456  	if w.isDisabled() {
   457  		return
   458  	}
   459  	w.connector.Disconnect()
   460  	w.mtx.Lock()
   461  	w.hookedUp = false
   462  	w.mtx.Unlock()
   463  }
   464  
   465  // rescan will initiate a rescan of the wallet if the asset.Wallet
   466  // implementation is a Rescanner.
   467  func (w *xcWallet) rescan(ctx context.Context, bday /* unix time seconds*/ uint64) error {
   468  	if !w.connected() {
   469  		return errWalletNotConnected
   470  	}
   471  	rescanner, ok := w.Wallet.(asset.Rescanner)
   472  	if !ok {
   473  		return errors.New("wallet does not support rescanning")
   474  	}
   475  	return rescanner.Rescan(ctx, bday)
   476  }
   477  
   478  // logFilePath returns the path of the wallet's log file if the
   479  // asset.Wallet implementation is a LogFiler.
   480  func (w *xcWallet) logFilePath() (string, error) {
   481  	logFiler, ok := w.Wallet.(asset.LogFiler)
   482  	if !ok {
   483  		return "", errors.New("wallet does not support getting log file")
   484  	}
   485  	return logFiler.LogFilePath(), nil
   486  }
   487  
   488  // accelerateOrder uses the Child-Pays-For-Parent technique to accelerate an
   489  // order if the wallet is an Accelerator.
   490  func (w *xcWallet) accelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) {
   491  	if w.isDisabled() { // cannot perform order acceleration with disabled wallet.
   492  		return nil, "", fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID)))
   493  	}
   494  	if !w.connected() {
   495  		return nil, "", errWalletNotConnected
   496  	}
   497  	accelerator, ok := w.Wallet.(asset.Accelerator)
   498  	if !ok {
   499  		return nil, "", errors.New("wallet does not support acceleration")
   500  	}
   501  	return accelerator.AccelerateOrder(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate)
   502  }
   503  
   504  // accelerationEstimate estimates the cost to accelerate an order if the wallet
   505  // is an Accelerator.
   506  func (w *xcWallet) accelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, error) {
   507  	if w.isDisabled() { // cannot perform acceleration estimate with disabled wallet.
   508  		return 0, fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID)))
   509  	}
   510  	if !w.connected() {
   511  		return 0, errWalletNotConnected
   512  	}
   513  	accelerator, ok := w.Wallet.(asset.Accelerator)
   514  	if !ok {
   515  		return 0, errors.New("wallet does not support acceleration")
   516  	}
   517  
   518  	return accelerator.AccelerationEstimate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion)
   519  }
   520  
   521  // preAccelerate gives the user information about accelerating an order if the
   522  // wallet is an Accelerator.
   523  func (w *xcWallet) preAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *asset.XYRange, *asset.EarlyAcceleration, error) {
   524  	if w.isDisabled() { // cannot perform operation with disabled wallet.
   525  		return 0, &asset.XYRange{}, nil, fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID)))
   526  	}
   527  	if !w.connected() {
   528  		return 0, &asset.XYRange{}, nil, errWalletNotConnected
   529  	}
   530  	accelerator, ok := w.Wallet.(asset.Accelerator)
   531  	if !ok {
   532  		return 0, &asset.XYRange{}, nil, errors.New("wallet does not support acceleration")
   533  	}
   534  
   535  	return accelerator.PreAccelerate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, feeSuggestion)
   536  }
   537  
   538  // swapConfirmations calls (asset.Wallet).SwapConfirmations with a timeout
   539  // Context. If the coin cannot be located, an asset.CoinNotFoundError is
   540  // returned. If the coin is located, but recognized as spent, no error is
   541  // returned.
   542  func (w *xcWallet) swapConfirmations(ctx context.Context, coinID []byte, contract []byte, matchTime uint64) (uint32, bool, error) {
   543  	if w.isDisabled() { // cannot check swap confirmation with disabled wallet.
   544  		return 0, false, fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(w.AssetID)))
   545  	}
   546  	if !w.connected() {
   547  		return 0, false, errWalletNotConnected
   548  	}
   549  	return w.Wallet.SwapConfirmations(ctx, coinID, contract, time.UnixMilli(int64(matchTime)))
   550  }
   551  
   552  // TxHistory returns all the transactions a wallet has made. If refID
   553  // is nil, then transactions starting from the most recent are returned
   554  // (past is ignored). If past is true, the transactions prior to the
   555  // refID are returned, otherwise the transactions after the refID are
   556  // returned. n is the number of transactions to return. If n is <= 0,
   557  // all the transactions will be returned.
   558  func (w *xcWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) {
   559  	if !w.connected() {
   560  		return nil, errWalletNotConnected
   561  	}
   562  
   563  	historian, ok := w.Wallet.(asset.WalletHistorian)
   564  	if !ok {
   565  		return nil, fmt.Errorf("wallet does not support transaction history")
   566  	}
   567  
   568  	return historian.TxHistory(n, refID, past)
   569  }
   570  
   571  // WalletTransaction returns information about a transaction that the wallet
   572  // has made or one in which that wallet received funds.
   573  func (w *xcWallet) WalletTransaction(ctx context.Context, txID string) (*asset.WalletTransaction, error) {
   574  	if !w.connected() {
   575  		return nil, errWalletNotConnected
   576  	}
   577  
   578  	historian, ok := w.Wallet.(asset.WalletHistorian)
   579  	if !ok {
   580  		return nil, fmt.Errorf("wallet does not support transaction history")
   581  	}
   582  
   583  	return historian.WalletTransaction(ctx, txID)
   584  }
   585  
   586  // MakeBondTx authors a DEX time-locked fidelity bond transaction if the
   587  // asset.Wallet implementation is a Bonder.
   588  func (w *xcWallet) MakeBondTx(ver uint16, amt, feeRate uint64, lockTime time.Time, priv *secp256k1.PrivateKey, acctID []byte) (*asset.Bond, func(), error) {
   589  	bonder, ok := w.Wallet.(asset.Bonder)
   590  	if !ok {
   591  		return nil, nil, errors.New("wallet does not support making bond transactions")
   592  	}
   593  	return bonder.MakeBondTx(ver, amt, feeRate, lockTime, priv, acctID)
   594  }
   595  
   596  // BondsFeeBuffer gets the bonds fee buffer based on the provided fee rate.
   597  func (w *xcWallet) BondsFeeBuffer(feeRate uint64) uint64 {
   598  	bonder, ok := w.Wallet.(asset.Bonder)
   599  	if !ok {
   600  		return 0
   601  	}
   602  	return bonder.BondsFeeBuffer(feeRate)
   603  }
   604  
   605  // RefundBond will refund the bond if the asset.Wallet implementation is a
   606  // Bonder. The lock time must be passed to spend the bond. LockTimeExpired
   607  // should be used to check first.
   608  func (w *xcWallet) RefundBond(ctx context.Context, ver uint16, coinID, script []byte, amt uint64, priv *secp256k1.PrivateKey) (asset.Coin, error) {
   609  	bonder, ok := w.Wallet.(asset.Bonder)
   610  	if !ok {
   611  		return nil, errors.New("wallet does not support refunding bond transactions")
   612  	}
   613  	return bonder.RefundBond(ctx, ver, coinID, script, amt, priv)
   614  }
   615  
   616  // FindBond finds the bond with coinID and returns the values used to create it.
   617  // The output should be unspent with the lockTime set to some time in the future.
   618  func (w *xcWallet) FindBond(ctx context.Context, coinID []byte, searchUntil time.Time) (*asset.BondDetails, error) {
   619  	bonder, ok := w.Wallet.(asset.Bonder)
   620  	if !ok {
   621  		return nil, errors.New("wallet does not support making bond transactions")
   622  	}
   623  	return bonder.FindBond(ctx, coinID, searchUntil)
   624  }
   625  
   626  // SendTransaction broadcasts a raw transaction if the wallet is a Broadcaster.
   627  func (w *xcWallet) SendTransaction(tx []byte) ([]byte, error) {
   628  	bonder, ok := w.Wallet.(asset.Broadcaster)
   629  	if !ok {
   630  		return nil, errors.New("wallet is not a Broadcaster")
   631  	}
   632  	return bonder.SendTransaction(tx)
   633  }
   634  
   635  // ApproveToken sends an approval transaction if the wallet is a TokenApprover.
   636  func (w *xcWallet) ApproveToken(assetVersion uint32, onConfirm func()) (string, error) {
   637  	approver, ok := w.Wallet.(asset.TokenApprover)
   638  	if !ok {
   639  		return "", fmt.Errorf("%s wallet is not a TokenApprover", unbip(w.AssetID))
   640  	}
   641  	return approver.ApproveToken(assetVersion, onConfirm)
   642  }
   643  
   644  // ApproveToken sends an approval transaction if the wallet is a TokenApprover.
   645  func (w *xcWallet) UnapproveToken(assetVersion uint32, onConfirm func()) (string, error) {
   646  	approver, ok := w.Wallet.(asset.TokenApprover)
   647  	if !ok {
   648  		return "", fmt.Errorf("%s wallet is not a TokenApprover", unbip(w.AssetID))
   649  	}
   650  	return approver.UnapproveToken(assetVersion, onConfirm)
   651  }
   652  
   653  // ApprovalFee returns the estimated fee to send an approval transaction if the
   654  // wallet is a TokenApprover.
   655  func (w *xcWallet) ApprovalFee(assetVersion uint32, approval bool) (uint64, error) {
   656  	approver, ok := w.Wallet.(asset.TokenApprover)
   657  	if !ok {
   658  		return 0, fmt.Errorf("%s wallet is not a TokenApprover", unbip(w.AssetID))
   659  	}
   660  	return approver.ApprovalFee(assetVersion, approval)
   661  }
   662  
   663  // ApprovalStatus returns the approval status of each version of the asset if
   664  // the wallet is a TokenApprover.
   665  func (w *xcWallet) ApprovalStatus() map[uint32]asset.ApprovalStatus {
   666  	approver, ok := w.Wallet.(asset.TokenApprover)
   667  	if !ok {
   668  		return nil
   669  	}
   670  
   671  	return approver.ApprovalStatus()
   672  }
   673  
   674  func (w *xcWallet) setFeeState(feeRate uint64) {
   675  	swapFees, refundFees, err := w.SingleLotSwapRefundFees(asset.VersionNewest, feeRate, false)
   676  	if err != nil {
   677  		w.log.Errorf("Error getting single-lot swap+refund estimates: %v", err)
   678  	}
   679  	redeemFees, err := w.SingleLotRedeemFees(asset.VersionNewest, feeRate)
   680  	if err != nil {
   681  		w.log.Errorf("Error getting single-lot redeem estimates: %v", err)
   682  	}
   683  	sendFees := w.StandardSendFee(feeRate)
   684  	w.feeState.Store(&FeeState{
   685  		Rate:    feeRate,
   686  		Send:    sendFees,
   687  		Swap:    swapFees,
   688  		Redeem:  redeemFees,
   689  		Refund:  refundFees,
   690  		StampMS: time.Now().UnixMilli(),
   691  	})
   692  }
   693  
   694  // feeRate returns a fee rate for a FeeRater is available and generates a
   695  // non-zero rate.
   696  func (w *xcWallet) feeRate() uint64 {
   697  	if rater, is := w.Wallet.(asset.FeeRater); !is {
   698  		return 0
   699  	} else if r := rater.FeeRate(); r != 0 {
   700  		w.setFeeState(r)
   701  		return r
   702  	}
   703  	return 0
   704  }