decred.org/dcrdex@v1.0.3/client/asset/dcr/spv.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package dcr
     5  
     6  import (
     7  	"context"
     8  	"encoding/base64"
     9  	"encoding/hex"
    10  	"errors"
    11  	"fmt"
    12  	"math"
    13  	"net"
    14  	"os"
    15  	"path/filepath"
    16  	"sort"
    17  	"sync"
    18  	"sync/atomic"
    19  	"time"
    20  
    21  	"decred.org/dcrdex/client/asset"
    22  	"decred.org/dcrdex/dex"
    23  	"decred.org/dcrdex/dex/utils"
    24  	"decred.org/dcrwallet/v4/chain"
    25  	walleterrors "decred.org/dcrwallet/v4/errors"
    26  	"decred.org/dcrwallet/v4/p2p"
    27  	walletjson "decred.org/dcrwallet/v4/rpc/jsonrpc/types"
    28  	"decred.org/dcrwallet/v4/spv"
    29  	vspclient "decred.org/dcrwallet/v4/vsp"
    30  	"decred.org/dcrwallet/v4/wallet"
    31  	"decred.org/dcrwallet/v4/wallet/udb"
    32  	"github.com/decred/dcrd/addrmgr/v2"
    33  	"github.com/decred/dcrd/blockchain/stake/v5"
    34  	"github.com/decred/dcrd/chaincfg/chainhash"
    35  	"github.com/decred/dcrd/chaincfg/v3"
    36  	"github.com/decred/dcrd/connmgr/v3"
    37  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    38  	"github.com/decred/dcrd/dcrutil/v4"
    39  	"github.com/decred/dcrd/gcs/v4"
    40  	chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
    41  	"github.com/decred/dcrd/txscript/v4"
    42  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    43  	"github.com/decred/dcrd/wire"
    44  	"github.com/decred/slog"
    45  	"github.com/jrick/logrotate/rotator"
    46  )
    47  
    48  const (
    49  	defaultAllowHighFees   = false
    50  	defaultRelayFeePerKb   = 1e4
    51  	defaultAccountGapLimit = 3
    52  	defaultManualTickets   = false
    53  	defaultMixSplitLimit   = 10
    54  
    55  	defaultAcct        = 0
    56  	defaultAccountName = "default"
    57  	walletDbName       = "wallet.db"
    58  	dbDriver           = "bdb"
    59  	logDirName         = "spvlogs"
    60  	logFileName        = "neutrino.log"
    61  )
    62  
    63  type dcrWallet interface {
    64  	KnownAddress(ctx context.Context, a stdaddr.Address) (wallet.KnownAddress, error)
    65  	AccountNumber(ctx context.Context, accountName string) (uint32, error)
    66  	AddressAtIdx(ctx context.Context, account, branch, childIdx uint32) (stdaddr.Address, error)
    67  	AccountBalance(ctx context.Context, account uint32, confirms int32) (wallet.Balances, error)
    68  	LockedOutpoints(ctx context.Context, accountName string) ([]chainjson.TransactionInput, error)
    69  	ListUnspent(ctx context.Context, minconf, maxconf int32, addresses map[string]struct{}, accountName string) ([]*walletjson.ListUnspentResult, error)
    70  	LockOutpoint(txHash *chainhash.Hash, index uint32)
    71  	ListTransactionDetails(ctx context.Context, txHash *chainhash.Hash) ([]walletjson.ListTransactionsResult, error)
    72  	MixAccount(context.Context, uint32, uint32, uint32) error
    73  	MainChainTip(ctx context.Context) (hash chainhash.Hash, height int32)
    74  	NewExternalAddress(ctx context.Context, account uint32, callOpts ...wallet.NextAddressCallOption) (stdaddr.Address, error)
    75  	NewInternalAddress(ctx context.Context, account uint32, callOpts ...wallet.NextAddressCallOption) (stdaddr.Address, error)
    76  	PublishTransaction(ctx context.Context, tx *wire.MsgTx, n wallet.NetworkBackend) (*chainhash.Hash, error)
    77  	BlockHeader(ctx context.Context, blockHash *chainhash.Hash) (*wire.BlockHeader, error)
    78  	BlockInMainChain(ctx context.Context, hash *chainhash.Hash) (haveBlock, invalidated bool, err error)
    79  	CFilterV2(ctx context.Context, blockHash *chainhash.Hash) ([gcs.KeySize]byte, *gcs.FilterV2, error)
    80  	BlockInfo(ctx context.Context, blockID *wallet.BlockIdentifier) (*wallet.BlockInfo, error)
    81  	AccountUnlocked(ctx context.Context, account uint32) (bool, error)
    82  	LockAccount(ctx context.Context, account uint32) error
    83  	UnlockAccount(ctx context.Context, account uint32, passphrase []byte) error
    84  	LoadPrivateKey(ctx context.Context, addr stdaddr.Address) (key *secp256k1.PrivateKey, zero func(), err error)
    85  	TxDetails(ctx context.Context, txHash *chainhash.Hash) (*udb.TxDetails, error)
    86  	StakeInfo(ctx context.Context) (*wallet.StakeInfoData, error)
    87  	PurchaseTickets(ctx context.Context, n wallet.NetworkBackend, req *wallet.PurchaseTicketsRequest) (*wallet.PurchaseTicketsResponse, error)
    88  	ForUnspentUnexpiredTickets(ctx context.Context, f func(hash *chainhash.Hash) error) error
    89  	GetTickets(ctx context.Context, f func([]*wallet.TicketSummary, *wire.BlockHeader) (bool, error), startBlock, endBlock *wallet.BlockIdentifier) error
    90  	TreasuryKeyPolicies() []wallet.TreasuryKeyPolicy
    91  	GetAllTSpends(ctx context.Context) []*wire.MsgTx
    92  	TSpendPolicy(tspendHash, ticketHash *chainhash.Hash) stake.TreasuryVoteT
    93  	VSPHostForTicket(ctx context.Context, ticketHash *chainhash.Hash) (string, error)
    94  	SetAgendaChoices(ctx context.Context, ticketHash *chainhash.Hash, choices map[string]string) (voteBits uint16, err error)
    95  	SetTSpendPolicy(ctx context.Context, tspendHash *chainhash.Hash, policy stake.TreasuryVoteT, ticketHash *chainhash.Hash) error
    96  	SetTreasuryKeyPolicy(ctx context.Context, pikey []byte, policy stake.TreasuryVoteT, ticketHash *chainhash.Hash) error
    97  	SetRelayFee(relayFee dcrutil.Amount)
    98  	GetTicketInfo(ctx context.Context, hash *chainhash.Hash) (*wallet.TicketSummary, *wire.BlockHeader, error)
    99  	GetTransactions(ctx context.Context, f func(*wallet.Block) (bool, error), startBlock, endBlock *wallet.BlockIdentifier) error
   100  	ListSinceBlock(ctx context.Context, start, end, syncHeight int32) ([]walletjson.ListTransactionsResult, error)
   101  	UnlockOutpoint(txHash *chainhash.Hash, index uint32)
   102  	SignTransaction(ctx context.Context, tx *wire.MsgTx, hashType txscript.SigHashType, additionalPrevScripts map[wire.OutPoint][]byte,
   103  		additionalKeysByAddress map[string]*dcrutil.WIF, p2shRedeemScriptsByAddress map[string][]byte) ([]wallet.SignatureError, error)
   104  	AgendaChoices(ctx context.Context, ticketHash *chainhash.Hash) (choices map[string]string, voteBits uint16, err error)
   105  	NewVSPTicket(ctx context.Context, hash *chainhash.Hash) (*wallet.VSPTicket, error)
   106  	RescanProgressFromHeight(ctx context.Context, n wallet.NetworkBackend, startHeight int32, p chan<- wallet.RescanProgress)
   107  	RescanPoint(ctx context.Context) (*chainhash.Hash, error)
   108  }
   109  
   110  // Interface for *spv.Syncer so that we can test with a stub.
   111  type spvSyncer interface {
   112  	wallet.NetworkBackend
   113  	Synced(context.Context) (bool, int32)
   114  	GetRemotePeers() map[string]*p2p.RemotePeer
   115  }
   116  
   117  // cachedBlock is a cached MsgBlock with a last-access time. The cleanBlockCache
   118  // loop is started in Connect to periodically discard cachedBlocks that are too
   119  // old.
   120  type cachedBlock struct {
   121  	*wire.MsgBlock
   122  	lastAccess time.Time
   123  }
   124  
   125  type blockCache struct {
   126  	sync.Mutex
   127  	blocks map[chainhash.Hash]*cachedBlock // block hash -> block
   128  }
   129  
   130  // extendedWallet adds the TxDetails method to *wallet.Wallet.
   131  type extendedWallet struct {
   132  	*wallet.Wallet
   133  }
   134  
   135  // TxDetails exposes the (UnstableApi).TxDetails method.
   136  func (w *extendedWallet) TxDetails(ctx context.Context, txHash *chainhash.Hash) (*udb.TxDetails, error) {
   137  	return wallet.UnstableAPI(w.Wallet).TxDetails(ctx, txHash)
   138  }
   139  
   140  // MainTipChangedNotifications returns a channel for receiving main tip change
   141  // notifications, along with a function to close the channel when it is no
   142  // longer needed.
   143  func (w *extendedWallet) MainTipChangedNotifications() (chan *wallet.MainTipChangedNotification, func()) {
   144  	ntfn := w.NtfnServer.MainTipChangedNotifications()
   145  	return ntfn.C, ntfn.Done
   146  }
   147  
   148  // spvWallet is a Wallet built on dcrwallet's *wallet.Wallet running in SPV
   149  // mode.
   150  type spvWallet struct {
   151  	dcrWallet         // *extendedWallet
   152  	db                wallet.DB
   153  	dir               string
   154  	chainParams       *chaincfg.Params
   155  	log               dex.Logger
   156  	spv               spvSyncer // *spv.Syncer
   157  	bestSpvPeerHeight int32     // atomic
   158  	tipChan           chan *block
   159  	gapLimit          uint32
   160  
   161  	blockCache blockCache
   162  
   163  	accts atomic.Value
   164  
   165  	cancel context.CancelFunc
   166  	wg     sync.WaitGroup
   167  }
   168  
   169  var _ Wallet = (*spvWallet)(nil)
   170  var _ tipNotifier = (*spvWallet)(nil)
   171  
   172  func createSPVWallet(pw, seed []byte, dataDir string, extIdx, intIdx, gapLimit uint32, chainParams *chaincfg.Params) error {
   173  	netDir := filepath.Join(dataDir, chainParams.Name)
   174  	walletDir := filepath.Join(netDir, "spv")
   175  
   176  	if err := initLogging(netDir); err != nil {
   177  		return fmt.Errorf("error initializing dcrwallet logging: %w", err)
   178  	}
   179  
   180  	if exists, err := walletExists(walletDir); err != nil {
   181  		return err
   182  	} else if exists {
   183  		return fmt.Errorf("wallet at %q already exists", walletDir)
   184  	}
   185  
   186  	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
   187  	defer cancel()
   188  
   189  	dbPath := filepath.Join(walletDir, walletDbName)
   190  	exists, err := fileExists(dbPath)
   191  	if err != nil {
   192  		return fmt.Errorf("error checking file existence for %q: %w", dbPath, err)
   193  	}
   194  	if exists {
   195  		return fmt.Errorf("database file already exists at %q", dbPath)
   196  	}
   197  
   198  	// Ensure the data directory for the network exists.
   199  	if err := checkCreateDir(walletDir); err != nil {
   200  		return fmt.Errorf("checkCreateDir error: %w", err)
   201  	}
   202  
   203  	// At this point it is asserted that there is no existing database file, and
   204  	// deleting anything won't destroy a wallet in use.  Defer a function that
   205  	// attempts to remove any wallet remnants.
   206  	defer func() {
   207  		if err != nil {
   208  			_ = os.Remove(walletDir)
   209  		}
   210  	}()
   211  
   212  	// Create the wallet database backed by bolt db.
   213  	db, err := wallet.CreateDB(dbDriver, dbPath)
   214  	if err != nil {
   215  		return fmt.Errorf("CreateDB error: %w", err)
   216  	}
   217  
   218  	// Initialize the newly created database for the wallet before opening.
   219  	err = wallet.Create(ctx, db, nil, pw, seed, chainParams)
   220  	if err != nil {
   221  		return fmt.Errorf("wallet.Create error: %w", err)
   222  	}
   223  
   224  	// Open the newly-created wallet.
   225  	w, err := wallet.Open(ctx, newWalletConfig(db, chainParams, gapLimit))
   226  	if err != nil {
   227  		return fmt.Errorf("wallet.Open error: %w", err)
   228  	}
   229  
   230  	defer func() {
   231  		if err := db.Close(); err != nil {
   232  			fmt.Println("Error closing database:", err)
   233  		}
   234  	}()
   235  
   236  	err = w.UpgradeToSLIP0044CoinType(ctx)
   237  	if err != nil {
   238  		return err
   239  	}
   240  
   241  	err = w.Unlock(ctx, pw, nil)
   242  	if err != nil {
   243  		return fmt.Errorf("error unlocking wallet: %w", err)
   244  	}
   245  
   246  	err = w.SetAccountPassphrase(ctx, defaultAcct, pw)
   247  	if err != nil {
   248  		return fmt.Errorf("error setting Decred account %d passphrase: %v", defaultAcct, err)
   249  	}
   250  
   251  	err = setupMixingAccounts(ctx, w, pw)
   252  	if err != nil {
   253  		return fmt.Errorf("error setting up mixing accounts: %v", err)
   254  	}
   255  
   256  	w.Lock()
   257  
   258  	if extIdx > 0 || intIdx > 0 {
   259  		err = extendAddresses(ctx, extIdx, intIdx, w)
   260  		if err != nil {
   261  			return fmt.Errorf("failed to set starting address indexes: %w", err)
   262  		}
   263  	}
   264  
   265  	return nil
   266  }
   267  
   268  // If we're running on simnet, add some tspends and treasury keys.
   269  func (w *spvWallet) initializeSimnetTspends(ctx context.Context) {
   270  	if w.chainParams.Net != wire.SimNet {
   271  		return
   272  	}
   273  	tspendWallet, is := w.dcrWallet.(interface {
   274  		AddTSpend(tx wire.MsgTx) error
   275  		GetAllTSpends(ctx context.Context) []*wire.MsgTx
   276  		SetTreasuryKeyPolicy(ctx context.Context, pikey []byte, policy stake.TreasuryVoteT, ticketHash *chainhash.Hash) error
   277  		TreasuryKeyPolicies() []wallet.TreasuryKeyPolicy
   278  	})
   279  	if !is {
   280  		return
   281  	}
   282  	const numFakeTspends = 3
   283  	if len(tspendWallet.GetAllTSpends(ctx)) >= numFakeTspends {
   284  		return
   285  	}
   286  	expiryBase := uint32(time.Now().Add(time.Hour * 24 * 365).Unix())
   287  	for i := uint32(0); i < numFakeTspends; i++ {
   288  		var signatureScript [100]byte
   289  		tx := &wire.MsgTx{
   290  			Expiry: expiryBase + i,
   291  			TxIn:   []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, signatureScript[:])},
   292  			TxOut:  []*wire.TxOut{{Value: int64(i+1) * 1e8}},
   293  		}
   294  		if err := tspendWallet.AddTSpend(*tx); err != nil {
   295  			w.log.Errorf("Error adding simnet tspend: %v", err)
   296  		}
   297  	}
   298  	if len(tspendWallet.TreasuryKeyPolicies()) == 0 {
   299  		priv, _ := secp256k1.GeneratePrivateKey()
   300  		tspendWallet.SetTreasuryKeyPolicy(ctx, priv.PubKey().SerializeCompressed(), 0x01 /* yes */, nil)
   301  	}
   302  }
   303  
   304  // setupMixingAccounts checks if the mixed, unmixed and trading accounts
   305  // required to use this wallet for funds mixing exists and creates any of the
   306  // accounts that does not yet exist. The wallet should be unlocked before
   307  // calling this function.
   308  func setupMixingAccounts(ctx context.Context, w *wallet.Wallet, pw []byte) error {
   309  	requiredAccts := []string{mixedAccountName, tradingAccountName} // unmixed (default) acct already exists
   310  	for _, acct := range requiredAccts {
   311  		_, err := w.AccountNumber(ctx, acct)
   312  		if err == nil {
   313  			continue // account exist, check next account
   314  		}
   315  
   316  		if !errors.Is(err, walleterrors.NotExist) {
   317  			return err
   318  		}
   319  
   320  		acctNum, err := w.NextAccount(ctx, acct)
   321  		if err != nil {
   322  			return err
   323  		}
   324  		if err = w.SetAccountPassphrase(ctx, acctNum, pw); err != nil {
   325  			return err
   326  		}
   327  	}
   328  
   329  	return nil
   330  }
   331  
   332  func (w *spvWallet) setAccounts(mixingEnabled bool) {
   333  	if mixingEnabled {
   334  		w.accts.Store(XCWalletAccounts{
   335  			PrimaryAccount: mixedAccountName,
   336  			UnmixedAccount: defaultAccountName,
   337  			TradingAccount: tradingAccountName,
   338  		})
   339  		return
   340  	}
   341  	w.accts.Store(XCWalletAccounts{
   342  		PrimaryAccount: defaultAccountName,
   343  	})
   344  }
   345  
   346  // Accounts returns the names of the accounts for use by the exchange wallet.
   347  func (w *spvWallet) Accounts() XCWalletAccounts {
   348  	return w.accts.Load().(XCWalletAccounts)
   349  }
   350  
   351  func (w *spvWallet) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, net dex.Network, currentAddress string) (restart bool, err error) {
   352  	return cfg.Type != walletTypeSPV, nil
   353  }
   354  
   355  // InitialAddress returns the branch 0, child 0 address of the default
   356  // account.
   357  func (w *spvWallet) InitialAddress(ctx context.Context) (string, error) {
   358  	acctNum, err := w.dcrWallet.AccountNumber(ctx, defaultAccountName)
   359  	if err != nil {
   360  		return "", err
   361  	}
   362  
   363  	addr, err := w.dcrWallet.AddressAtIdx(ctx, acctNum, 0, 0)
   364  	if err != nil {
   365  		return "", err
   366  	}
   367  
   368  	return addr.String(), nil
   369  }
   370  
   371  func (w *spvWallet) startWallet(ctx context.Context) error {
   372  	netDir := filepath.Dir(w.dir)
   373  	if err := initLogging(netDir); err != nil {
   374  		return fmt.Errorf("error initializing dcrwallet logging: %w", err)
   375  	}
   376  
   377  	db, err := wallet.OpenDB(dbDriver, filepath.Join(w.dir, walletDbName))
   378  	if err != nil {
   379  		return fmt.Errorf("wallet.OpenDB error: %w", err)
   380  	}
   381  
   382  	dcrw, err := wallet.Open(ctx, newWalletConfig(db, w.chainParams, w.gapLimit))
   383  	if err != nil {
   384  		// If this function does not return to completion the database must be
   385  		// closed.  Otherwise, because the database is locked on open, any
   386  		// other attempts to open the wallet will hang, and there is no way to
   387  		// recover since this db handle would be leaked.
   388  		if err := db.Close(); err != nil {
   389  			w.log.Errorf("Uh oh. Failed to close the database: %v", err)
   390  		}
   391  		return fmt.Errorf("wallet.Open error: %w", err)
   392  	}
   393  	w.dcrWallet = &extendedWallet{dcrw}
   394  	w.db = db
   395  
   396  	var connectPeers []string
   397  	switch w.chainParams.Net {
   398  	case wire.SimNet:
   399  		connectPeers = []string{"localhost:19560"}
   400  	}
   401  
   402  	spv := newSpvSyncer(dcrw, w.dir, connectPeers)
   403  	w.spv = spv
   404  
   405  	w.wg.Add(2)
   406  	go func() {
   407  		defer w.wg.Done()
   408  		w.spvLoop(ctx, spv)
   409  	}()
   410  	go func() {
   411  		defer w.wg.Done()
   412  		w.notesLoop(ctx, dcrw)
   413  	}()
   414  
   415  	w.initializeSimnetTspends(ctx)
   416  
   417  	return nil
   418  }
   419  
   420  // stop stops the wallet and database threads.
   421  func (w *spvWallet) stop() {
   422  	w.log.Info("Unloading wallet")
   423  	if err := w.db.Close(); err != nil {
   424  		w.log.Info("Error closing database: %v", err)
   425  	}
   426  
   427  	w.log.Info("SPV wallet closed")
   428  }
   429  
   430  func (w *spvWallet) spvLoop(ctx context.Context, syncer *spv.Syncer) {
   431  	for {
   432  		err := syncer.Run(ctx)
   433  		if ctx.Err() != nil {
   434  			return
   435  		}
   436  		w.log.Errorf("SPV synchronization ended. trying again in 10 seconds: %v", err)
   437  		select {
   438  		case <-ctx.Done():
   439  			return
   440  		case <-time.After(time.Second * 10):
   441  		}
   442  	}
   443  }
   444  
   445  func (w *spvWallet) notesLoop(ctx context.Context, dcrw *wallet.Wallet) {
   446  	txNotes := dcrw.NtfnServer.TransactionNotifications()
   447  	defer txNotes.Done()
   448  	// removeTxNotes := dcrw.NtfnServer.RemovedTransactionNotifications()
   449  	// defer removeTxNotes.Done()
   450  	// acctNotes := dcrw.NtfnServer.AccountNotifications()
   451  	// defer acctNotes.Done()
   452  	// tipNotes := dcrw.NtfnServer.MainTipChangedNotifications()
   453  	// defer tipNotes.Done()
   454  	// confirmNotes := w.NtfnServer.ConfirmationNotifications(ctx)
   455  
   456  	for {
   457  		select {
   458  		case n := <-txNotes.C:
   459  			if len(n.AttachedBlocks) == 0 {
   460  				if len(n.UnminedTransactions) > 0 {
   461  					select {
   462  					case w.tipChan <- nil:
   463  					default:
   464  						w.log.Warnf("tx report channel was blocking")
   465  					}
   466  				}
   467  				continue
   468  			}
   469  			lastBlock := n.AttachedBlocks[len(n.AttachedBlocks)-1]
   470  			h := lastBlock.Header.BlockHash()
   471  			select {
   472  			case w.tipChan <- &block{
   473  				hash:   &h,
   474  				height: int64(lastBlock.Header.Height),
   475  			}:
   476  			default:
   477  				w.log.Warnf("tip report channel was blocking")
   478  			}
   479  		case <-ctx.Done():
   480  			return
   481  		}
   482  	}
   483  }
   484  
   485  func (w *spvWallet) tipFeed() <-chan *block {
   486  	return w.tipChan
   487  }
   488  
   489  // Connect starts the wallet and begins synchronization.
   490  func (w *spvWallet) Connect(ctx context.Context) error {
   491  	ctx, w.cancel = context.WithCancel(ctx)
   492  	err := w.startWallet(ctx)
   493  	if err != nil {
   494  		return err
   495  	}
   496  
   497  	w.wg.Add(1)
   498  	go func() {
   499  		defer w.wg.Done()
   500  		defer w.stop()
   501  
   502  		ticker := time.NewTicker(time.Minute * 20)
   503  
   504  		for {
   505  			select {
   506  			case <-ticker.C:
   507  				w.cleanBlockCache()
   508  			case <-ctx.Done():
   509  				return
   510  			}
   511  		}
   512  	}()
   513  
   514  	return nil
   515  }
   516  
   517  // Disconnect shuts down the wallet and waits for monitored threads to exit.
   518  // Part of the Wallet interface.
   519  func (w *spvWallet) Disconnect() {
   520  	w.cancel()
   521  	w.wg.Wait()
   522  }
   523  
   524  // SpvMode is always true for spvWallet.
   525  // Part of the Wallet interface.
   526  func (w *spvWallet) SpvMode() bool {
   527  	return true
   528  }
   529  
   530  // AddressInfo returns information for the provided address. It is an error if
   531  // the address is not owned by the wallet.
   532  func (w *spvWallet) AddressInfo(ctx context.Context, addrStr string) (*AddressInfo, error) {
   533  	addr, err := stdaddr.DecodeAddress(addrStr, w.chainParams)
   534  	if err != nil {
   535  		return nil, err
   536  	}
   537  	ka, err := w.KnownAddress(ctx, addr)
   538  	if err != nil {
   539  		return nil, err
   540  	}
   541  
   542  	if ka, ok := ka.(wallet.BIP0044Address); ok {
   543  		_, branch, _ := ka.Path()
   544  		return &AddressInfo{Account: ka.AccountName(), Branch: branch}, nil
   545  	}
   546  	return nil, fmt.Errorf("unsupported address type %T", ka)
   547  }
   548  
   549  // WalletOwnsAddress returns whether any of the account controlled by this
   550  // wallet owns the specified address.
   551  func (w *spvWallet) WalletOwnsAddress(ctx context.Context, addr stdaddr.Address) (bool, error) {
   552  	ka, err := w.KnownAddress(ctx, addr)
   553  	if err != nil {
   554  		if errors.Is(err, walleterrors.NotExist) {
   555  			return false, nil
   556  		}
   557  		return false, fmt.Errorf("KnownAddress error: %w", err)
   558  	}
   559  	if kind := ka.AccountKind(); kind != wallet.AccountKindBIP0044 && kind != wallet.AccountKindImported {
   560  		return false, nil
   561  	}
   562  
   563  	return true, nil
   564  }
   565  
   566  // AccountOwnsAddress checks if the provided address belongs to the specified
   567  // account.
   568  // Part of the Wallet interface.
   569  func (w *spvWallet) AccountOwnsAddress(ctx context.Context, addr stdaddr.Address, account string) (bool, error) {
   570  	ka, err := w.KnownAddress(ctx, addr)
   571  	if err != nil {
   572  		if errors.Is(err, walleterrors.NotExist) {
   573  			return false, nil
   574  		}
   575  		return false, fmt.Errorf("KnownAddress error: %w", err)
   576  	}
   577  	if ka.AccountName() != account {
   578  		return false, nil
   579  	}
   580  	if kind := ka.AccountKind(); kind != wallet.AccountKindBIP0044 && kind != wallet.AccountKindImported {
   581  		return false, nil
   582  	}
   583  	return true, nil
   584  }
   585  
   586  // AccountBalance returns the balance breakdown for the specified account.
   587  // Part of the Wallet interface.
   588  func (w *spvWallet) AccountBalance(ctx context.Context, confirms int32, accountName string) (*walletjson.GetAccountBalanceResult, error) {
   589  	bal, err := w.accountBalance(ctx, confirms, accountName)
   590  	if err != nil {
   591  		return nil, err
   592  	}
   593  
   594  	return &walletjson.GetAccountBalanceResult{
   595  		AccountName:             accountName,
   596  		ImmatureCoinbaseRewards: bal.ImmatureCoinbaseRewards.ToCoin(),
   597  		ImmatureStakeGeneration: bal.ImmatureStakeGeneration.ToCoin(),
   598  		LockedByTickets:         bal.LockedByTickets.ToCoin(),
   599  		Spendable:               bal.Spendable.ToCoin(),
   600  		Total:                   bal.Total.ToCoin(),
   601  		Unconfirmed:             bal.Unconfirmed.ToCoin(),
   602  		VotingAuthority:         bal.VotingAuthority.ToCoin(),
   603  	}, nil
   604  }
   605  
   606  func (w *spvWallet) accountBalance(ctx context.Context, confirms int32, accountName string) (wallet.Balances, error) {
   607  	acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName)
   608  	if err != nil {
   609  		return wallet.Balances{}, err
   610  	}
   611  	return w.dcrWallet.AccountBalance(ctx, acctNum, confirms)
   612  }
   613  
   614  // LockedOutputs fetches locked outputs for the specified account.
   615  // Part of the Wallet interface.
   616  func (w *spvWallet) LockedOutputs(ctx context.Context, accountName string) ([]chainjson.TransactionInput, error) {
   617  	return w.dcrWallet.LockedOutpoints(ctx, accountName)
   618  }
   619  
   620  // Unspents fetches unspent outputs for the specified account.
   621  // Part of the Wallet interface.
   622  func (w *spvWallet) Unspents(ctx context.Context, accountName string) ([]*walletjson.ListUnspentResult, error) {
   623  	return w.dcrWallet.ListUnspent(ctx, 0, math.MaxInt32, nil, accountName)
   624  }
   625  
   626  // LockUnspent locks or unlocks the specified outpoint.
   627  // Part of the Wallet interface.
   628  func (w *spvWallet) LockUnspent(ctx context.Context, unlock bool, ops []*wire.OutPoint) error {
   629  	fun := w.LockOutpoint
   630  	if unlock {
   631  		fun = w.UnlockOutpoint
   632  	}
   633  	for _, op := range ops {
   634  		fun(&op.Hash, op.Index)
   635  	}
   636  	return nil
   637  }
   638  
   639  // UnspentOutput returns information about an unspent tx output, if found
   640  // and unspent.
   641  // This method is only guaranteed to return results for outputs that pay to
   642  // the wallet. Returns asset.CoinNotFoundError if the unspent output cannot
   643  // be located.
   644  // Part of the Wallet interface.
   645  func (w *spvWallet) UnspentOutput(ctx context.Context, txHash *chainhash.Hash, index uint32, _ int8) (*TxOutput, error) {
   646  	txd, err := w.dcrWallet.TxDetails(ctx, txHash)
   647  	if errors.Is(err, walleterrors.NotExist) {
   648  		return nil, asset.CoinNotFoundError
   649  	} else if err != nil {
   650  		return nil, err
   651  	}
   652  
   653  	details, err := w.ListTransactionDetails(ctx, txHash)
   654  	if err != nil {
   655  		return nil, err
   656  	}
   657  
   658  	var addrStr string
   659  	for _, detail := range details {
   660  		if detail.Vout == index {
   661  			addrStr = detail.Address
   662  		}
   663  	}
   664  	if addrStr == "" {
   665  		return nil, fmt.Errorf("error locating address for output")
   666  	}
   667  
   668  	tree := wire.TxTreeRegular
   669  	if txd.TxType != stake.TxTypeRegular {
   670  		tree = wire.TxTreeStake
   671  	}
   672  
   673  	if len(txd.MsgTx.TxOut) <= int(index) {
   674  		return nil, fmt.Errorf("not enough outputs")
   675  	}
   676  
   677  	_, tipHeight := w.MainChainTip(ctx)
   678  
   679  	var ours bool
   680  	for _, credit := range txd.Credits {
   681  		if credit.Index == index {
   682  			if credit.Spent {
   683  				return nil, asset.CoinNotFoundError
   684  			}
   685  			ours = true
   686  			break
   687  		}
   688  	}
   689  
   690  	if !ours {
   691  		return nil, asset.CoinNotFoundError
   692  	}
   693  
   694  	return &TxOutput{
   695  		TxOut:         txd.MsgTx.TxOut[index],
   696  		Tree:          tree,
   697  		Addresses:     []string{addrStr},
   698  		Confirmations: uint32(txd.Block.Height - tipHeight + 1),
   699  	}, nil
   700  }
   701  
   702  // ExternalAddress returns an external address using GapPolicyIgnore.
   703  // Part of the Wallet interface.
   704  // Using GapPolicyWrap here, introducing a relatively small risk of address
   705  // reuse, but improving wallet recoverability.
   706  func (w *spvWallet) ExternalAddress(ctx context.Context, accountName string) (stdaddr.Address, error) {
   707  	acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName)
   708  	if err != nil {
   709  		return nil, err
   710  	}
   711  	return w.NewExternalAddress(ctx, acctNum, wallet.WithGapPolicyWrap())
   712  }
   713  
   714  // InternalAddress returns an internal address using GapPolicyIgnore.
   715  // Part of the Wallet interface.
   716  func (w *spvWallet) InternalAddress(ctx context.Context, accountName string) (stdaddr.Address, error) {
   717  	acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName)
   718  	if err != nil {
   719  		return nil, err
   720  	}
   721  	return w.NewInternalAddress(ctx, acctNum, wallet.WithGapPolicyWrap())
   722  }
   723  
   724  // SignRawTransaction signs the provided transaction.
   725  // Part of the Wallet interface.
   726  func (w *spvWallet) SignRawTransaction(ctx context.Context, baseTx *wire.MsgTx) (*wire.MsgTx, error) {
   727  	tx := baseTx.Copy()
   728  	sigErrs, err := w.dcrWallet.SignTransaction(ctx, tx, txscript.SigHashAll, nil, nil, nil)
   729  	if err != nil {
   730  		return nil, err
   731  	}
   732  	if len(sigErrs) > 0 {
   733  		for _, sigErr := range sigErrs {
   734  			w.log.Errorf("signature error for index %d: %v", sigErr.InputIndex, sigErr.Error)
   735  		}
   736  		return nil, fmt.Errorf("%d signature errors", len(sigErrs))
   737  	}
   738  	return tx, nil
   739  }
   740  
   741  // SendRawTransaction broadcasts the provided transaction to the Decred network.
   742  // Part of the Wallet interface.
   743  func (w *spvWallet) SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) {
   744  	// TODO: Conditional high fee check?
   745  	return w.PublishTransaction(ctx, tx, w.spv)
   746  }
   747  
   748  // BlockTimestamp gets the timestamp of the block.
   749  func (w *spvWallet) BlockTimestamp(ctx context.Context, blockHash *chainhash.Hash) (time.Time, error) {
   750  	hdr, err := w.dcrWallet.BlockHeader(ctx, blockHash)
   751  	if err != nil {
   752  		return time.Time{}, err
   753  	}
   754  	return hdr.Timestamp, nil
   755  }
   756  
   757  // GetBlockHeader generates a *BlockHeader for the specified block hash. The
   758  // returned block header is a wire.BlockHeader with the addition of the block's
   759  // median time and other auxiliary information.
   760  func (w *spvWallet) GetBlockHeader(ctx context.Context, blockHash *chainhash.Hash) (*BlockHeader, error) {
   761  	hdr, err := w.dcrWallet.BlockHeader(ctx, blockHash)
   762  	if err != nil {
   763  		return nil, err
   764  	}
   765  
   766  	medianTime, err := w.medianTime(ctx, hdr)
   767  	if err != nil {
   768  		return nil, err
   769  	}
   770  
   771  	// Get next block hash unless there are none.
   772  	var nextHash *chainhash.Hash
   773  	confirmations := int64(-1)
   774  	mainChainHasBlock, _, err := w.BlockInMainChain(ctx, blockHash)
   775  	if err != nil {
   776  		return nil, fmt.Errorf("error checking if block is in mainchain: %w", err)
   777  	}
   778  	if mainChainHasBlock {
   779  		_, tipHeight := w.MainChainTip(ctx)
   780  		if int32(hdr.Height) < tipHeight {
   781  			nextHash, err = w.GetBlockHash(ctx, int64(hdr.Height)+1)
   782  			if err != nil {
   783  				return nil, fmt.Errorf("error getting next hash for block %q: %w", blockHash, err)
   784  			}
   785  		}
   786  		if int32(hdr.Height) <= tipHeight {
   787  			confirmations = int64(tipHeight) - int64(hdr.Height) + 1
   788  		} else { // if tip is less, may be rolling back, so just mock dcrd/dcrwallet
   789  			confirmations = 0
   790  		}
   791  	}
   792  
   793  	return &BlockHeader{
   794  		BlockHeader:   hdr,
   795  		MedianTime:    medianTime,
   796  		Confirmations: confirmations,
   797  		NextHash:      nextHash,
   798  	}, nil
   799  }
   800  
   801  // medianTime calculates a blocks median time, which is the median of the
   802  // timestamps of the previous 11 blocks.
   803  func (w *spvWallet) medianTime(ctx context.Context, iBlkHeader *wire.BlockHeader) (int64, error) {
   804  	// Calculate past median time. Look at the last 11 blocks, starting
   805  	// with the requested block, which is consistent with dcrd.
   806  	const numStamp = 11
   807  	timestamps := make([]int64, 0, numStamp)
   808  	for {
   809  		timestamps = append(timestamps, iBlkHeader.Timestamp.Unix())
   810  		if iBlkHeader.Height == 0 || len(timestamps) == numStamp {
   811  			break
   812  		}
   813  		var err error
   814  		iBlkHeader, err = w.dcrWallet.BlockHeader(ctx, &iBlkHeader.PrevBlock)
   815  		if err != nil {
   816  			return 0, fmt.Errorf("info not found for previous block: %v", err)
   817  		}
   818  	}
   819  	sort.Slice(timestamps, func(i, j int) bool {
   820  		return timestamps[i] < timestamps[j]
   821  	})
   822  	return timestamps[len(timestamps)/2], nil
   823  }
   824  
   825  // GetBlock returns the MsgBlock.
   826  // Part of the Wallet interface.
   827  func (w *spvWallet) GetBlock(ctx context.Context, blockHash *chainhash.Hash) (*wire.MsgBlock, error) {
   828  	if block := w.cachedBlock(blockHash); block != nil {
   829  		return block, nil
   830  	}
   831  
   832  	blocks, err := w.spv.Blocks(ctx, []*chainhash.Hash{blockHash})
   833  	if err != nil {
   834  		return nil, err
   835  	}
   836  	if len(blocks) == 0 { // Shouldn't actually be possible.
   837  		return nil, fmt.Errorf("network returned 0 blocks")
   838  	}
   839  
   840  	block := blocks[0]
   841  	w.cacheBlock(block)
   842  	return block, nil
   843  }
   844  
   845  // GetTransaction returns the details of a wallet tx, if the wallet contains a
   846  // tx with the provided hash. Returns asset.CoinNotFoundError if the tx is not
   847  // found in the wallet.
   848  // Part of the Wallet interface.
   849  func (w *spvWallet) GetTransaction(ctx context.Context, txHash *chainhash.Hash) (*WalletTransaction, error) {
   850  	// copy-pasted from dcrwallet/internal/rpc/jsonrpc/methods.go
   851  	txd, err := w.dcrWallet.TxDetails(ctx, txHash)
   852  	if errors.Is(err, walleterrors.NotExist) {
   853  		return nil, asset.CoinNotFoundError
   854  	} else if err != nil {
   855  		return nil, err
   856  	}
   857  
   858  	_, tipHeight := w.MainChainTip(ctx)
   859  
   860  	ret := WalletTransaction{
   861  		MsgTx: &txd.MsgTx,
   862  	}
   863  
   864  	if txd.Block.Height != -1 {
   865  		ret.BlockHash = txd.Block.Hash.String()
   866  		if tipHeight >= txd.Block.Height {
   867  			ret.Confirmations = int64(tipHeight - txd.Block.Height + 1)
   868  		} else {
   869  			ret.Confirmations = 1
   870  		}
   871  	}
   872  
   873  	details, err := w.ListTransactionDetails(ctx, txHash)
   874  	if err != nil {
   875  		return nil, err
   876  	}
   877  	ret.Details = make([]walletjson.GetTransactionDetailsResult, len(details))
   878  	for i, d := range details {
   879  		ret.Details[i] = walletjson.GetTransactionDetailsResult{
   880  			Account:           d.Account,
   881  			Address:           d.Address,
   882  			Amount:            d.Amount,
   883  			Category:          d.Category,
   884  			InvolvesWatchOnly: d.InvolvesWatchOnly,
   885  			Fee:               d.Fee,
   886  			Vout:              d.Vout,
   887  		}
   888  	}
   889  
   890  	return &ret, nil
   891  }
   892  
   893  // MatchAnyScript looks for any of the provided scripts in the block specified.
   894  // Part of the Wallet interface.
   895  func (w *spvWallet) MatchAnyScript(ctx context.Context, blockHash *chainhash.Hash, scripts [][]byte) (bool, error) {
   896  	key, filter, err := w.dcrWallet.CFilterV2(ctx, blockHash)
   897  	if err != nil {
   898  		return false, err
   899  	}
   900  	return filter.MatchAny(key, scripts), nil
   901  
   902  }
   903  
   904  // GetBestBlock returns the hash and height of the wallet's best block.
   905  // Part of the Wallet interface.
   906  func (w *spvWallet) GetBestBlock(ctx context.Context) (*chainhash.Hash, int64, error) {
   907  	blockHash, blockHeight := w.dcrWallet.MainChainTip(ctx)
   908  	return &blockHash, int64(blockHeight), nil
   909  }
   910  
   911  // GetBlockHash returns the hash of the mainchain block at the specified height.
   912  // Part of the Wallet interface.
   913  func (w *spvWallet) GetBlockHash(ctx context.Context, blockHeight int64) (*chainhash.Hash, error) {
   914  	info, err := w.dcrWallet.BlockInfo(ctx, wallet.NewBlockIdentifierFromHeight(int32(blockHeight)))
   915  	if err != nil {
   916  		return nil, err
   917  	}
   918  	return &info.Hash, nil
   919  }
   920  
   921  // AccountUnlocked returns true if the account is unlocked.
   922  // Part of the Wallet interface.
   923  func (w *spvWallet) AccountUnlocked(ctx context.Context, accountName string) (bool, error) {
   924  	acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName)
   925  	if err != nil {
   926  		return false, err
   927  	}
   928  	return w.dcrWallet.AccountUnlocked(ctx, acctNum)
   929  }
   930  
   931  // LockAccount locks the specified account.
   932  // Part of the Wallet interface.
   933  func (w *spvWallet) LockAccount(ctx context.Context, accountName string) error {
   934  	acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName)
   935  	if err != nil {
   936  		return err
   937  	}
   938  	return w.dcrWallet.LockAccount(ctx, acctNum)
   939  }
   940  
   941  // UnlockAccount unlocks the specified account or the wallet if account is not
   942  // encrypted. Part of the Wallet interface.
   943  func (w *spvWallet) UnlockAccount(ctx context.Context, pw []byte, accountName string) error {
   944  	acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName)
   945  	if err != nil {
   946  		return err
   947  	}
   948  	return w.dcrWallet.UnlockAccount(ctx, acctNum, pw)
   949  }
   950  
   951  func (w *spvWallet) upgradeAccounts(ctx context.Context, pw []byte) error {
   952  	ew, ok := w.dcrWallet.(*extendedWallet)
   953  	if !ok {
   954  		return nil // assume the accts exist, since we can't verify
   955  	}
   956  
   957  	if err := ew.Unlock(ctx, pw, nil); err != nil {
   958  		return fmt.Errorf("cannot unlock wallet to check mixing accts: %v", err)
   959  	}
   960  	defer ew.Lock()
   961  
   962  	if err := setupMixingAccounts(ctx, ew.Wallet, pw); err != nil {
   963  		return err
   964  	}
   965  	return nil
   966  }
   967  
   968  // SyncStatus returns the wallet's sync status.
   969  // Part of the Wallet interface.
   970  func (w *spvWallet) SyncStatus(ctx context.Context) (*asset.SyncStatus, error) {
   971  	ss := new(asset.SyncStatus)
   972  
   973  	targetHeight := w.bestPeerInitialHeight()
   974  	if targetHeight == 0 {
   975  		return ss, nil
   976  	}
   977  	ss.TargetHeight = uint64(targetHeight)
   978  
   979  	_, height := w.dcrWallet.MainChainTip(ctx)
   980  	if height == 0 {
   981  		return ss, nil
   982  	}
   983  	height = utils.Clamp(height, 0, targetHeight)
   984  	ss.Blocks = uint64(height)
   985  
   986  	ss.Synced, _ = w.spv.Synced(ctx)
   987  
   988  	if rescanHash, err := w.dcrWallet.RescanPoint(ctx); err != nil {
   989  		return nil, fmt.Errorf("error getting rescan point: %w", err)
   990  	} else if rescanHash != nil {
   991  		rescanHeader, err := w.dcrWallet.BlockHeader(ctx, rescanHash)
   992  		if err != nil {
   993  			return nil, fmt.Errorf("error getting rescan point header: %w", err)
   994  		}
   995  		h := uint64(utils.Clamp(rescanHeader.Height, 1, uint32(targetHeight)+1) - 1)
   996  		ss.Transactions = &h
   997  	}
   998  
   999  	return ss, nil
  1000  }
  1001  
  1002  // bestPeerInitialHeight is the highest InitialHeight recorded from connected
  1003  // spv peers. If no peers are connected, the last observed max peer height is
  1004  // returned.
  1005  func (w *spvWallet) bestPeerInitialHeight() int32 {
  1006  	peers := w.spv.GetRemotePeers()
  1007  	if len(peers) == 0 {
  1008  		return atomic.LoadInt32(&w.bestSpvPeerHeight)
  1009  	}
  1010  
  1011  	var bestHeight int32
  1012  	for _, p := range peers {
  1013  		if h := p.InitialHeight(); h > bestHeight {
  1014  			bestHeight = h
  1015  		}
  1016  	}
  1017  	atomic.StoreInt32(&w.bestSpvPeerHeight, bestHeight)
  1018  	return bestHeight
  1019  }
  1020  
  1021  // AddressPrivKey fetches the privkey for the specified address.
  1022  // Part of the Wallet interface.
  1023  func (w *spvWallet) AddressPrivKey(ctx context.Context, addr stdaddr.Address) (*secp256k1.PrivateKey, error) {
  1024  	privKey, _, err := w.dcrWallet.LoadPrivateKey(ctx, addr)
  1025  	return privKey, err
  1026  }
  1027  
  1028  // StakeInfo returns the current stake info.
  1029  func (w *spvWallet) StakeInfo(ctx context.Context) (*wallet.StakeInfoData, error) {
  1030  	return w.dcrWallet.StakeInfo(ctx)
  1031  }
  1032  
  1033  func (w *spvWallet) newVSPClient(vspHost, vspPubKey string, log dex.Logger) (*vspclient.Client, error) {
  1034  	return vspclient.New(vspclient.Config{
  1035  		URL:    vspHost,
  1036  		PubKey: vspPubKey,
  1037  		Dialer: new(net.Dialer).DialContext,
  1038  		Wallet: w.dcrWallet.(*extendedWallet).Wallet,
  1039  		Policy: &vspclient.Policy{
  1040  			MaxFee:     0.2e8,
  1041  			FeeAcct:    0,
  1042  			ChangeAcct: 0,
  1043  		},
  1044  		Params: w.chainParams,
  1045  	}, log)
  1046  }
  1047  
  1048  // rescan performs a blocking rescan, sending updates on the channel.
  1049  func (w *spvWallet) rescan(ctx context.Context, fromHeight int32, c chan wallet.RescanProgress) {
  1050  	w.dcrWallet.RescanProgressFromHeight(ctx, w.spv, fromHeight, c)
  1051  }
  1052  
  1053  // PurchaseTickets purchases n tickets, tells the provided vspd to monitor the
  1054  // ticket, and pays the vsp fee.
  1055  func (w *spvWallet) PurchaseTickets(ctx context.Context, n int, vspHost, vspPubKey string, mixing bool) ([]*asset.Ticket, error) {
  1056  	vspClient, err := w.newVSPClient(vspHost, vspPubKey, w.log.SubLogger("VSP"))
  1057  	if err != nil {
  1058  		return nil, err
  1059  	}
  1060  
  1061  	req := &wallet.PurchaseTicketsRequest{
  1062  		Count:                n,
  1063  		VSPFeePaymentProcess: vspClient.Process,
  1064  		VSPFeePercent:        vspClient.FeePercentage,
  1065  		Mixing:               mixing,
  1066  	}
  1067  
  1068  	if mixing {
  1069  		accts := w.Accounts()
  1070  		mixedAccountNum, err := w.AccountNumber(ctx, accts.PrimaryAccount)
  1071  		if err != nil {
  1072  			return nil, fmt.Errorf("error getting mixed account number: %w", err)
  1073  		}
  1074  		req.SourceAccount = mixedAccountNum
  1075  		// For simnet, we just change the source account. Others we need to
  1076  		// mix tickets through the cspp server.
  1077  		if w.chainParams.Net != wire.SimNet {
  1078  			req.MixedAccount = mixedAccountNum
  1079  			req.MixedAccountBranch = mixedAccountBranch
  1080  			req.MixedSplitAccount = req.MixedAccount
  1081  			req.ChangeAccount, err = w.AccountNumber(ctx, accts.UnmixedAccount)
  1082  			if err != nil {
  1083  				return nil, fmt.Errorf("error getting mixed change account number: %w", err)
  1084  			}
  1085  		}
  1086  	}
  1087  
  1088  	res, err := w.dcrWallet.PurchaseTickets(ctx, w.spv, req)
  1089  	if err != nil {
  1090  		return nil, err
  1091  	}
  1092  
  1093  	tickets := make([]*asset.Ticket, len(res.TicketHashes))
  1094  	for i, h := range res.TicketHashes {
  1095  		w.log.Debugf("Purchased ticket %s", h)
  1096  		ticketSummary, hdr, err := w.dcrWallet.GetTicketInfo(ctx, h)
  1097  		if err != nil {
  1098  			return nil, fmt.Errorf("error fetching info for new ticket")
  1099  		}
  1100  		ticket := ticketSummaryToAssetTicket(ticketSummary, hdr, w.log)
  1101  		if ticket == nil {
  1102  			return nil, fmt.Errorf("invalid ticket summary for %s", h)
  1103  		}
  1104  		tickets[i] = ticket
  1105  	}
  1106  	return tickets, err
  1107  }
  1108  
  1109  const (
  1110  	upperHeightMempool   = -1
  1111  	lowerHeightAutomatic = -1
  1112  	pageSizeUnlimited    = 0
  1113  )
  1114  
  1115  // Tickets returns current active tickets.
  1116  func (w *spvWallet) Tickets(ctx context.Context) ([]*asset.Ticket, error) {
  1117  	return w.ticketsInRange(ctx, lowerHeightAutomatic, upperHeightMempool, pageSizeUnlimited, 0)
  1118  }
  1119  
  1120  var _ ticketPager = (*spvWallet)(nil)
  1121  
  1122  func (w *spvWallet) TicketPage(ctx context.Context, scanStart int32, n, skipN int) ([]*asset.Ticket, error) {
  1123  	if scanStart == -1 {
  1124  		_, scanStart = w.MainChainTip(ctx)
  1125  	}
  1126  	return w.ticketsInRange(ctx, 0, scanStart, n, skipN)
  1127  }
  1128  
  1129  func (w *spvWallet) ticketsInRange(ctx context.Context, lowerHeight, upperHeight int32, maxN, skipN /* 0 = mempool */ int) ([]*asset.Ticket, error) {
  1130  	p := w.chainParams
  1131  	var startBlock, endBlock *wallet.BlockIdentifier // null endBlock goes through mempool
  1132  	// If mempool is included, there is no way to scan backwards.
  1133  	includeMempool := upperHeight == upperHeightMempool
  1134  	if includeMempool {
  1135  		_, upperHeight = w.MainChainTip(ctx)
  1136  	} else {
  1137  		endBlock = wallet.NewBlockIdentifierFromHeight(upperHeight)
  1138  	}
  1139  	if lowerHeight == lowerHeightAutomatic {
  1140  		bn := upperHeight - int32(p.TicketExpiry+uint32(p.TicketMaturity))
  1141  		startBlock = wallet.NewBlockIdentifierFromHeight(bn)
  1142  	} else {
  1143  		startBlock = wallet.NewBlockIdentifierFromHeight(lowerHeight)
  1144  	}
  1145  
  1146  	// If not looking at mempool, we can reverse iteration order by swapping
  1147  	// start and end blocks.
  1148  	if endBlock != nil {
  1149  		startBlock, endBlock = endBlock, startBlock
  1150  	}
  1151  
  1152  	tickets := make([]*asset.Ticket, 0)
  1153  	var skipped int
  1154  	processTicket := func(ticketSummaries []*wallet.TicketSummary, hdr *wire.BlockHeader) (bool, error) {
  1155  		for _, ticketSummary := range ticketSummaries {
  1156  			if skipped < skipN {
  1157  				skipped++
  1158  				continue
  1159  			}
  1160  			if ticket := ticketSummaryToAssetTicket(ticketSummary, hdr, w.log); ticket != nil {
  1161  				tickets = append(tickets, ticket)
  1162  			}
  1163  
  1164  			if maxN > 0 && len(tickets) >= maxN {
  1165  				return true, nil
  1166  			}
  1167  		}
  1168  
  1169  		return false, nil
  1170  	}
  1171  
  1172  	if err := w.dcrWallet.GetTickets(ctx, processTicket, startBlock, endBlock); err != nil {
  1173  		return nil, err
  1174  	}
  1175  
  1176  	// If this is a mempool scan, we cannot scan backwards, so reverse the
  1177  	// result order.
  1178  	if includeMempool {
  1179  		utils.ReverseSlice(tickets)
  1180  	}
  1181  
  1182  	return tickets, nil
  1183  }
  1184  
  1185  // VotingPreferences returns current voting preferences.
  1186  func (w *spvWallet) VotingPreferences(ctx context.Context) ([]*walletjson.VoteChoice, []*asset.TBTreasurySpend, []*walletjson.TreasuryPolicyResult, error) {
  1187  	_, agendas := wallet.CurrentAgendas(w.chainParams)
  1188  
  1189  	choices, _, err := w.dcrWallet.AgendaChoices(ctx, nil)
  1190  	if err != nil {
  1191  		return nil, nil, nil, fmt.Errorf("unable to get agenda choices: %v", err)
  1192  	}
  1193  
  1194  	voteChoices := make([]*walletjson.VoteChoice, len(choices))
  1195  
  1196  	i := 0
  1197  	for agendaID, choiceID := range choices {
  1198  		voteChoices[i] = &walletjson.VoteChoice{
  1199  			AgendaID: agendaID,
  1200  			ChoiceID: choiceID,
  1201  		}
  1202  		for _, agenda := range agendas {
  1203  			if agenda.Vote.Id != agendaID {
  1204  				continue
  1205  			}
  1206  			voteChoices[i].AgendaDescription = agenda.Vote.Description
  1207  			for _, choice := range agenda.Vote.Choices {
  1208  				if choiceID == choice.Id {
  1209  					voteChoices[i].ChoiceDescription = choice.Description
  1210  					break
  1211  				}
  1212  			}
  1213  		}
  1214  		i++
  1215  	}
  1216  	policyToStr := func(p stake.TreasuryVoteT) string {
  1217  		var policy string
  1218  		switch p {
  1219  		case stake.TreasuryVoteYes:
  1220  			policy = "yes"
  1221  		case stake.TreasuryVoteNo:
  1222  			policy = "no"
  1223  		}
  1224  		return policy
  1225  	}
  1226  	tspends := w.dcrWallet.GetAllTSpends(ctx)
  1227  	tSpendPolicy := make([]*asset.TBTreasurySpend, 0, len(tspends))
  1228  	for i := range tspends {
  1229  		msgTx := tspends[i]
  1230  		tspendHash := msgTx.TxHash()
  1231  		var val uint64
  1232  		for _, txOut := range msgTx.TxOut {
  1233  			val += uint64(txOut.Value)
  1234  		}
  1235  		p := w.dcrWallet.TSpendPolicy(&tspendHash, nil)
  1236  		tSpendPolicy = append(tSpendPolicy, &asset.TBTreasurySpend{
  1237  			Hash:          tspendHash.String(),
  1238  			CurrentPolicy: policyToStr(p),
  1239  			Value:         val,
  1240  		})
  1241  	}
  1242  
  1243  	policies := w.dcrWallet.TreasuryKeyPolicies()
  1244  	treasuryPolicy := make([]*walletjson.TreasuryPolicyResult, 0, len(policies))
  1245  	for i := range policies {
  1246  		r := walletjson.TreasuryPolicyResult{
  1247  			Key:    hex.EncodeToString(policies[i].PiKey),
  1248  			Policy: policyToStr(policies[i].Policy),
  1249  		}
  1250  		if policies[i].Ticket != nil {
  1251  			r.Ticket = policies[i].Ticket.String()
  1252  		}
  1253  		treasuryPolicy = append(treasuryPolicy, &r)
  1254  	}
  1255  
  1256  	return voteChoices, tSpendPolicy, treasuryPolicy, nil
  1257  }
  1258  
  1259  // SetVotingPreferences sets voting preferences for the wallet and for vsps with
  1260  // active tickets.
  1261  func (w *spvWallet) SetVotingPreferences(ctx context.Context, choices, tspendPolicy,
  1262  	treasuryPolicy map[string]string) error {
  1263  	// Set the consensus vote choices for the wallet.
  1264  	if len(choices) > 0 {
  1265  		_, err := w.SetAgendaChoices(ctx, nil, choices)
  1266  		if err != nil {
  1267  			return err
  1268  		}
  1269  	}
  1270  	strToPolicy := func(s, t string) (stake.TreasuryVoteT, error) {
  1271  		var policy stake.TreasuryVoteT
  1272  		switch s {
  1273  		case "abstain", "invalid", "":
  1274  			policy = stake.TreasuryVoteInvalid
  1275  		case "yes":
  1276  			policy = stake.TreasuryVoteYes
  1277  		case "no":
  1278  			policy = stake.TreasuryVoteNo
  1279  		default:
  1280  			return 0, fmt.Errorf("unknown %s policy %q", t, s)
  1281  		}
  1282  		return policy, nil
  1283  	}
  1284  	// Set the tspend policy for the wallet.
  1285  	for k, v := range tspendPolicy {
  1286  		if len(k) != chainhash.MaxHashStringSize {
  1287  			return fmt.Errorf("invalid tspend hash length, expected %d got %d",
  1288  				chainhash.MaxHashStringSize, len(k))
  1289  		}
  1290  		hash, err := chainhash.NewHashFromStr(k)
  1291  		if err != nil {
  1292  			return fmt.Errorf("invalid hash %s: %v", k, err)
  1293  		}
  1294  		policy, err := strToPolicy(v, "tspend")
  1295  		if err != nil {
  1296  			return err
  1297  		}
  1298  		err = w.dcrWallet.SetTSpendPolicy(ctx, hash, policy, nil)
  1299  		if err != nil {
  1300  			return err
  1301  		}
  1302  	}
  1303  	// Set the treasury policy for the wallet.
  1304  	for k, v := range treasuryPolicy {
  1305  		pikey, err := hex.DecodeString(k)
  1306  		if err != nil {
  1307  			return fmt.Errorf("unable to decode pi key %s: %v", k, err)
  1308  		}
  1309  		if len(pikey) != secp256k1.PubKeyBytesLenCompressed {
  1310  			return fmt.Errorf("treasury key %s must be 33 bytes", k)
  1311  		}
  1312  		policy, err := strToPolicy(v, "treasury")
  1313  		if err != nil {
  1314  			return err
  1315  		}
  1316  		err = w.dcrWallet.SetTreasuryKeyPolicy(ctx, pikey, policy, nil)
  1317  		if err != nil {
  1318  			return err
  1319  		}
  1320  	}
  1321  	clientCache := make(map[string]*vspclient.Client)
  1322  	// Set voting preferences for VSPs. Continuing for all errors.
  1323  	// NOTE: Doing this in an unmetered loop like this is a privacy breaker.
  1324  	return w.dcrWallet.ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error {
  1325  		vspHost, err := w.dcrWallet.VSPHostForTicket(ctx, hash)
  1326  		if err != nil {
  1327  			if errors.Is(err, walleterrors.NotExist) {
  1328  				w.log.Warnf("ticket %s is not associated with a VSP", hash)
  1329  				return nil
  1330  			}
  1331  			w.log.Warnf("unable to get VSP associated with ticket %s: %v", hash, err)
  1332  			return nil
  1333  		}
  1334  		vspClient, have := clientCache[vspHost]
  1335  		if !have {
  1336  			info, err := vspInfo(ctx, vspHost)
  1337  			if err != nil {
  1338  				w.log.Warnf("unable to get info from vsp at %s for ticket %s: %v", vspHost, hash, err)
  1339  				return nil
  1340  			}
  1341  			vspPubKey := base64.StdEncoding.EncodeToString(info.PubKey)
  1342  			vspClient, err = w.newVSPClient(vspHost, vspPubKey, w.log.SubLogger("VSP"))
  1343  			if err != nil {
  1344  				w.log.Warnf("unable to load vsp at %s for ticket %s: %v", vspHost, hash, err)
  1345  				return nil
  1346  			}
  1347  		}
  1348  		// Never return errors here, so all tickets are tried.
  1349  		// The first error will be returned to the user.
  1350  		vspTicket, err := w.NewVSPTicket(ctx, hash)
  1351  		if err != nil {
  1352  			w.log.Warnf("unable to create vsp ticket for vsp at %s for ticket %s: %v", vspHost, hash, err)
  1353  		}
  1354  		err = vspClient.SetVoteChoice(ctx, vspTicket, choices, tspendPolicy, treasuryPolicy)
  1355  		if err != nil {
  1356  			w.log.Warnf("unable to set vote for vsp at %s for ticket %s: %v", vspHost, hash, err)
  1357  		}
  1358  		return nil
  1359  	})
  1360  }
  1361  
  1362  func (w *spvWallet) ListSinceBlock(ctx context.Context, start int32) ([]ListTransactionsResult, error) {
  1363  	res := make([]ListTransactionsResult, 0)
  1364  	f := func(block *wallet.Block) (bool, error) {
  1365  		for _, tx := range block.Transactions {
  1366  			convertTxType := func(txType wallet.TransactionType) *walletjson.ListTransactionsTxType {
  1367  				switch txType {
  1368  				case wallet.TransactionTypeTicketPurchase:
  1369  					txType := walletjson.LTTTTicket
  1370  					return &txType
  1371  				case wallet.TransactionTypeVote:
  1372  					txType := walletjson.LTTTVote
  1373  					return &txType
  1374  				case wallet.TransactionTypeRevocation:
  1375  					txType := walletjson.LTTTRevocation
  1376  					return &txType
  1377  				case wallet.TransactionTypeCoinbase:
  1378  				case wallet.TransactionTypeRegular:
  1379  					txType := walletjson.LTTTRegular
  1380  					return &txType
  1381  				}
  1382  				w.log.Warnf("unknown transaction type %v", tx.Type)
  1383  				regularTxType := walletjson.LTTTRegular
  1384  				return &regularTxType
  1385  			}
  1386  			fee := tx.Fee.ToUnit(dcrutil.AmountCoin)
  1387  			var blockIndex, blockTime int64
  1388  			if block.Header != nil {
  1389  				blockIndex = int64(block.Header.Height)
  1390  				blockTime = block.Header.Timestamp.Unix()
  1391  			}
  1392  			res = append(res, ListTransactionsResult{
  1393  				TxID:       tx.Hash.String(),
  1394  				BlockIndex: &blockIndex,
  1395  				BlockTime:  blockTime,
  1396  				Send:       len(tx.MyInputs) > 0,
  1397  				TxType:     convertTxType(tx.Type),
  1398  				Fee:        &fee,
  1399  			})
  1400  		}
  1401  		return false, nil
  1402  	}
  1403  
  1404  	startID := wallet.NewBlockIdentifierFromHeight(start)
  1405  	return res, w.dcrWallet.GetTransactions(ctx, f, startID, nil)
  1406  }
  1407  
  1408  func (w *spvWallet) SetTxFee(_ context.Context, feePerKB dcrutil.Amount) error {
  1409  	w.dcrWallet.SetRelayFee(feePerKB)
  1410  	return nil
  1411  }
  1412  
  1413  // cacheBlock caches a block for future use. The block has a lastAccess stamp
  1414  // added, and will be discarded if not accessed again within 2 hours.
  1415  func (w *spvWallet) cacheBlock(block *wire.MsgBlock) {
  1416  	blockHash := block.BlockHash()
  1417  	w.blockCache.Lock()
  1418  	defer w.blockCache.Unlock()
  1419  	cached := w.blockCache.blocks[blockHash]
  1420  	if cached == nil {
  1421  		cb := &cachedBlock{
  1422  			MsgBlock:   block,
  1423  			lastAccess: time.Now(),
  1424  		}
  1425  		w.blockCache.blocks[blockHash] = cb
  1426  	} else {
  1427  		cached.lastAccess = time.Now()
  1428  	}
  1429  }
  1430  
  1431  // cachedBlock retrieves the MsgBlock from the cache, if it's been cached, else
  1432  // nil.
  1433  func (w *spvWallet) cachedBlock(blockHash *chainhash.Hash) *wire.MsgBlock {
  1434  	w.blockCache.Lock()
  1435  	defer w.blockCache.Unlock()
  1436  	cached := w.blockCache.blocks[*blockHash]
  1437  	if cached == nil {
  1438  		return nil
  1439  	}
  1440  	cached.lastAccess = time.Now()
  1441  	return cached.MsgBlock
  1442  }
  1443  
  1444  // PeerCount returns the count of currently connected peers.
  1445  func (w *spvWallet) PeerCount(ctx context.Context) (uint32, error) {
  1446  	return uint32(len(w.spv.GetRemotePeers())), nil
  1447  }
  1448  
  1449  // cleanBlockCache discards from the blockCache any blocks that have not been
  1450  // accessed for > 2 hours.
  1451  func (w *spvWallet) cleanBlockCache() {
  1452  	w.blockCache.Lock()
  1453  	defer w.blockCache.Unlock()
  1454  	for blockHash, cb := range w.blockCache.blocks {
  1455  		if time.Since(cb.lastAccess) > time.Hour*2 {
  1456  			delete(w.blockCache.blocks, blockHash)
  1457  		}
  1458  	}
  1459  }
  1460  
  1461  func newSpvSyncer(w *wallet.Wallet, netDir string, connectPeers []string) *spv.Syncer {
  1462  	addr := &net.TCPAddr{IP: net.ParseIP("::1"), Port: 0}
  1463  	amgr := addrmgr.New(netDir, net.LookupIP)
  1464  	lp := p2p.NewLocalPeer(w.ChainParams(), addr, amgr)
  1465  	syncer := spv.NewSyncer(w, lp)
  1466  	if len(connectPeers) > 0 {
  1467  		syncer.SetPersistentPeers(connectPeers)
  1468  	}
  1469  	w.SetNetworkBackend(syncer)
  1470  	return syncer
  1471  }
  1472  
  1473  // extendAddresses ensures that the internal and external branches have been
  1474  // extended to the specified indices. This can be used at wallet restoration to
  1475  // ensure that no duplicates are encountered with existing but unused addresses.
  1476  func extendAddresses(ctx context.Context, extIdx, intIdx uint32, dcrw *wallet.Wallet) error {
  1477  	if err := dcrw.SyncLastReturnedAddress(ctx, defaultAcct, udb.ExternalBranch, extIdx); err != nil {
  1478  		return fmt.Errorf("error syncing external branch index: %w", err)
  1479  	}
  1480  
  1481  	if err := dcrw.SyncLastReturnedAddress(ctx, defaultAcct, udb.InternalBranch, intIdx); err != nil {
  1482  		return fmt.Errorf("error syncing internal branch index: %w", err)
  1483  	}
  1484  
  1485  	return nil
  1486  }
  1487  
  1488  func newWalletConfig(db wallet.DB, chainParams *chaincfg.Params, gapLimit uint32) *wallet.Config {
  1489  	if gapLimit < wallet.DefaultGapLimit {
  1490  		gapLimit = wallet.DefaultGapLimit
  1491  	}
  1492  	return &wallet.Config{
  1493  		DB:              db,
  1494  		GapLimit:        gapLimit,
  1495  		AccountGapLimit: defaultAccountGapLimit,
  1496  		ManualTickets:   defaultManualTickets,
  1497  		AllowHighFees:   defaultAllowHighFees,
  1498  		RelayFee:        defaultRelayFeePerKb,
  1499  		Params:          chainParams,
  1500  		MixSplitLimit:   defaultMixSplitLimit,
  1501  	}
  1502  }
  1503  
  1504  func checkCreateDir(path string) error {
  1505  	if fi, err := os.Stat(path); err != nil {
  1506  		if os.IsNotExist(err) {
  1507  			// Attempt data directory creation
  1508  			if err = os.MkdirAll(path, 0700); err != nil {
  1509  				return fmt.Errorf("cannot create directory: %s", err)
  1510  			}
  1511  		} else {
  1512  			return fmt.Errorf("error checking directory: %s", err)
  1513  		}
  1514  	} else if !fi.IsDir() {
  1515  		return fmt.Errorf("path '%s' is not a directory", path)
  1516  	}
  1517  
  1518  	return nil
  1519  }
  1520  
  1521  // walletExists returns whether a file exists at the loader's database path.
  1522  // This may return an error for unexpected I/O failures.
  1523  func walletExists(dbDir string) (bool, error) {
  1524  	return fileExists(filepath.Join(dbDir, walletDbName))
  1525  }
  1526  
  1527  func fileExists(filePath string) (bool, error) {
  1528  	_, err := os.Stat(filePath)
  1529  	if err != nil {
  1530  		if os.IsNotExist(err) {
  1531  			return false, nil
  1532  		}
  1533  		return false, err
  1534  	}
  1535  	return true, nil
  1536  }
  1537  
  1538  // logWriter implements an io.Writer that outputs to a rotating log file.
  1539  type logWriter struct {
  1540  	*rotator.Rotator
  1541  }
  1542  
  1543  // Write writes the data in p to the log file.
  1544  func (w logWriter) Write(p []byte) (n int, err error) {
  1545  	return w.Rotator.Write(p)
  1546  }
  1547  
  1548  var (
  1549  	// loggingInited will be set when the log rotator has been initialized.
  1550  	loggingInited uint32
  1551  )
  1552  
  1553  // logRotator initializes a rotating file logger.
  1554  func logRotator(netDir string) (*rotator.Rotator, error) {
  1555  	const maxLogRolls = 8
  1556  	logDir := filepath.Join(netDir, logDirName)
  1557  	if err := os.MkdirAll(logDir, 0744); err != nil {
  1558  		return nil, fmt.Errorf("error creating log directory: %w", err)
  1559  	}
  1560  
  1561  	logFilename := filepath.Join(logDir, logFileName)
  1562  	return rotator.New(logFilename, 32*1024, false, maxLogRolls)
  1563  }
  1564  
  1565  // initLogging initializes logging in the dcrwallet packages. Logging only has
  1566  // to be initialized once, so an atomic flag is used internally to return early
  1567  // on subsequent invocations.
  1568  //
  1569  // TODO: See if the below precaution is even necessary for dcrwallet. In theory,
  1570  // the the rotating file logger must be Close'd at some point, but there are
  1571  // concurrency issues with that since btcd and btcwallet have unsupervised
  1572  // goroutines still running after shutdown. So we leave the rotator running at
  1573  // the risk of losing some logs.
  1574  func initLogging(netDir string) error {
  1575  	if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) {
  1576  		return nil
  1577  	}
  1578  
  1579  	logSpinner, err := logRotator(netDir)
  1580  	if err != nil {
  1581  		return fmt.Errorf("error initializing log rotator: %w", err)
  1582  	}
  1583  
  1584  	backendLog := slog.NewBackend(logWriter{logSpinner})
  1585  
  1586  	logger := func(name string, lvl slog.Level) slog.Logger {
  1587  		l := backendLog.Logger(name)
  1588  		l.SetLevel(lvl)
  1589  		return l
  1590  	}
  1591  	wallet.UseLogger(logger("WLLT", slog.LevelInfo))
  1592  	udb.UseLogger(logger("UDB", slog.LevelInfo))
  1593  	chain.UseLogger(logger("CHAIN", slog.LevelInfo))
  1594  	spv.UseLogger(logger("SPV", slog.LevelDebug))
  1595  	p2p.UseLogger(logger("P2P", slog.LevelInfo))
  1596  	connmgr.UseLogger(logger("CONMGR", slog.LevelInfo))
  1597  
  1598  	return nil
  1599  }
  1600  
  1601  func ticketSummaryToAssetTicket(ticketSummary *wallet.TicketSummary, hdr *wire.BlockHeader, log dex.Logger) *asset.Ticket {
  1602  	spender := ""
  1603  	if ticketSummary.Spender != nil {
  1604  		spender = ticketSummary.Spender.Hash.String()
  1605  	}
  1606  
  1607  	if ticketSummary.Ticket == nil || len(ticketSummary.Ticket.MyOutputs) < 1 {
  1608  		log.Errorf("No zeroth output")
  1609  		return nil
  1610  	}
  1611  
  1612  	var blockHeight int64 = -1
  1613  	if hdr != nil {
  1614  		blockHeight = int64(hdr.Height)
  1615  	}
  1616  
  1617  	return &asset.Ticket{
  1618  		Tx: asset.TicketTransaction{
  1619  			Hash:        ticketSummary.Ticket.Hash.String(),
  1620  			TicketPrice: uint64(ticketSummary.Ticket.MyOutputs[0].Amount),
  1621  			Fees:        uint64(ticketSummary.Ticket.Fee),
  1622  			Stamp:       uint64(ticketSummary.Ticket.Timestamp),
  1623  			BlockHeight: blockHeight,
  1624  		},
  1625  		Status:  asset.TicketStatus(ticketSummary.Status),
  1626  		Spender: spender,
  1627  	}
  1628  }