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