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