decred.org/dcrdex@v1.0.3/client/asset/btc/spv_wrapper.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  // spvWallet implements a Wallet backed by a built-in btcwallet + Neutrino.
     5  //
     6  // There are a few challenges presented in using an SPV wallet for DEX.
     7  // 1. Finding non-wallet related blockchain data requires possession of the
     8  //    pubkey script, not just transaction hash and output index
     9  // 2. Finding non-wallet related blockchain data can often entail extensive
    10  //    scanning of compact filters. We can limit these scans with more
    11  //    information, such as the match time, which would be the earliest a
    12  //    transaction could be found on-chain.
    13  // 3. We don't see a mempool. We're blind to new transactions until they are
    14  //    mined. This requires special handling by the caller. We've been
    15  //    anticipating this, so Core and Swapper are permissive of missing acks for
    16  //    audit requests.
    17  
    18  package btc
    19  
    20  import (
    21  	"context"
    22  	"encoding/hex"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/fs"
    28  	"math"
    29  	"os"
    30  	"path/filepath"
    31  	"sync"
    32  	"sync/atomic"
    33  	"time"
    34  
    35  	"decred.org/dcrdex/client/asset"
    36  	"decred.org/dcrdex/dex"
    37  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    38  	"github.com/btcsuite/btcd/btcec/v2"
    39  	"github.com/btcsuite/btcd/btcjson"
    40  	"github.com/btcsuite/btcd/btcutil"
    41  	"github.com/btcsuite/btcd/btcutil/gcs"
    42  	"github.com/btcsuite/btcd/btcutil/psbt"
    43  	"github.com/btcsuite/btcd/chaincfg"
    44  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    45  	"github.com/btcsuite/btcd/wire"
    46  	"github.com/btcsuite/btcwallet/waddrmgr"
    47  	"github.com/btcsuite/btcwallet/wallet"
    48  	"github.com/btcsuite/btcwallet/walletdb"
    49  	_ "github.com/btcsuite/btcwallet/walletdb/bdb" // bdb init() registers a driver
    50  	"github.com/btcsuite/btcwallet/wtxmgr"
    51  	"github.com/lightninglabs/neutrino"
    52  	"github.com/lightninglabs/neutrino/headerfs"
    53  )
    54  
    55  const (
    56  	WalletTransactionNotFound = dex.ErrorKind("wallet transaction not found")
    57  	SpentStatusUnknown        = dex.ErrorKind("spend status not known")
    58  	// NOTE: possibly unexport the two above error kinds.
    59  
    60  	// defaultBroadcastWait is long enough for btcwallet's PublishTransaction
    61  	// method to record the outgoing transaction and queue it for broadcasting.
    62  	// This rough duration is necessary since with neutrino as the wallet's
    63  	// chain service, its chainClient.SendRawTransaction call is blocking for up
    64  	// to neutrino.Config.BroadcastTimeout while peers either respond to the inv
    65  	// request with a getdata or time out. However, in virtually all cases, we
    66  	// just need to know that btcwallet was able to create and store the
    67  	// transaction record, and pass it to the chain service.
    68  	defaultBroadcastWait = 2 * time.Second
    69  
    70  	maxFutureBlockTime = 2 * time.Hour // see MaxTimeOffsetSeconds in btcd/blockchain/validate.go
    71  	neutrinoDBName     = "neutrino.db"
    72  	logDirName         = "logs"
    73  	logFileName        = "neutrino.log"
    74  	defaultAcctNum     = 0
    75  	defaultAcctName    = "default"
    76  )
    77  
    78  var wAddrMgrBkt = []byte("waddrmgr")
    79  
    80  // BTCWallet is roughly the (btcwallet/wallet.*Wallet) interface, with some
    81  // additional required methods added.
    82  type BTCWallet interface {
    83  	PublishTransaction(tx *wire.MsgTx, label string) error
    84  	CalculateAccountBalances(account uint32, confirms int32) (wallet.Balances, error)
    85  	ListUnspent(minconf, maxconf int32, acctName string) ([]*btcjson.ListUnspentResult, error)
    86  	FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error)
    87  	ResetLockedOutpoints()
    88  	LockOutpoint(op wire.OutPoint)
    89  	UnlockOutpoint(op wire.OutPoint)
    90  	LockedOutpoints() []btcjson.TransactionInput
    91  	NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error)
    92  	NewAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error)
    93  	PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error)
    94  	Unlock(passphrase []byte, lock <-chan time.Time) error
    95  	Lock()
    96  	Locked() bool
    97  	SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, account uint32, minconf int32,
    98  		satPerKb btcutil.Amount, coinSelectionStrategy wallet.CoinSelectionStrategy, label string) (*wire.MsgTx, error)
    99  	HaveAddress(a btcutil.Address) (bool, error)
   100  	WaitForShutdown()
   101  	ChainSynced() bool // currently unused
   102  	AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error)
   103  	// AccountInfo returns the account information of the wallet for use by the
   104  	// exchange wallet.
   105  	AccountInfo() XCWalletAccount
   106  	// The below methods are not implemented by *wallet.Wallet, so must be
   107  	// implemented by the BTCWallet implementation.
   108  	WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error)
   109  	SyncedTo() waddrmgr.BlockStamp
   110  	SignTx(*wire.MsgTx) error
   111  	BlockNotifications(context.Context) <-chan *BlockNotification
   112  	RescanAsync() error
   113  	ForceRescan()
   114  	Start() (SPVService, error)
   115  	Stop()
   116  	Birthday() time.Time
   117  	Peers() ([]*asset.WalletPeer, error)
   118  	AddPeer(string) error
   119  	RemovePeer(string) error
   120  	GetTransactions(startHeight, endHeight int32, accountName string, cancel <-chan struct{}) (*wallet.GetTransactionsResult, error)
   121  }
   122  
   123  type XCWalletAccount struct {
   124  	AccountName   string
   125  	AccountNumber uint32
   126  }
   127  
   128  // BlockNotification is block hash and height delivered by a BTCWallet when it
   129  // is finished processing a block.
   130  type BlockNotification struct {
   131  	Hash   chainhash.Hash
   132  	Height int32
   133  }
   134  
   135  // SPVService is satisfied by *neutrino.ChainService, with the exception of the
   136  // Peers method, which has a generic interface in place of neutrino.ServerPeer.
   137  type SPVService interface {
   138  	GetBlockHash(int64) (*chainhash.Hash, error)
   139  	BestBlock() (*headerfs.BlockStamp, error)
   140  	Peers() []SPVPeer
   141  	AddPeer(addr string) error
   142  	GetBlockHeight(hash *chainhash.Hash) (int32, error)
   143  	GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error)
   144  	GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error)
   145  	GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error)
   146  	Stop() error
   147  }
   148  
   149  // SPVPeer is satisfied by *neutrino.ServerPeer, but is generalized to
   150  // accommodate underlying implementations other than lightninglabs/neutrino.
   151  type SPVPeer interface {
   152  	StartingHeight() int32
   153  	LastBlock() int32
   154  	Addr() string
   155  }
   156  
   157  // btcChainService wraps *neutrino.ChainService in order to translate the
   158  // neutrino.ServerPeer to the SPVPeer interface type.
   159  type btcChainService struct {
   160  	*neutrino.ChainService
   161  }
   162  
   163  func (s *btcChainService) Peers() []SPVPeer {
   164  	rawPeers := s.ChainService.Peers()
   165  	peers := make([]SPVPeer, 0, len(rawPeers))
   166  	for _, p := range rawPeers {
   167  		peers = append(peers, p)
   168  	}
   169  	return peers
   170  }
   171  
   172  func (s *btcChainService) AddPeer(addr string) error {
   173  	return s.ChainService.ConnectNode(addr, true)
   174  }
   175  
   176  func (s *btcChainService) RemovePeer(addr string) error {
   177  	return s.ChainService.RemoveNodeByAddr(addr)
   178  }
   179  
   180  var _ SPVService = (*btcChainService)(nil)
   181  
   182  // BTCWalletConstructor is a function to construct a BTCWallet.
   183  type BTCWalletConstructor func(dir string, cfg *WalletConfig, chainParams *chaincfg.Params, log dex.Logger) BTCWallet
   184  
   185  func extendAddresses(extIdx, intIdx uint32, btcw *wallet.Wallet) error {
   186  	scopedKeyManager, err := btcw.Manager.FetchScopedKeyManager(waddrmgr.KeyScopeBIP0084)
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	return walletdb.Update(btcw.Database(), func(dbtx walletdb.ReadWriteTx) error {
   192  		ns := dbtx.ReadWriteBucket(wAddrMgrBkt)
   193  		if extIdx > 0 {
   194  			if err := scopedKeyManager.ExtendExternalAddresses(ns, defaultAcctNum, extIdx); err != nil {
   195  				return err
   196  			}
   197  		}
   198  		if intIdx > 0 {
   199  			return scopedKeyManager.ExtendInternalAddresses(ns, defaultAcctNum, intIdx)
   200  		}
   201  		return nil
   202  	})
   203  }
   204  
   205  // spvWallet is an in-process btcwallet.Wallet + neutrino light-filter-based
   206  // Bitcoin wallet. spvWallet controls an instance of btcwallet.Wallet directly
   207  // and does not run or connect to the RPC server.
   208  type spvWallet struct {
   209  	chainParams *chaincfg.Params
   210  	cfg         *WalletConfig
   211  	wallet      BTCWallet
   212  	cl          SPVService
   213  	acctNum     uint32
   214  	dir         string
   215  	decodeAddr  dexbtc.AddressDecoder
   216  
   217  	log dex.Logger
   218  
   219  	tipChan            chan *BlockVector
   220  	syncTarget         int32
   221  	lastPrenatalHeight int32
   222  
   223  	*BlockFiltersScanner
   224  }
   225  
   226  var _ Wallet = (*spvWallet)(nil)
   227  var _ tipNotifier = (*spvWallet)(nil)
   228  
   229  // reconfigure attempts to reconfigure the rpcClient for the new settings. Live
   230  // reconfiguration is only attempted if the new wallet type is walletTypeSPV.
   231  func (w *spvWallet) reconfigure(cfg *asset.WalletConfig, currentAddress string) (restartRequired bool, err error) {
   232  	// If the wallet type is not SPV, then we can't reconfigure the wallet.
   233  	if cfg.Type != walletTypeSPV {
   234  		restartRequired = true
   235  		return
   236  	}
   237  
   238  	// Check if the SPV wallet exists. If it doesn't, then we can't reconfigure it.
   239  	exists, err := walletExists(w.dir, w.chainParams)
   240  	if err != nil {
   241  		return false, err
   242  	}
   243  	if !exists {
   244  		return false, errors.New("wallet not found")
   245  	}
   246  
   247  	return false, nil
   248  }
   249  
   250  // tipFeed satisfies the tipNotifier interface, signaling that *spvWallet
   251  // will take precedence in sending block notifications.
   252  func (w *spvWallet) tipFeed() <-chan *BlockVector {
   253  	return w.tipChan
   254  }
   255  
   256  func (w *spvWallet) RawRequest(ctx context.Context, method string, params []json.RawMessage) (json.RawMessage, error) {
   257  	// Not needed for spv wallet.
   258  	return nil, errors.New("RawRequest not available on spv")
   259  }
   260  
   261  func (w *spvWallet) ownsAddress(addr btcutil.Address) (bool, error) {
   262  	return w.wallet.HaveAddress(addr)
   263  }
   264  
   265  func (w *spvWallet) sendRawTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) {
   266  	// Publish the transaction in a goroutine so the caller may wait for a given
   267  	// period before it goes asynchronous and it is assumed that btcwallet at
   268  	// least succeeded with its DB updates and queueing of the transaction for
   269  	// rebroadcasting. In the future, a new btcwallet method should be added
   270  	// that returns after performing its internal actions, but broadcasting
   271  	// asynchronously and sending the outcome in a channel or promise.
   272  	res := make(chan error, 1)
   273  	go func() {
   274  		tStart := time.Now()
   275  		defer close(res)
   276  		if err := w.wallet.PublishTransaction(tx, ""); err != nil {
   277  			w.log.Errorf("PublishTransaction(%v) failure: %v", tx.TxHash(), err)
   278  			res <- err
   279  			return
   280  		}
   281  		defer func() {
   282  			w.log.Tracef("PublishTransaction(%v) completed in %v", tx.TxHash(), time.Since(tStart))
   283  		}() // after outpoint unlocking and signalling
   284  		res <- nil
   285  	}()
   286  
   287  	select {
   288  	case err := <-res:
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  	case <-time.After(defaultBroadcastWait):
   293  		w.log.Debugf("No error from PublishTransaction after %v for txn %v. "+
   294  			"Assuming wallet accepted it.", defaultBroadcastWait, tx.TxHash())
   295  	}
   296  
   297  	// bitcoind would unlock these, btcwallet does not. Although it seems like
   298  	// they are no longer returned from ListUnspent after publishing, it must
   299  	// not be returned by LockedOutpoints (listlockunspent) for the lockedSats
   300  	// computations to be correct.
   301  	for _, txIn := range tx.TxIn {
   302  		w.wallet.UnlockOutpoint(txIn.PreviousOutPoint)
   303  	}
   304  
   305  	txHash := tx.TxHash() // down here in case... the msgTx was mutated?
   306  	return &txHash, nil
   307  }
   308  
   309  func (w *spvWallet) getBlock(blockHash chainhash.Hash) (*wire.MsgBlock, error) {
   310  	block, err := w.cl.GetBlock(blockHash)
   311  	if err != nil {
   312  		return nil, fmt.Errorf("neutrino GetBlock error: %v", err)
   313  	}
   314  
   315  	return block.MsgBlock(), nil
   316  }
   317  
   318  func (w *spvWallet) getBlockHash(blockHeight int64) (*chainhash.Hash, error) {
   319  	return w.cl.GetBlockHash(blockHeight)
   320  }
   321  
   322  // getBlockHeight gets the mainchain height for the specified block. Returns
   323  // error for orphaned blocks.
   324  func (w *spvWallet) getBlockHeight(h *chainhash.Hash) (int32, error) {
   325  	return w.cl.GetBlockHeight(h)
   326  }
   327  
   328  func (w *spvWallet) getBestBlockHash() (*chainhash.Hash, error) {
   329  	blk := w.wallet.SyncedTo()
   330  	return &blk.Hash, nil
   331  }
   332  
   333  // getBestBlockHeight returns the height of the best block processed by the
   334  // wallet, which indicates the height at which the compact filters have been
   335  // retrieved and scanned for wallet addresses. This is may be less than
   336  // getChainHeight, which indicates the height that the chain service has reached
   337  // in its retrieval of block headers and compact filter headers.
   338  func (w *spvWallet) getBestBlockHeight() (int32, error) {
   339  	return w.wallet.SyncedTo().Height, nil
   340  }
   341  
   342  // getChainStamp satisfies chainStamper for manual median time calculations.
   343  func (w *spvWallet) getChainStamp(blockHash *chainhash.Hash) (stamp time.Time, prevHash *chainhash.Hash, err error) {
   344  	hdr, err := w.cl.GetBlockHeader(blockHash)
   345  	if err != nil {
   346  		return
   347  	}
   348  	return hdr.Timestamp, &hdr.PrevBlock, nil
   349  }
   350  
   351  // medianTime is the median time for the current best block.
   352  func (w *spvWallet) medianTime() (time.Time, error) {
   353  	blk := w.wallet.SyncedTo()
   354  	return CalcMedianTime(w.getChainStamp, &blk.Hash)
   355  }
   356  
   357  // getChainHeight is only for confirmations since it does not reflect the wallet
   358  // manager's sync height, just the chain service.
   359  func (w *spvWallet) getChainHeight() (int32, error) {
   360  	blk, err := w.cl.BestBlock()
   361  	if err != nil {
   362  		return -1, err
   363  	}
   364  	return blk.Height, err
   365  }
   366  
   367  func (w *spvWallet) peerCount() (uint32, error) {
   368  	return uint32(len(w.cl.Peers())), nil
   369  }
   370  
   371  func (w *spvWallet) peers() ([]*asset.WalletPeer, error) {
   372  	return w.wallet.Peers()
   373  }
   374  
   375  func (w *spvWallet) addPeer(addr string) error {
   376  	return w.wallet.AddPeer(addr)
   377  }
   378  
   379  func (w *spvWallet) removePeer(addr string) error {
   380  	return w.wallet.RemovePeer(addr)
   381  }
   382  
   383  // syncHeight is the best known sync height among peers.
   384  func (w *spvWallet) syncHeight() int32 {
   385  	var maxHeight int32
   386  	for _, p := range w.cl.Peers() {
   387  		tipHeight := p.StartingHeight()
   388  		lastBlockHeight := p.LastBlock()
   389  		if lastBlockHeight > tipHeight {
   390  			tipHeight = lastBlockHeight
   391  		}
   392  		if tipHeight > maxHeight {
   393  			maxHeight = tipHeight
   394  		}
   395  	}
   396  	return maxHeight
   397  }
   398  
   399  // SyncStatus is information about the wallet's sync status.
   400  //
   401  // The neutrino wallet has a two stage sync:
   402  //  1. chain service fetching block headers and filter headers
   403  //  2. wallet address manager retrieving and scanning filters
   404  //
   405  // We only report a single sync height, so we are going to show some progress in
   406  // the chain service sync stage that comes before the wallet has performed any
   407  // address recovery/rescan, and switch to the wallet's sync height when it
   408  // reports non-zero height.
   409  func (w *spvWallet) syncStatus() (*asset.SyncStatus, error) {
   410  	// Chain service headers (block and filter) height.
   411  	chainBlk, err := w.cl.BestBlock()
   412  	if err != nil {
   413  		return nil, err
   414  	}
   415  
   416  	currentHeight := chainBlk.Height
   417  
   418  	var target int32
   419  	if len(w.cl.Peers()) > 0 {
   420  		target = w.syncHeight()
   421  	} else { // use cached value if available
   422  		target = atomic.LoadInt32(&w.syncTarget)
   423  	}
   424  
   425  	if target == 0 {
   426  		return new(asset.SyncStatus), nil
   427  	}
   428  
   429  	var synced bool
   430  	var blk *BlockVector
   431  	// Wallet address manager sync height.
   432  	if chainBlk.Timestamp.After(w.wallet.Birthday()) {
   433  		// After the wallet's birthday, the wallet address manager should begin
   434  		// syncing. Although block time stamps are not necessarily monotonically
   435  		// increasing, this is a reasonable condition at which the wallet's sync
   436  		// height should be consulted instead of the chain service's height.
   437  		walletBlock := w.wallet.SyncedTo()
   438  		if walletBlock.Height == 0 {
   439  			// The wallet is about to start its sync, so just return the last
   440  			// chain service height prior to wallet birthday until it begins.
   441  			h := atomic.LoadInt32(&w.lastPrenatalHeight)
   442  			return &asset.SyncStatus{
   443  				Synced:       false,
   444  				TargetHeight: uint64(target),
   445  				Blocks:       uint64(h),
   446  			}, nil
   447  		}
   448  		blk = &BlockVector{
   449  			Height: int64(walletBlock.Height),
   450  			Hash:   walletBlock.Hash,
   451  		}
   452  		currentHeight = walletBlock.Height
   453  		synced = currentHeight >= target // maybe && w.wallet.ChainSynced()
   454  	} else {
   455  		// Chain service still syncing.
   456  		blk = &BlockVector{
   457  			Height: int64(currentHeight),
   458  			Hash:   chainBlk.Hash,
   459  		}
   460  		atomic.StoreInt32(&w.lastPrenatalHeight, currentHeight)
   461  	}
   462  
   463  	if target > 0 && atomic.SwapInt32(&w.syncTarget, target) == 0 {
   464  		w.tipChan <- blk
   465  	}
   466  
   467  	return &asset.SyncStatus{
   468  		Synced:       synced,
   469  		TargetHeight: uint64(target),
   470  		Blocks:       uint64(blk.Height),
   471  	}, nil
   472  }
   473  
   474  // ownsInputs determines if we own the inputs of the tx.
   475  func (w *spvWallet) ownsInputs(txid string) bool {
   476  	txHash, err := chainhash.NewHashFromStr(txid)
   477  	if err != nil {
   478  		w.log.Warnf("Error decoding txid %q: %v", txid, err)
   479  		return false
   480  	}
   481  	txDetails, err := w.wallet.WalletTransaction(txHash)
   482  	if err != nil {
   483  		w.log.Warnf("walletTransaction(%v) error: %v", txid, err)
   484  		return false
   485  	}
   486  
   487  	for _, txIn := range txDetails.MsgTx.TxIn {
   488  		_, _, _, _, err = w.wallet.FetchInputInfo(&txIn.PreviousOutPoint)
   489  		if err != nil {
   490  			if !errors.Is(err, wallet.ErrNotMine) {
   491  				w.log.Warnf("FetchInputInfo error: %v", err)
   492  			}
   493  			return false
   494  		}
   495  	}
   496  	return true
   497  }
   498  
   499  func (w *spvWallet) listTransactionsSinceBlock(blockHeight int32) ([]*ListTransactionsResult, error) {
   500  	acctName := w.wallet.AccountInfo().AccountName
   501  	tip, err := w.cl.BestBlock()
   502  	if err != nil {
   503  		return nil, fmt.Errorf("BestBlock error: %v", err)
   504  	}
   505  
   506  	// We use GetTransactions instead of ListSinceBlock, because ListSinceBlock
   507  	// does not include transactions that pay to a change address, which
   508  	// Redeem, Refund, and RedeemBond do.
   509  	res, err := w.wallet.GetTransactions(blockHeight, tip.Height, acctName, nil)
   510  	if err != nil {
   511  		return nil, err
   512  	}
   513  
   514  	txs := make([]*ListTransactionsResult, 0, len(res.MinedTransactions)+len(res.UnminedTransactions))
   515  
   516  	toLTR := func(tx *wallet.TransactionSummary, blockHeight uint32, blockTime uint64) *ListTransactionsResult {
   517  		fee := tx.Fee.ToBTC()
   518  		return &ListTransactionsResult{
   519  			TxID:        tx.Hash.String(),
   520  			BlockHeight: blockHeight,
   521  			BlockTime:   blockTime,
   522  			Fee:         &fee,
   523  			Send:        len(tx.MyInputs) > 0,
   524  		}
   525  	}
   526  
   527  	for _, block := range res.MinedTransactions {
   528  		for _, tx := range block.Transactions {
   529  			txs = append(txs, toLTR(&tx, uint32(block.Height), uint64(block.Timestamp)))
   530  		}
   531  	}
   532  
   533  	for _, tx := range res.UnminedTransactions {
   534  		txs = append(txs, toLTR(&tx, 0, 0))
   535  	}
   536  
   537  	return txs, nil
   538  }
   539  
   540  // balances retrieves a wallet's balance details.
   541  func (w *spvWallet) balances() (*GetBalancesResult, error) {
   542  	// Determine trusted vs untrusted coins with listunspent.
   543  	unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, w.wallet.AccountInfo().AccountName)
   544  	if err != nil {
   545  		return nil, fmt.Errorf("error listing unspent outputs: %w", err)
   546  	}
   547  	var trusted, untrusted btcutil.Amount
   548  	for _, txout := range unspents {
   549  		if txout.Confirmations > 0 || w.ownsInputs(txout.TxID) {
   550  			trusted += btcutil.Amount(toSatoshi(txout.Amount))
   551  			continue
   552  		}
   553  		untrusted += btcutil.Amount(toSatoshi(txout.Amount))
   554  	}
   555  
   556  	// listunspent does not include immature coinbase outputs or locked outputs.
   557  	bals, err := w.wallet.CalculateAccountBalances(w.acctNum, 0 /* confs */)
   558  	if err != nil {
   559  		return nil, err
   560  	}
   561  	w.log.Tracef("Bals: spendable = %v (%v trusted, %v untrusted, %v assumed locked), immature = %v",
   562  		bals.Spendable, trusted, untrusted, bals.Spendable-trusted-untrusted, bals.ImmatureReward)
   563  	// Locked outputs would be in wallet.Balances.Spendable. Assume they would
   564  	// be considered trusted and add them back in.
   565  	if all := trusted + untrusted; bals.Spendable > all {
   566  		trusted += bals.Spendable - all
   567  	}
   568  
   569  	return &GetBalancesResult{
   570  		Mine: Balances{
   571  			Trusted:   trusted.ToBTC(),
   572  			Untrusted: untrusted.ToBTC(),
   573  			Immature:  bals.ImmatureReward.ToBTC(),
   574  		},
   575  	}, nil
   576  }
   577  
   578  // listUnspent retrieves list of the wallet's UTXOs.
   579  func (w *spvWallet) listUnspent() ([]*ListUnspentResult, error) {
   580  	unspents, err := w.wallet.ListUnspent(0, math.MaxInt32, w.wallet.AccountInfo().AccountName)
   581  	if err != nil {
   582  		return nil, err
   583  	}
   584  	res := make([]*ListUnspentResult, 0, len(unspents))
   585  	for _, utxo := range unspents {
   586  		// If the utxo is unconfirmed, we should determine whether it's "safe"
   587  		// by seeing if we control the inputs of its transaction.
   588  		safe := utxo.Confirmations > 0 || w.ownsInputs(utxo.TxID)
   589  
   590  		// These hex decodings are unlikely to fail because they come directly
   591  		// from the listunspent result. Regardless, they should not result in an
   592  		// error for the caller as we can return the valid utxos.
   593  		pkScript, err := hex.DecodeString(utxo.ScriptPubKey)
   594  		if err != nil {
   595  			w.log.Warnf("ScriptPubKey decode failure: %v", err)
   596  			continue
   597  		}
   598  
   599  		redeemScript, err := hex.DecodeString(utxo.RedeemScript)
   600  		if err != nil {
   601  			w.log.Warnf("ScriptPubKey decode failure: %v", err)
   602  			continue
   603  		}
   604  
   605  		res = append(res, &ListUnspentResult{
   606  			TxID:    utxo.TxID,
   607  			Vout:    utxo.Vout,
   608  			Address: utxo.Address,
   609  			// Label: ,
   610  			ScriptPubKey:  pkScript,
   611  			Amount:        utxo.Amount,
   612  			Confirmations: uint32(utxo.Confirmations),
   613  			RedeemScript:  redeemScript,
   614  			Spendable:     utxo.Spendable,
   615  			// Solvable: ,
   616  			SafePtr: &safe,
   617  		})
   618  	}
   619  	return res, nil
   620  }
   621  
   622  // lockUnspent locks and unlocks outputs for spending. An output that is part of
   623  // an order, but not yet spent, should be locked until spent or until the order
   624  // is  canceled or fails.
   625  func (w *spvWallet) lockUnspent(unlock bool, ops []*Output) error {
   626  	switch {
   627  	case unlock && len(ops) == 0:
   628  		w.wallet.ResetLockedOutpoints()
   629  	default:
   630  		for _, op := range ops {
   631  			op := wire.OutPoint{Hash: op.Pt.TxHash, Index: op.Pt.Vout}
   632  			if unlock {
   633  				w.wallet.UnlockOutpoint(op)
   634  			} else {
   635  				w.wallet.LockOutpoint(op)
   636  			}
   637  		}
   638  	}
   639  	return nil
   640  }
   641  
   642  // listLockUnspent returns a slice of outpoints for all unspent outputs marked
   643  // as locked by a wallet.
   644  func (w *spvWallet) listLockUnspent() ([]*RPCOutpoint, error) {
   645  	outpoints := w.wallet.LockedOutpoints()
   646  	pts := make([]*RPCOutpoint, 0, len(outpoints))
   647  	for _, pt := range outpoints {
   648  		pts = append(pts, &RPCOutpoint{
   649  			TxID: pt.Txid,
   650  			Vout: pt.Vout,
   651  		})
   652  	}
   653  	return pts, nil
   654  }
   655  
   656  // changeAddress gets a new internal address from the wallet. The address will
   657  // be bech32-encoded (P2WPKH).
   658  func (w *spvWallet) changeAddress() (btcutil.Address, error) {
   659  	return w.wallet.NewChangeAddress(w.acctNum, waddrmgr.KeyScopeBIP0084)
   660  }
   661  
   662  // externalAddress gets a new bech32-encoded (P2WPKH) external address from the
   663  // wallet.
   664  func (w *spvWallet) externalAddress() (btcutil.Address, error) {
   665  	return w.wallet.NewAddress(w.acctNum, waddrmgr.KeyScopeBIP0084)
   666  }
   667  
   668  // signTx attempts to have the wallet sign the transaction inputs.
   669  func (w *spvWallet) signTx(tx *wire.MsgTx) (*wire.MsgTx, error) {
   670  	// Can't use btcwallet.Wallet.SignTransaction, because it doesn't work for
   671  	// segwit transactions (for real?).
   672  	return tx, w.wallet.SignTx(tx)
   673  }
   674  
   675  // privKeyForAddress retrieves the private key associated with the specified
   676  // address.
   677  func (w *spvWallet) privKeyForAddress(addr string) (*btcec.PrivateKey, error) {
   678  	a, err := w.decodeAddr(addr, w.chainParams)
   679  	if err != nil {
   680  		return nil, err
   681  	}
   682  	return w.wallet.PrivKeyForAddress(a)
   683  }
   684  
   685  // Unlock unlocks the wallet.
   686  func (w *spvWallet) Unlock(pw []byte) error {
   687  	return w.wallet.Unlock(pw, nil)
   688  }
   689  
   690  // Lock locks the wallet.
   691  func (w *spvWallet) Lock() error {
   692  	w.wallet.Lock()
   693  	return nil
   694  }
   695  
   696  // estimateSendTxFee callers should provide at least one output value.
   697  func (w *spvWallet) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract bool) (fee uint64, err error) {
   698  	minTxSize := uint64(tx.SerializeSize())
   699  	var sendAmount uint64
   700  	for _, txOut := range tx.TxOut {
   701  		sendAmount += uint64(txOut.Value)
   702  	}
   703  
   704  	unspents, err := w.listUnspent()
   705  	if err != nil {
   706  		return 0, fmt.Errorf("error listing unspent outputs: %w", err)
   707  	}
   708  
   709  	utxos, _, _, err := convertUnspent(0, unspents, w.chainParams)
   710  	if err != nil {
   711  		return 0, fmt.Errorf("error converting unspent outputs: %w", err)
   712  	}
   713  
   714  	enough := sendEnough(sendAmount, feeRate, subtract, minTxSize, true, false)
   715  	sum, _, inputsSize, _, _, _, _, err := TryFund(utxos, enough)
   716  	if err != nil {
   717  		return 0, err
   718  	}
   719  
   720  	txSize := minTxSize + inputsSize
   721  	estFee := feeRate * txSize
   722  	remaining := sum - sendAmount
   723  
   724  	// Check if there will be a change output if there is enough remaining.
   725  	estFeeWithChange := (txSize + dexbtc.P2WPKHOutputSize) * feeRate
   726  	var changeValue uint64
   727  	if remaining > estFeeWithChange {
   728  		changeValue = remaining - estFeeWithChange
   729  	}
   730  
   731  	if subtract {
   732  		// fees are already included in sendAmount, anything else is change.
   733  		changeValue = remaining
   734  	}
   735  
   736  	var finalFee uint64
   737  	if dexbtc.IsDustVal(dexbtc.P2WPKHOutputSize, changeValue, feeRate, true) {
   738  		// remaining cannot cover a non-dust change and the fee for the change.
   739  		finalFee = estFee + remaining
   740  	} else {
   741  		// additional fee will be paid for non-dust change
   742  		finalFee = estFeeWithChange
   743  	}
   744  
   745  	if subtract {
   746  		sendAmount -= finalFee
   747  	}
   748  	if dexbtc.IsDustVal(minTxSize, sendAmount, feeRate, true) {
   749  		return 0, errors.New("output value is dust")
   750  	}
   751  
   752  	return finalFee, nil
   753  }
   754  
   755  // swapConfirmations attempts to get the number of confirmations and the spend
   756  // status for the specified tx output. For swap outputs that were not generated
   757  // by this wallet, startTime must be supplied to limit the search. Use the match
   758  // time assigned by the server.
   759  func (w *spvWallet) swapConfirmations(txHash *chainhash.Hash, vout uint32, pkScript []byte,
   760  	startTime time.Time) (confs uint32, spent bool, err error) {
   761  
   762  	// First, check if it's a wallet transaction. We probably won't be able
   763  	// to see the spend status, since the wallet doesn't track the swap contract
   764  	// output, but we can get the block if it's been mined.
   765  	blockHash, confs, spent, err := w.confirmations(txHash, vout)
   766  	if err == nil {
   767  		return confs, spent, nil
   768  	}
   769  	var assumedMempool bool
   770  	switch err {
   771  	case WalletTransactionNotFound:
   772  		w.log.Tracef("swapConfirmations - WalletTransactionNotFound: %v:%d", txHash, vout)
   773  	case SpentStatusUnknown:
   774  		w.log.Tracef("swapConfirmations - SpentStatusUnknown: %v:%d (block %v, confs %d)",
   775  			txHash, vout, blockHash, confs)
   776  		if blockHash == nil {
   777  			// We generated this swap, but it probably hasn't been mined yet.
   778  			// It's SpentStatusUnknown because the wallet doesn't track the
   779  			// spend status of the swap contract output itself, since it's not
   780  			// recognized as a wallet output. We'll still try to find the
   781  			// confirmations with other means, but if we can't find it, we'll
   782  			// report it as a zero-conf unspent output. This ignores the remote
   783  			// possibility that the output could be both in mempool and spent.
   784  			assumedMempool = true
   785  		}
   786  	default:
   787  		return 0, false, err
   788  	}
   789  
   790  	// If we still don't have the block hash, we may have it stored. Check the
   791  	// dex database first. This won't give us the confirmations and spent
   792  	// status, but it will allow us to short circuit a longer scan if we already
   793  	// know the output is spent.
   794  	if blockHash == nil {
   795  		blockHash, _ = w.mainchainBlockForStoredTx(txHash)
   796  	}
   797  
   798  	// Our last option is neutrino.
   799  	w.log.Tracef("swapConfirmations - scanFilters: %v:%d (block %v, start time %v)",
   800  		txHash, vout, blockHash, startTime)
   801  	walletBlock := w.wallet.SyncedTo() // where cfilters are received and processed
   802  	walletTip := walletBlock.Height
   803  	utxo, err := w.ScanFilters(txHash, vout, pkScript, walletTip, startTime, blockHash)
   804  	if err != nil {
   805  		return 0, false, err
   806  	}
   807  
   808  	if utxo.spend == nil && utxo.blockHash == nil {
   809  		if assumedMempool {
   810  			w.log.Tracef("swapConfirmations - scanFilters did not find %v:%d, assuming in mempool.",
   811  				txHash, vout)
   812  			// NOT asset.CoinNotFoundError since this is normal for mempool
   813  			// transactions with an SPV wallet.
   814  			return 0, false, nil
   815  		}
   816  		return 0, false, fmt.Errorf("output %s:%v not found with search parameters startTime = %s, pkScript = %x",
   817  			txHash, vout, startTime, pkScript)
   818  	}
   819  
   820  	if utxo.blockHash != nil {
   821  		bestHeight, err := w.getChainHeight()
   822  		if err != nil {
   823  			return 0, false, fmt.Errorf("getBestBlockHeight error: %v", err)
   824  		}
   825  		confs = uint32(bestHeight) - utxo.blockHeight + 1
   826  	}
   827  
   828  	if utxo.spend != nil {
   829  		// In the off-chance that a spend was found but not the output itself,
   830  		// confs will be incorrect here.
   831  		// In situations where we're looking for the counter-party's swap, we
   832  		// revoke if it's found to be spent, without inspecting the confs, so
   833  		// accuracy of confs is not significant. When it's our output, we'll
   834  		// know the block and won't end up here. (even if we did, we just end up
   835  		// sending out some inaccurate Data-severity notifications to the UI
   836  		// until the match progresses)
   837  		return confs, true, nil
   838  	}
   839  
   840  	// unspent
   841  	return confs, false, nil
   842  }
   843  
   844  func (w *spvWallet) locked() bool {
   845  	return w.wallet.Locked()
   846  }
   847  
   848  func (w *spvWallet) walletLock() error {
   849  	w.wallet.Lock()
   850  	return nil
   851  }
   852  
   853  func (w *spvWallet) walletUnlock(pw []byte) error {
   854  	return w.Unlock(pw)
   855  }
   856  
   857  func (w *spvWallet) getBlockHeaderVerbose(blockHash *chainhash.Hash) (*wire.BlockHeader, error) {
   858  	return w.cl.GetBlockHeader(blockHash)
   859  }
   860  
   861  // getBlockHeader gets the *blockHeader for the specified block hash. It also
   862  // returns a bool value to indicate whether this block is a part of main chain.
   863  // For orphaned blocks header.Confirmations is negative.
   864  func (w *spvWallet) getBlockHeader(blockHash *chainhash.Hash) (header *BlockHeader, mainchain bool, err error) {
   865  	hdr, err := w.cl.GetBlockHeader(blockHash)
   866  	if err != nil {
   867  		return nil, false, err
   868  	}
   869  
   870  	tip, err := w.cl.BestBlock()
   871  	if err != nil {
   872  		return nil, false, fmt.Errorf("BestBlock error: %v", err)
   873  	}
   874  
   875  	blockHeight, err := w.cl.GetBlockHeight(blockHash)
   876  	if err != nil {
   877  		return nil, false, err
   878  	}
   879  
   880  	confirmations := int64(-1)
   881  	mainchain = w.blockIsMainchain(blockHash, blockHeight)
   882  	if mainchain {
   883  		confirmations = int64(confirms(blockHeight, tip.Height))
   884  	}
   885  
   886  	return &BlockHeader{
   887  		Hash:              hdr.BlockHash().String(),
   888  		Confirmations:     confirmations,
   889  		Height:            int64(blockHeight),
   890  		Time:              hdr.Timestamp.Unix(),
   891  		PreviousBlockHash: hdr.PrevBlock.String(),
   892  	}, mainchain, nil
   893  }
   894  
   895  func (w *spvWallet) getBestBlockHeader() (*BlockHeader, error) {
   896  	hash, err := w.getBestBlockHash()
   897  	if err != nil {
   898  		return nil, err
   899  	}
   900  	hdr, _, err := w.getBlockHeader(hash)
   901  	return hdr, err
   902  }
   903  
   904  func (w *spvWallet) logFilePath() string {
   905  	return filepath.Join(w.dir, logDirName, logFileName)
   906  }
   907  
   908  // connect will start the wallet and begin syncing.
   909  func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) (err error) {
   910  	w.cl, err = w.wallet.Start()
   911  	if err != nil {
   912  		return err
   913  	}
   914  
   915  	blockNotes := w.wallet.BlockNotifications(ctx)
   916  
   917  	// Nanny for the caches checkpoints and txBlocks caches.
   918  	wg.Add(1)
   919  	go func() {
   920  		defer wg.Done()
   921  		defer w.wallet.Stop()
   922  
   923  		ticker := time.NewTicker(time.Minute * 20)
   924  		defer ticker.Stop()
   925  		expiration := time.Hour * 2
   926  		for {
   927  			select {
   928  			case <-ticker.C:
   929  				w.BlockFiltersScanner.CleanCaches(expiration)
   930  
   931  			case blk := <-blockNotes:
   932  				syncTarget := atomic.LoadInt32(&w.syncTarget)
   933  				if syncTarget == 0 || (blk.Height < syncTarget && blk.Height%10_000 != 0) {
   934  					continue
   935  				}
   936  
   937  				select {
   938  				case w.tipChan <- &BlockVector{
   939  					Hash:   blk.Hash,
   940  					Height: int64(blk.Height),
   941  				}:
   942  				default:
   943  					w.log.Warnf("tip report channel was blocking")
   944  				}
   945  
   946  			case <-ctx.Done():
   947  				return
   948  			}
   949  		}
   950  	}()
   951  
   952  	return nil
   953  }
   954  
   955  // moveWalletData will move all wallet files to a backup directory, but leaving
   956  // the logs folder.
   957  func (w *spvWallet) moveWalletData(backupDir string) error {
   958  	timeString := time.Now().Format("2006-01-02T15_04_05")
   959  	backupFolder := filepath.Join(backupDir, w.chainParams.Name, timeString)
   960  	err := os.MkdirAll(backupFolder, 0744)
   961  	if err != nil {
   962  		return err
   963  	}
   964  
   965  	// Copy wallet logs folder since we do not move it.
   966  	backupLogDir := filepath.Join(backupFolder, logDirName)
   967  	walletLogDir := filepath.Join(w.dir, logDirName)
   968  	if err := copyDir(walletLogDir, backupLogDir); err != nil {
   969  		return err
   970  	}
   971  
   972  	// Move contents of the wallet dir, except the logs folder.
   973  	return filepath.WalkDir(w.dir, func(path string, d fs.DirEntry, err error) error {
   974  		if err != nil {
   975  			return err
   976  		}
   977  		if path == w.dir { // top
   978  			return nil
   979  		}
   980  		if d.IsDir() && d.Name() == logDirName {
   981  			return filepath.SkipDir
   982  		}
   983  		rel, err := filepath.Rel(w.dir, path)
   984  		if err != nil {
   985  			return err
   986  		}
   987  		err = os.Rename(path, filepath.Join(backupFolder, rel))
   988  		if err != nil {
   989  			return err
   990  		}
   991  		if d.IsDir() { // we just moved a folder, including the contents
   992  			return filepath.SkipDir
   993  		}
   994  		return nil
   995  	})
   996  }
   997  
   998  // copyFile copies a file from src to dst.
   999  func copyFile(src, dst string) error {
  1000  	out, err := os.Create(dst)
  1001  	if err != nil {
  1002  		return err
  1003  	}
  1004  	defer out.Close()
  1005  
  1006  	in, err := os.Open(src)
  1007  	if err != nil {
  1008  		return err
  1009  	}
  1010  	defer in.Close()
  1011  
  1012  	_, err = io.Copy(out, in)
  1013  	return err
  1014  }
  1015  
  1016  // copyDir recursively copies the directories and files in source directory to
  1017  // destination directory without preserving the original file permissions. The
  1018  // destination folder must not exist.
  1019  func copyDir(src, dst string) error {
  1020  	entries, err := os.ReadDir(src)
  1021  	if err != nil {
  1022  		return err
  1023  	}
  1024  
  1025  	fi, err := os.Stat(dst)
  1026  	if err != nil {
  1027  		if !errors.Is(err, os.ErrNotExist) {
  1028  			return err
  1029  		}
  1030  		err = os.MkdirAll(dst, 0744)
  1031  		if err != nil {
  1032  			return err
  1033  		}
  1034  	} else if !fi.IsDir() {
  1035  		return fmt.Errorf("%q is not a directory", dst)
  1036  	}
  1037  
  1038  	for _, fd := range entries {
  1039  		fName := fd.Name()
  1040  		srcFile := filepath.Join(src, fName)
  1041  		dstFile := filepath.Join(dst, fName)
  1042  		if fd.IsDir() {
  1043  			err = copyDir(srcFile, dstFile)
  1044  		} else if fd.Type().IsRegular() {
  1045  			err = copyFile(srcFile, dstFile)
  1046  		}
  1047  		if err != nil {
  1048  			return err
  1049  		}
  1050  	}
  1051  
  1052  	return nil
  1053  }
  1054  
  1055  // numDerivedAddresses returns the number of internal and external addresses
  1056  // that the wallet has derived.
  1057  func (w *spvWallet) numDerivedAddresses() (internal, external uint32, err error) {
  1058  	props, err := w.wallet.AccountProperties(waddrmgr.KeyScopeBIP0084, w.acctNum)
  1059  	if err != nil {
  1060  		return 0, 0, err
  1061  	}
  1062  
  1063  	return props.InternalKeyCount, props.ExternalKeyCount, nil
  1064  }
  1065  
  1066  // fingerprint returns an identifier for this wallet. It is the hash of the
  1067  // compressed serialization of the account pub key.
  1068  func (w *spvWallet) fingerprint() (string, error) {
  1069  	props, err := w.wallet.AccountProperties(waddrmgr.KeyScopeBIP0084, w.acctNum)
  1070  	if err != nil {
  1071  		return "", err
  1072  	}
  1073  
  1074  	if props.AccountPubKey == nil {
  1075  		return "", fmt.Errorf("no account key available")
  1076  	}
  1077  
  1078  	pk, err := props.AccountPubKey.ECPubKey()
  1079  	if err != nil {
  1080  		return "", err
  1081  	}
  1082  
  1083  	return hex.EncodeToString(btcutil.Hash160(pk.SerializeCompressed())), nil
  1084  }
  1085  
  1086  // getTxOut finds an unspent transaction output and its number of confirmations.
  1087  // To match the behavior of the RPC method, even if an output is found, if it's
  1088  // known to be spent, no *wire.TxOut and no error will be returned.
  1089  func (w *spvWallet) getTxOut(txHash *chainhash.Hash, vout uint32, pkScript []byte, startTime time.Time) (*wire.TxOut, uint32, error) {
  1090  	// Check for a wallet transaction first
  1091  	txDetails, err := w.wallet.WalletTransaction(txHash)
  1092  	var blockHash *chainhash.Hash
  1093  	if err != nil && !errors.Is(err, WalletTransactionNotFound) {
  1094  		return nil, 0, fmt.Errorf("walletTransaction error: %w", err)
  1095  	}
  1096  
  1097  	if txDetails != nil {
  1098  		spent, found := outputSpendStatus(txDetails, vout)
  1099  		if found {
  1100  			if spent {
  1101  				return nil, 0, nil
  1102  			}
  1103  			if len(txDetails.MsgTx.TxOut) <= int(vout) {
  1104  				return nil, 0, fmt.Errorf("wallet transaction %s doesn't have enough outputs for vout %d", txHash, vout)
  1105  			}
  1106  
  1107  			var confs uint32
  1108  			if txDetails.Block.Height > 0 {
  1109  				tip, err := w.cl.BestBlock()
  1110  				if err != nil {
  1111  					return nil, 0, fmt.Errorf("BestBlock error: %v", err)
  1112  				}
  1113  				confs = uint32(confirms(txDetails.Block.Height, tip.Height))
  1114  			}
  1115  
  1116  			msgTx := &txDetails.MsgTx
  1117  			if len(msgTx.TxOut) <= int(vout) {
  1118  				return nil, 0, fmt.Errorf("wallet transaction %s found, but not enough outputs for vout %d", txHash, vout)
  1119  			}
  1120  			return msgTx.TxOut[vout], confs, nil
  1121  		}
  1122  		if txDetails.Block.Hash != (chainhash.Hash{}) {
  1123  			blockHash = &txDetails.Block.Hash
  1124  		}
  1125  	}
  1126  
  1127  	// We don't really know if it's spent, so we'll need to scan.
  1128  	walletBlock := w.wallet.SyncedTo() // where cfilters are received and processed
  1129  	walletTip := walletBlock.Height
  1130  	utxo, err := w.ScanFilters(txHash, vout, pkScript, walletTip, startTime, blockHash)
  1131  	if err != nil {
  1132  		return nil, 0, err
  1133  	}
  1134  
  1135  	if utxo == nil || utxo.spend != nil || utxo.blockHash == nil {
  1136  		return nil, 0, nil
  1137  	}
  1138  
  1139  	tip, err := w.cl.BestBlock()
  1140  	if err != nil {
  1141  		return nil, 0, fmt.Errorf("BestBlock error: %v", err)
  1142  	}
  1143  
  1144  	confs := uint32(confirms(int32(utxo.blockHeight), tip.Height))
  1145  
  1146  	return utxo.txOut, confs, nil
  1147  }
  1148  
  1149  // matchPkScript pulls the filter for the block and attempts to match the
  1150  // supplied scripts.
  1151  func (w *spvWallet) matchPkScript(blockHash *chainhash.Hash, scripts [][]byte) (bool, error) {
  1152  	filter, err := w.cl.GetCFilter(*blockHash, wire.GCSFilterRegular)
  1153  	if err != nil {
  1154  		return false, fmt.Errorf("GetCFilter error: %w", err)
  1155  	}
  1156  
  1157  	if filter.N() == 0 {
  1158  		return false, fmt.Errorf("unexpected empty filter for %s", blockHash)
  1159  	}
  1160  
  1161  	var filterKey [gcs.KeySize]byte
  1162  	copy(filterKey[:], blockHash[:gcs.KeySize])
  1163  
  1164  	matchFound, err := filter.MatchAny(filterKey, scripts)
  1165  	if err != nil {
  1166  		return false, fmt.Errorf("MatchAny error: %w", err)
  1167  	}
  1168  	return matchFound, nil
  1169  }
  1170  
  1171  // searchBlockForRedemptions attempts to find spending info for the specified
  1172  // contracts by searching every input of all txs in the provided block range.
  1173  func (w *spvWallet) searchBlockForRedemptions(ctx context.Context, reqs map[OutPoint]*FindRedemptionReq,
  1174  	blockHash chainhash.Hash) (discovered map[OutPoint]*FindRedemptionResult) {
  1175  
  1176  	// Just match all the scripts together.
  1177  	scripts := make([][]byte, 0, len(reqs))
  1178  	for _, req := range reqs {
  1179  		scripts = append(scripts, req.pkScript)
  1180  	}
  1181  
  1182  	discovered = make(map[OutPoint]*FindRedemptionResult, len(reqs))
  1183  
  1184  	matchFound, err := w.matchPkScript(&blockHash, scripts)
  1185  	if err != nil {
  1186  		w.log.Errorf("matchPkScript error: %v", err)
  1187  		return
  1188  	}
  1189  
  1190  	if !matchFound {
  1191  		return
  1192  	}
  1193  
  1194  	// There is at least one match. Pull the block.
  1195  	block, err := w.cl.GetBlock(blockHash)
  1196  	if err != nil {
  1197  		w.log.Errorf("neutrino GetBlock error: %v", err)
  1198  		return
  1199  	}
  1200  
  1201  	for _, msgTx := range block.MsgBlock().Transactions {
  1202  		newlyDiscovered := findRedemptionsInTxWithHasher(ctx, true, reqs, msgTx, w.chainParams, hashTx)
  1203  		for outPt, res := range newlyDiscovered {
  1204  			discovered[outPt] = res
  1205  		}
  1206  	}
  1207  	return
  1208  }
  1209  
  1210  // findRedemptionsInMempool is unsupported for SPV.
  1211  func (w *spvWallet) findRedemptionsInMempool(ctx context.Context, reqs map[OutPoint]*FindRedemptionReq) (discovered map[OutPoint]*FindRedemptionResult) {
  1212  	return
  1213  }
  1214  
  1215  // confirmations looks for the confirmation count and spend status on a
  1216  // transaction output that pays to this wallet.
  1217  func (w *spvWallet) confirmations(txHash *chainhash.Hash, vout uint32) (blockHash *chainhash.Hash, confs uint32, spent bool, err error) {
  1218  	details, err := w.wallet.WalletTransaction(txHash)
  1219  	if err != nil {
  1220  		return nil, 0, false, err
  1221  	}
  1222  
  1223  	if details.Block.Hash != (chainhash.Hash{}) {
  1224  		blockHash = &details.Block.Hash
  1225  		height, err := w.getChainHeight()
  1226  		if err != nil {
  1227  			return nil, 0, false, err
  1228  		}
  1229  		confs = uint32(confirms(details.Block.Height, height))
  1230  	}
  1231  
  1232  	spent, found := outputSpendStatus(details, vout)
  1233  	if found {
  1234  		return blockHash, confs, spent, nil
  1235  	}
  1236  
  1237  	return blockHash, confs, false, SpentStatusUnknown
  1238  }
  1239  
  1240  // getWalletTransaction checks the wallet database for the specified
  1241  // transaction. Only transactions with output scripts that pay to the wallet or
  1242  // transactions that spend wallet outputs are stored in the wallet database.
  1243  // This is pretty much copy-paste from btcwallet 'gettransaction' JSON-RPC
  1244  // handler.
  1245  func (w *spvWallet) getWalletTransaction(txHash *chainhash.Hash) (*GetTransactionResult, error) {
  1246  	// Option # 1 just copies from UnstableAPI.TxDetails. Duplicating the
  1247  	// unexported bucket key feels dirty.
  1248  	//
  1249  	// var details *wtxmgr.TxDetails
  1250  	// err := walletdb.View(w.Database(), func(dbtx walletdb.ReadTx) error {
  1251  	// 	txKey := []byte("wtxmgr")
  1252  	// 	txmgrNs := dbtx.ReadBucket(txKey)
  1253  	// 	var err error
  1254  	// 	details, err = w.TxStore.TxDetails(txmgrNs, txHash)
  1255  	// 	return err
  1256  	// })
  1257  
  1258  	// Option #2
  1259  	// This is what the JSON-RPC does (and has since at least May 2018).
  1260  	details, err := w.wallet.WalletTransaction(txHash)
  1261  	if err != nil {
  1262  		if errors.Is(err, WalletTransactionNotFound) {
  1263  			return nil, asset.CoinNotFoundError // for the asset.Wallet interface
  1264  		}
  1265  		return nil, err
  1266  	}
  1267  
  1268  	syncBlock := w.wallet.SyncedTo()
  1269  
  1270  	// TODO: The serialized transaction is already in the DB, so reserializing
  1271  	// might be avoided here. According to btcwallet, details.SerializedTx is
  1272  	// "optional" (?), but we might check for it.
  1273  	txRaw, err := serializeMsgTx(&details.MsgTx)
  1274  	if err != nil {
  1275  		return nil, err
  1276  	}
  1277  
  1278  	ret := &GetTransactionResult{
  1279  		TxID:         txHash.String(),
  1280  		Bytes:        txRaw, // 'Hex' field name is a lie, kinda
  1281  		Time:         uint64(details.Received.Unix()),
  1282  		TimeReceived: uint64(details.Received.Unix()),
  1283  	}
  1284  
  1285  	if details.Block.Height >= 0 {
  1286  		ret.BlockHash = details.Block.Hash.String()
  1287  		ret.BlockTime = uint64(details.Block.Time.Unix())
  1288  		// ret.BlockHeight = uint64(details.Block.Height)
  1289  		ret.Confirmations = uint64(confirms(details.Block.Height, syncBlock.Height))
  1290  	}
  1291  
  1292  	return ret, nil
  1293  
  1294  	/*
  1295  		var debitTotal, creditTotal btcutil.Amount // credits excludes change
  1296  		for _, deb := range details.Debits {
  1297  			debitTotal += deb.Amount
  1298  		}
  1299  		for _, cred := range details.Credits {
  1300  			if !cred.Change {
  1301  				creditTotal += cred.Amount
  1302  			}
  1303  		}
  1304  
  1305  		// Fee can only be determined if every input is a debit.
  1306  		if len(details.Debits) == len(details.MsgTx.TxIn) {
  1307  			var outputTotal btcutil.Amount
  1308  			for _, output := range details.MsgTx.TxOut {
  1309  				outputTotal += btcutil.Amount(output.Value)
  1310  			}
  1311  			ret.Fee = (debitTotal - outputTotal).ToBTC()
  1312  		}
  1313  
  1314  		ret.Amount = creditTotal.ToBTC()
  1315  		return ret, nil
  1316  	*/
  1317  }
  1318  
  1319  func confirms(txHeight, curHeight int32) int32 {
  1320  	switch {
  1321  	case txHeight == -1, txHeight > curHeight:
  1322  		return 0
  1323  	default:
  1324  		return curHeight - txHeight + 1
  1325  	}
  1326  }
  1327  
  1328  // outputSpendStatus will return the spend status of the output if it's found
  1329  // in the TxDetails.Credits.
  1330  func outputSpendStatus(details *wtxmgr.TxDetails, vout uint32) (spend, found bool) {
  1331  	for _, credit := range details.Credits {
  1332  		if credit.Index == vout {
  1333  			return credit.Spent, true
  1334  		}
  1335  	}
  1336  	return false, false
  1337  }