decred.org/dcrdex@v1.0.3/client/asset/dcr/dcr.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  	"bytes"
     8  	"context"
     9  	"crypto/sha256"
    10  	"encoding/base64"
    11  	"encoding/binary"
    12  	"encoding/hex"
    13  	"encoding/json"
    14  	"errors"
    15  	"fmt"
    16  	"io"
    17  	"math"
    18  	"net/http"
    19  	neturl "net/url"
    20  	"os"
    21  	"path/filepath"
    22  	"regexp"
    23  	"sort"
    24  	"strconv"
    25  	"strings"
    26  	"sync"
    27  	"sync/atomic"
    28  	"time"
    29  
    30  	"decred.org/dcrdex/client/asset"
    31  	"decred.org/dcrdex/client/asset/btc"
    32  	"decred.org/dcrdex/dex"
    33  	"decred.org/dcrdex/dex/calc"
    34  	"decred.org/dcrdex/dex/config"
    35  	"decred.org/dcrdex/dex/dexnet"
    36  	dexdcr "decred.org/dcrdex/dex/networks/dcr"
    37  	walletjson "decred.org/dcrwallet/v4/rpc/jsonrpc/types"
    38  	"decred.org/dcrwallet/v4/wallet"
    39  	_ "decred.org/dcrwallet/v4/wallet/drivers/bdb"
    40  	"github.com/decred/dcrd/blockchain/stake/v5"
    41  	blockchain "github.com/decred/dcrd/blockchain/standalone/v2"
    42  	"github.com/decred/dcrd/chaincfg/chainhash"
    43  	"github.com/decred/dcrd/chaincfg/v3"
    44  	"github.com/decred/dcrd/dcrec"
    45  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    46  	"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
    47  	"github.com/decred/dcrd/dcrutil/v4"
    48  	"github.com/decred/dcrd/hdkeychain/v3"
    49  	chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
    50  	"github.com/decred/dcrd/txscript/v4"
    51  	"github.com/decred/dcrd/txscript/v4/sign"
    52  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    53  	"github.com/decred/dcrd/txscript/v4/stdscript"
    54  	"github.com/decred/dcrd/wire"
    55  	vspdjson "github.com/decred/vspd/types/v2"
    56  )
    57  
    58  const (
    59  	// The implementation version. This considers the dex/networks package too.
    60  	version = 0
    61  
    62  	// BipID is the BIP-0044 asset ID.
    63  	BipID = 42
    64  
    65  	// defaultFee is the default value for the fallbackfee.
    66  	defaultFee = 20
    67  	// defaultFeeRateLimit is the default value for the feeratelimit.
    68  	defaultFeeRateLimit = 100
    69  	// defaultRedeemConfTarget is the default redeem transaction confirmation
    70  	// target in blocks used by estimatesmartfee to get the optimal fee for a
    71  	// redeem transaction.
    72  	defaultRedeemConfTarget = 1
    73  
    74  	// splitTxBaggage is the total number of additional bytes associated with
    75  	// using a split transaction to fund a swap.
    76  	splitTxBaggage = dexdcr.MsgTxOverhead + dexdcr.P2PKHInputSize + 2*dexdcr.P2PKHOutputSize
    77  
    78  	walletTypeDcrwRPC = "dcrwalletRPC"
    79  	walletTypeLegacy  = "" // dcrwallet RPC prior to wallet types
    80  	walletTypeSPV     = "SPV"
    81  
    82  	// confCheckTimeout is the amount of time allowed to check for
    83  	// confirmations. If SPV, this might involve pulling a full block.
    84  	confCheckTimeout = 4 * time.Second
    85  
    86  	// acctInternalBranch is the child number used when performing BIP0044 style
    87  	// hierarchical deterministic key derivation for the internal branch of an
    88  	// account.
    89  	acctInternalBranch uint32 = 1
    90  
    91  	// freshFeeAge is the expiry age for cached fee rates of external origin,
    92  	// past which fetchFeeFromOracle should be used to refresh the rate.
    93  	freshFeeAge = time.Minute
    94  
    95  	// requiredRedeemConfirms is the amount of confirms a redeem transaction
    96  	// needs before the trade is considered confirmed. The redeem is
    97  	// monitored until this number of confirms is reached. Two to make sure
    98  	// the block containing the redeem is stakeholder-approved
    99  	requiredRedeemConfirms = 2
   100  
   101  	vspFileName = "vsp.json"
   102  
   103  	defaultCSPPMainnet  = "mix.decred.org:5760"
   104  	defaultCSPPTestnet3 = "mix.decred.org:15760"
   105  
   106  	ticketSize               = dexdcr.MsgTxOverhead + dexdcr.P2PKHInputSize + 2*dexdcr.P2SHOutputSize /* stakesubmission and sstxchanges */ + 32 /* see e.g. RewardCommitmentScript */
   107  	minVSPTicketPurchaseSize = dexdcr.MsgTxOverhead + dexdcr.P2PKHInputSize + dexdcr.P2PKHOutputSize + ticketSize
   108  )
   109  
   110  var (
   111  	// ContractSearchLimit is how far back in time AuditContract in SPV mode
   112  	// will search for a contract if no txData is provided. This should be a
   113  	// positive duration.
   114  	ContractSearchLimit = 48 * time.Hour
   115  
   116  	// blockTicker is the delay between calls to check for new blocks.
   117  	blockTicker                  = time.Second
   118  	peerCountTicker              = 5 * time.Second
   119  	conventionalConversionFactor = float64(dexdcr.UnitInfo.Conventional.ConversionFactor)
   120  	walletBlockAllowance         = time.Second * 10
   121  
   122  	// maxRedeemMempoolAge is the max amount of time the wallet will let a
   123  	// redeem transaction sit in mempool from the time it is first seen
   124  	// until it attempts to abandon it and try to send a new transaction.
   125  	// This is necessary because transactions with already spent inputs may
   126  	// be tried over and over with wallet in SPV mode.
   127  	maxRedeemMempoolAge = time.Hour * 2
   128  
   129  	walletOpts = []*asset.ConfigOption{
   130  		{
   131  			Key:         "fallbackfee",
   132  			DisplayName: "Fallback fee rate",
   133  			Description: "The fee rate to use for fee payment and withdrawals when " +
   134  				"estimatesmartfee is not available. Units: DCR/kB",
   135  			DefaultValue: defaultFee * 1000 / 1e8,
   136  		},
   137  		{
   138  			Key:         "feeratelimit",
   139  			DisplayName: "Highest acceptable fee rate",
   140  			Description: "This is the highest network fee rate you are willing to " +
   141  				"pay on swap transactions. If feeratelimit is lower than a market's " +
   142  				"maxfeerate, you will not be able to trade on that market with this " +
   143  				"wallet.  Units: DCR/kB",
   144  			DefaultValue: defaultFeeRateLimit * 1000 / 1e8,
   145  		},
   146  		{
   147  			Key:         "redeemconftarget",
   148  			DisplayName: "Redeem confirmation target",
   149  			Description: "The target number of blocks for the redeem transaction " +
   150  				"to get a confirmation. Used to set the transaction's fee rate." +
   151  				" (default: 1 block)",
   152  			DefaultValue: defaultRedeemConfTarget,
   153  		},
   154  		{
   155  			Key:          "gaplimit",
   156  			DisplayName:  "Address Gap Limit",
   157  			Description:  "The gap limit for used address discovery",
   158  			DefaultValue: wallet.DefaultGapLimit,
   159  		},
   160  		{
   161  			Key:         "txsplit",
   162  			DisplayName: "Pre-size funding inputs",
   163  			Description: "When placing an order, create a \"split\" transaction to " +
   164  				"fund the order without locking more of the wallet balance than " +
   165  				"necessary. Otherwise, excess funds may be reserved to fund the order " +
   166  				"until the first swap contract is broadcast during match settlement, or " +
   167  				"the order is canceled. This an extra transaction for which network " +
   168  				"mining fees are paid.",
   169  			IsBoolean:    true,
   170  			DefaultValue: true, // cheap fees, helpful for bond reserves, and adjustable at order-time
   171  		},
   172  		{
   173  			Key:         "apifeefallback",
   174  			DisplayName: "External fee rate estimates",
   175  			Description: "Allow fee rate estimation from a block explorer API. " +
   176  				"This is useful as a fallback for SPV wallets and RPC wallets " +
   177  				"that have recently been started.",
   178  			IsBoolean:    true,
   179  			DefaultValue: true,
   180  		},
   181  	}
   182  
   183  	rpcOpts = []*asset.ConfigOption{
   184  		{
   185  			Key:         "account",
   186  			DisplayName: "Account Name",
   187  			Description: "Primary dcrwallet account name for trading. If automatic mixing of trading funds is " +
   188  				"desired, this should be the wallet's mixed account and the other accounts should be set too. " +
   189  				"See wallet documentation for mixing wallet setup instructions.",
   190  		},
   191  		{
   192  			Key:         "unmixedaccount",
   193  			DisplayName: "Change Account Name",
   194  			Description: "dcrwallet change account name. This and the 'Temporary Trading Account' should only be " +
   195  				"set if mixing is enabled on the wallet. If set, deposit addresses will be from this account and will " +
   196  				"be mixed before being available to trade.",
   197  		},
   198  		{
   199  			Key:         "tradingaccount",
   200  			DisplayName: "Temporary Trading Account",
   201  			Description: "dcrwallet account to temporarily store split tx outputs or change from chained swaps in " +
   202  				"multi-lot orders. This should only be set if 'Change Account Name' is set.",
   203  		},
   204  		{
   205  			Key:         "username",
   206  			DisplayName: "RPC Username",
   207  			Description: "dcrwallet's 'username' setting for JSON-RPC",
   208  		},
   209  		{
   210  			Key:         "password",
   211  			DisplayName: "RPC Password",
   212  			Description: "dcrwallet's 'password' setting for JSON-RPC",
   213  			NoEcho:      true,
   214  		},
   215  		{
   216  			Key:          "rpclisten",
   217  			DisplayName:  "RPC Address",
   218  			Description:  "dcrwallet's address (host or host:port) (default port: 9110)",
   219  			DefaultValue: "127.0.0.1:9110",
   220  		},
   221  		{
   222  			Key:          "rpccert",
   223  			DisplayName:  "TLS Certificate",
   224  			Description:  "Path to the dcrwallet TLS certificate file",
   225  			DefaultValue: defaultRPCCert,
   226  		},
   227  	}
   228  
   229  	multiFundingOpts = []*asset.OrderOption{
   230  		{
   231  			ConfigOption: asset.ConfigOption{
   232  				Key:         multiSplitKey,
   233  				DisplayName: "Allow multi split",
   234  				Description: "Allow split funding transactions that pre-size outputs to " +
   235  					"prevent excessive overlock.",
   236  				IsBoolean:    true,
   237  				DefaultValue: true,
   238  			},
   239  		},
   240  		{
   241  			ConfigOption: asset.ConfigOption{
   242  				Key:         multiSplitBufferKey,
   243  				DisplayName: "Multi split buffer",
   244  				Description: "Add an integer percent buffer to split output amounts to " +
   245  					"facilitate output reuse. This is only required for quote assets.",
   246  				DefaultValue: 5,
   247  				DependsOn:    multiSplitKey,
   248  			},
   249  			QuoteAssetOnly: true,
   250  			XYRange: &asset.XYRange{
   251  				Start: asset.XYRangePoint{
   252  					Label: "0%",
   253  					X:     0,
   254  					Y:     0,
   255  				},
   256  				End: asset.XYRangePoint{
   257  					Label: "100%",
   258  					X:     100,
   259  					Y:     100,
   260  				},
   261  				XUnit:  "%",
   262  				YUnit:  "%",
   263  				RoundX: true,
   264  				RoundY: true,
   265  			},
   266  		},
   267  	}
   268  
   269  	// WalletInfo defines some general information about a Decred wallet.
   270  	WalletInfo = &asset.WalletInfo{
   271  		Name:              "Decred",
   272  		SupportedVersions: []uint32{version},
   273  		UnitInfo:          dexdcr.UnitInfo,
   274  		AvailableWallets: []*asset.WalletDefinition{
   275  			{
   276  				Type:             walletTypeSPV,
   277  				Tab:              "Native",
   278  				Description:      "Use the built-in SPV wallet",
   279  				ConfigOpts:       walletOpts,
   280  				Seeded:           true,
   281  				MultiFundingOpts: multiFundingOpts,
   282  			},
   283  			{
   284  				Type:              walletTypeDcrwRPC,
   285  				Tab:               "External",
   286  				Description:       "Connect to dcrwallet",
   287  				DefaultConfigPath: defaultConfigPath,
   288  				ConfigOpts:        append(rpcOpts, walletOpts...),
   289  				MultiFundingOpts:  multiFundingOpts,
   290  			},
   291  		},
   292  	}
   293  	swapFeeBumpKey      = "swapfeebump"
   294  	splitKey            = "swapsplit"
   295  	multiSplitKey       = "multisplit"
   296  	multiSplitBufferKey = "multisplitbuffer"
   297  	redeemFeeBumpFee    = "redeemfeebump"
   298  	client              http.Client
   299  )
   300  
   301  // outPoint is the hash and output index of a transaction output.
   302  type outPoint struct {
   303  	txHash chainhash.Hash
   304  	vout   uint32
   305  }
   306  
   307  // newOutPoint is the constructor for a new outPoint.
   308  func newOutPoint(txHash *chainhash.Hash, vout uint32) outPoint {
   309  	return outPoint{
   310  		txHash: *txHash,
   311  		vout:   vout,
   312  	}
   313  }
   314  
   315  // String is a human-readable string representation of the outPoint.
   316  func (pt outPoint) String() string {
   317  	return pt.txHash.String() + ":" + strconv.Itoa(int(pt.vout))
   318  }
   319  
   320  // output is information about a transaction output. output satisfies the
   321  // asset.Coin interface.
   322  type output struct {
   323  	pt    outPoint
   324  	tree  int8
   325  	value uint64
   326  }
   327  
   328  // newOutput is the constructor for an output.
   329  func newOutput(txHash *chainhash.Hash, vout uint32, value uint64, tree int8) *output {
   330  	return &output{
   331  		pt: outPoint{
   332  			txHash: *txHash,
   333  			vout:   vout,
   334  		},
   335  		value: value,
   336  		tree:  tree,
   337  	}
   338  }
   339  
   340  // Value returns the value of the output. Part of the asset.Coin interface.
   341  func (op *output) Value() uint64 {
   342  	return op.value
   343  }
   344  
   345  // ID is the output's coin ID. Part of the asset.Coin interface. For DCR, the
   346  // coin ID is 36 bytes = 32 bytes tx hash + 4 bytes big-endian vout.
   347  func (op *output) ID() dex.Bytes {
   348  	return toCoinID(op.txHash(), op.vout())
   349  }
   350  
   351  func (op *output) TxID() string {
   352  	return op.txHash().String()
   353  }
   354  
   355  // String is a string representation of the coin.
   356  func (op *output) String() string {
   357  	return op.pt.String()
   358  }
   359  
   360  // txHash returns the pointer of the outPoint's txHash.
   361  func (op *output) txHash() *chainhash.Hash {
   362  	return &op.pt.txHash
   363  }
   364  
   365  // vout returns the outPoint's vout.
   366  func (op *output) vout() uint32 {
   367  	return op.pt.vout
   368  }
   369  
   370  // wireOutPoint creates and returns a new *wire.OutPoint for the output.
   371  func (op *output) wireOutPoint() *wire.OutPoint {
   372  	return wire.NewOutPoint(op.txHash(), op.vout(), op.tree)
   373  }
   374  
   375  // auditInfo is information about a swap contract on the blockchain, not
   376  // necessarily created by this wallet, as would be returned from AuditContract.
   377  type auditInfo struct {
   378  	output     *output
   379  	secretHash []byte
   380  	contract   []byte
   381  	recipient  stdaddr.Address // unused?
   382  	expiration time.Time
   383  }
   384  
   385  // Expiration is the expiration time of the contract, which is the earliest time
   386  // that a refund can be issued for an un-redeemed contract.
   387  func (ci *auditInfo) Expiration() time.Time {
   388  	return ci.expiration
   389  }
   390  
   391  // Contract is the contract script.
   392  func (ci *auditInfo) Contract() dex.Bytes {
   393  	return ci.contract
   394  }
   395  
   396  // Coin returns the output as an asset.Coin.
   397  func (ci *auditInfo) Coin() asset.Coin {
   398  	return ci.output
   399  }
   400  
   401  // SecretHash is the contract's secret hash.
   402  func (ci *auditInfo) SecretHash() dex.Bytes {
   403  	return ci.secretHash
   404  }
   405  
   406  // convertAuditInfo converts from the common *asset.AuditInfo type to our
   407  // internal *auditInfo type.
   408  func convertAuditInfo(ai *asset.AuditInfo, chainParams *chaincfg.Params) (*auditInfo, error) {
   409  	if ai.Coin == nil {
   410  		return nil, fmt.Errorf("no coin")
   411  	}
   412  
   413  	op, ok := ai.Coin.(*output)
   414  	if !ok {
   415  		return nil, fmt.Errorf("unknown coin type %T", ai.Coin)
   416  	}
   417  
   418  	recip, err := stdaddr.DecodeAddress(ai.Recipient, chainParams)
   419  	if err != nil {
   420  		return nil, err
   421  	}
   422  
   423  	return &auditInfo{
   424  		output:     op,            // *output
   425  		recipient:  recip,         // btcutil.Address
   426  		contract:   ai.Contract,   // []byte
   427  		secretHash: ai.SecretHash, // []byte
   428  		expiration: ai.Expiration, // time.Time
   429  	}, nil
   430  }
   431  
   432  // swapReceipt is information about a swap contract that was broadcast by this
   433  // wallet. Satisfies the asset.Receipt interface.
   434  type swapReceipt struct {
   435  	output       *output
   436  	contract     []byte
   437  	signedRefund []byte
   438  	expiration   time.Time
   439  }
   440  
   441  // Expiration is the time that the contract will expire, allowing the user to
   442  // issue a refund transaction. Part of the asset.Receipt interface.
   443  func (r *swapReceipt) Expiration() time.Time {
   444  	return r.expiration
   445  }
   446  
   447  // Coin is the contract script. Part of the asset.Receipt interface.
   448  func (r *swapReceipt) Contract() dex.Bytes {
   449  	return r.contract
   450  }
   451  
   452  // Coin is the output information as an asset.Coin. Part of the asset.Receipt
   453  // interface.
   454  func (r *swapReceipt) Coin() asset.Coin {
   455  	return r.output
   456  }
   457  
   458  // String provides a human-readable representation of the contract's Coin.
   459  func (r *swapReceipt) String() string {
   460  	return r.output.String()
   461  }
   462  
   463  // SignedRefund is a signed refund script that can be used to return
   464  // funds to the user in the case a contract expires.
   465  func (r *swapReceipt) SignedRefund() dex.Bytes {
   466  	return r.signedRefund
   467  }
   468  
   469  // fundingCoin is similar to output, but also stores the address. The
   470  // ExchangeWallet fundingCoins dict is used as a local cache of coins being
   471  // spent.
   472  type fundingCoin struct {
   473  	op   *output
   474  	addr string
   475  }
   476  
   477  // Driver implements asset.Driver.
   478  type Driver struct{}
   479  
   480  // Check that Driver implements asset.Driver.
   481  var _ asset.Driver = (*Driver)(nil)
   482  var _ asset.Creator = (*Driver)(nil)
   483  
   484  // Open creates the DCR exchange wallet. Start the wallet with its Run method.
   485  func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) {
   486  	return NewWallet(cfg, logger, network)
   487  }
   488  
   489  // DecodeCoinID creates a human-readable representation of a coin ID for Decred.
   490  func (d *Driver) DecodeCoinID(coinID []byte) (string, error) {
   491  	txid, vout, err := decodeCoinID(coinID)
   492  	if err != nil {
   493  		return "<invalid>", err
   494  	}
   495  	return fmt.Sprintf("%v:%d", txid, vout), err
   496  }
   497  
   498  // Info returns basic information about the wallet and asset.
   499  func (d *Driver) Info() *asset.WalletInfo {
   500  	return WalletInfo
   501  }
   502  
   503  // Exists checks the existence of the wallet. Part of the Creator interface.
   504  func (d *Driver) Exists(walletType, dataDir string, _ map[string]string, net dex.Network) (bool, error) {
   505  	if walletType != walletTypeSPV {
   506  		return false, fmt.Errorf("no Decred wallet of type %q available", walletType)
   507  	}
   508  
   509  	chainParams, err := parseChainParams(net)
   510  	if err != nil {
   511  		return false, err
   512  	}
   513  
   514  	return walletExists(filepath.Join(dataDir, chainParams.Name, "spv"))
   515  }
   516  
   517  // Create creates a new SPV wallet.
   518  func (d *Driver) Create(params *asset.CreateWalletParams) error {
   519  	if params.Type != walletTypeSPV {
   520  		return fmt.Errorf("SPV is the only seeded wallet type. required = %q, requested = %q", walletTypeSPV, params.Type)
   521  	}
   522  	if len(params.Seed) == 0 {
   523  		return errors.New("wallet seed cannot be empty")
   524  	}
   525  	if len(params.DataDir) == 0 {
   526  		return errors.New("must specify wallet data directory")
   527  	}
   528  	chainParams, err := parseChainParams(params.Net)
   529  	if err != nil {
   530  		return fmt.Errorf("error parsing chain params: %w", err)
   531  	}
   532  
   533  	recoveryCfg := new(RecoveryCfg)
   534  	err = config.Unmapify(params.Settings, recoveryCfg)
   535  	if err != nil {
   536  		return err
   537  	}
   538  
   539  	return createSPVWallet(params.Pass, params.Seed, params.DataDir, recoveryCfg.NumExternalAddresses,
   540  		recoveryCfg.NumInternalAddresses, recoveryCfg.GapLimit, chainParams)
   541  }
   542  
   543  // MinLotSize calculates the minimum bond size for a given fee rate that avoids
   544  // dust outputs on the swap and refund txs, assuming the maxFeeRate doesn't
   545  // change.
   546  func (d *Driver) MinLotSize(maxFeeRate uint64) uint64 {
   547  	return dexdcr.MinLotSize(maxFeeRate)
   548  }
   549  
   550  func init() {
   551  	asset.Register(BipID, &Driver{})
   552  }
   553  
   554  // RecoveryCfg is the information that is transferred from the old wallet
   555  // to the new one when the wallet is recovered.
   556  type RecoveryCfg struct {
   557  	NumExternalAddresses uint32 `ini:"numexternaladdr"`
   558  	NumInternalAddresses uint32 `ini:"numinternaladdr"`
   559  	GapLimit             uint32 `ini:"gaplimit"`
   560  }
   561  
   562  // swapOptions captures the available Swap options. Tagged to be used with
   563  // config.Unmapify to decode e.g. asset.Order.Options.
   564  type swapOptions struct {
   565  	Split   *bool    `ini:"swapsplit"`
   566  	FeeBump *float64 `ini:"swapfeebump"`
   567  }
   568  
   569  func (s *swapOptions) feeBump() (float64, error) {
   570  	bump := 1.0
   571  	if s.FeeBump != nil {
   572  		bump = *s.FeeBump
   573  		if bump > 2.0 {
   574  			return 0, fmt.Errorf("fee bump %f is higher than the 2.0 limit", bump)
   575  		}
   576  		if bump < 1.0 {
   577  			return 0, fmt.Errorf("fee bump %f is lower than 1", bump)
   578  		}
   579  	}
   580  	return bump, nil
   581  }
   582  
   583  // redeemOptions are order options that apply to redemptions.
   584  type redeemOptions struct {
   585  	FeeBump *float64 `ini:"redeemfeebump"`
   586  }
   587  
   588  type feeStamped struct {
   589  	rate  uint64
   590  	stamp time.Time
   591  }
   592  
   593  // exchangeWalletConfig is the validated, unit-converted, user-configurable
   594  // wallet settings.
   595  type exchangeWalletConfig struct {
   596  	useSplitTx       bool
   597  	fallbackFeeRate  uint64
   598  	feeRateLimit     uint64
   599  	redeemConfTarget uint64
   600  	apiFeeFallback   bool
   601  }
   602  
   603  type mempoolRedeem struct {
   604  	txHash    chainhash.Hash
   605  	firstSeen time.Time
   606  }
   607  
   608  // vsp holds info needed for purchasing tickets from a vsp. PubKey is from the
   609  // vsp and is used for verifying communications.
   610  type vsp struct {
   611  	URL           string  `json:"url"`
   612  	FeePercentage float64 `json:"feepercent"`
   613  	PubKey        string  `json:"pubkey"`
   614  }
   615  
   616  // rescanProgress is the progress of an asynchronous rescan.
   617  type rescanProgress struct {
   618  	scannedThrough int64
   619  }
   620  
   621  // ExchangeWallet is a wallet backend for Decred. The backend is how the DEX
   622  // client app communicates with the Decred blockchain and wallet. ExchangeWallet
   623  // satisfies the dex.Wallet interface.
   624  type ExchangeWallet struct {
   625  	bondReserves atomic.Uint64
   626  	cfgV         atomic.Value // *exchangeWalletConfig
   627  
   628  	ctx            context.Context // the asset subsystem starts with Connect(ctx)
   629  	wg             sync.WaitGroup
   630  	wallet         Wallet
   631  	chainParams    *chaincfg.Params
   632  	log            dex.Logger
   633  	network        dex.Network
   634  	emit           *asset.WalletEmitter
   635  	lastPeerCount  uint32
   636  	peersChange    func(uint32, error)
   637  	vspFilepath    string
   638  	walletType     string
   639  	walletDir      string
   640  	startingBlocks atomic.Uint64
   641  
   642  	oracleFeesMtx sync.Mutex
   643  	oracleFees    map[uint64]feeStamped // conf target => fee rate
   644  	oracleFailing bool
   645  
   646  	handleTipMtx sync.Mutex
   647  	currentTip   atomic.Value // *block
   648  
   649  	// Coins returned by Fund are cached for quick reference.
   650  	fundingMtx   sync.RWMutex
   651  	fundingCoins map[outPoint]*fundingCoin
   652  
   653  	findRedemptionMtx   sync.RWMutex
   654  	findRedemptionQueue map[outPoint]*findRedemptionReq
   655  
   656  	externalTxMtx   sync.RWMutex
   657  	externalTxCache map[chainhash.Hash]*externalTx
   658  
   659  	// TODO: Consider persisting mempool redeems on file.
   660  	mempoolRedeemsMtx sync.RWMutex
   661  	mempoolRedeems    map[[32]byte]*mempoolRedeem // keyed by secret hash
   662  
   663  	vspV atomic.Value // *vsp
   664  
   665  	connected atomic.Bool
   666  
   667  	subsidyCache *blockchain.SubsidyCache
   668  
   669  	ticketBuyer struct {
   670  		running            atomic.Bool
   671  		remaining          atomic.Int32
   672  		unconfirmedTickets map[chainhash.Hash]struct{}
   673  	}
   674  
   675  	// Embedding wallets can set cycleMixer, which will be triggered after
   676  	// new block are seen.
   677  	cycleMixer func()
   678  	mixing     atomic.Bool
   679  
   680  	pendingTxsMtx sync.RWMutex
   681  	pendingTxs    map[chainhash.Hash]*btc.ExtendedWalletTx
   682  
   683  	receiveTxLastQuery atomic.Uint64
   684  
   685  	txHistoryDB      atomic.Value // *btc.BadgerTxDB
   686  	syncingTxHistory atomic.Bool
   687  
   688  	previouslySynced atomic.Bool
   689  
   690  	rescan struct {
   691  		sync.RWMutex
   692  		progress *rescanProgress // nil = no rescan in progress
   693  	}
   694  }
   695  
   696  func (dcr *ExchangeWallet) config() *exchangeWalletConfig {
   697  	return dcr.cfgV.Load().(*exchangeWalletConfig)
   698  }
   699  
   700  // Check that ExchangeWallet satisfies the Wallet interface.
   701  var _ asset.Wallet = (*ExchangeWallet)(nil)
   702  var _ asset.FeeRater = (*ExchangeWallet)(nil)
   703  var _ asset.Withdrawer = (*ExchangeWallet)(nil)
   704  var _ asset.LiveReconfigurer = (*ExchangeWallet)(nil)
   705  var _ asset.TxFeeEstimator = (*ExchangeWallet)(nil)
   706  var _ asset.Bonder = (*ExchangeWallet)(nil)
   707  var _ asset.Authenticator = (*ExchangeWallet)(nil)
   708  var _ asset.TicketBuyer = (*ExchangeWallet)(nil)
   709  var _ asset.WalletHistorian = (*ExchangeWallet)(nil)
   710  
   711  type block struct {
   712  	height int64
   713  	hash   *chainhash.Hash
   714  }
   715  
   716  // findRedemptionReq represents a request to find a contract's redemption,
   717  // which is added to the findRedemptionQueue with the contract outpoint as
   718  // key.
   719  type findRedemptionReq struct {
   720  	ctx                     context.Context
   721  	contractP2SHScript      []byte
   722  	contractOutputScriptVer uint16
   723  	resultChan              chan *findRedemptionResult
   724  }
   725  
   726  func (frr *findRedemptionReq) canceled() bool {
   727  	return frr.ctx.Err() != nil
   728  }
   729  
   730  // findRedemptionResult models the result of a find redemption attempt.
   731  type findRedemptionResult struct {
   732  	RedemptionCoinID dex.Bytes
   733  	Secret           dex.Bytes
   734  	Err              error
   735  }
   736  
   737  // NewWallet is the exported constructor by which the DEX will import the
   738  // exchange wallet.
   739  func NewWallet(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) {
   740  	// loadConfig will set fields if defaults are used and set the chainParams
   741  	// variable.
   742  	walletCfg := new(walletConfig)
   743  	chainParams, err := loadConfig(cfg.Settings, network, walletCfg)
   744  	if err != nil {
   745  		return nil, err
   746  	}
   747  
   748  	dcr, err := unconnectedWallet(cfg, walletCfg, chainParams, logger, network)
   749  	if err != nil {
   750  		return nil, err
   751  	}
   752  
   753  	var w asset.Wallet = dcr
   754  
   755  	switch cfg.Type {
   756  	case walletTypeDcrwRPC, walletTypeLegacy:
   757  		dcr.wallet, err = newRPCWallet(cfg.Settings, logger, network)
   758  		if err != nil {
   759  			return nil, err
   760  		}
   761  	case walletTypeSPV:
   762  		dcr.wallet, err = openSPVWallet(cfg.DataDir, walletCfg.GapLimit, chainParams, logger)
   763  		if err != nil {
   764  			return nil, err
   765  		}
   766  		w, err = initNativeWallet(dcr)
   767  		if err != nil {
   768  			return nil, err
   769  		}
   770  	default:
   771  		makeCustomWallet, ok := customWalletConstructors[cfg.Type]
   772  		if !ok {
   773  			return nil, fmt.Errorf("unknown wallet type %q", cfg.Type)
   774  		}
   775  
   776  		// Create custom wallet and return early if we encounter any error.
   777  		dcr.wallet, err = makeCustomWallet(cfg.Settings, chainParams, logger)
   778  		if err != nil {
   779  			return nil, fmt.Errorf("custom wallet setup error: %v", err)
   780  		}
   781  	}
   782  
   783  	return w, nil
   784  }
   785  
   786  func getExchangeWalletCfg(dcrCfg *walletConfig, logger dex.Logger) (*exchangeWalletConfig, error) {
   787  	// If set in the user config, the fallback fee will be in units of DCR/kB.
   788  	// Convert to atoms/B.
   789  	fallbackFeesPerByte := toAtoms(dcrCfg.FallbackFeeRate / 1000)
   790  	if fallbackFeesPerByte == 0 {
   791  		fallbackFeesPerByte = defaultFee
   792  	}
   793  	logger.Tracef("Fallback fees set at %d atoms/byte", fallbackFeesPerByte)
   794  
   795  	// If set in the user config, the fee rate limit will be in units of DCR/KB.
   796  	// Convert to atoms/byte & error if value is smaller than smallest unit.
   797  	feesLimitPerByte := uint64(defaultFeeRateLimit)
   798  	if dcrCfg.FeeRateLimit > 0 {
   799  		feesLimitPerByte = toAtoms(dcrCfg.FeeRateLimit / 1000)
   800  		if feesLimitPerByte == 0 {
   801  			return nil, fmt.Errorf("Fee rate limit is smaller than smallest unit: %v",
   802  				dcrCfg.FeeRateLimit)
   803  		}
   804  	}
   805  	logger.Tracef("Fees rate limit set at %d atoms/byte", feesLimitPerByte)
   806  
   807  	redeemConfTarget := dcrCfg.RedeemConfTarget
   808  	if redeemConfTarget == 0 {
   809  		redeemConfTarget = defaultRedeemConfTarget
   810  	}
   811  	logger.Tracef("Redeem conf target set to %d blocks", redeemConfTarget)
   812  
   813  	return &exchangeWalletConfig{
   814  		fallbackFeeRate:  fallbackFeesPerByte,
   815  		feeRateLimit:     feesLimitPerByte,
   816  		redeemConfTarget: redeemConfTarget,
   817  		useSplitTx:       dcrCfg.UseSplitTx,
   818  		apiFeeFallback:   dcrCfg.ApiFeeFallback,
   819  	}, nil
   820  }
   821  
   822  // unconnectedWallet returns an ExchangeWallet without a base wallet. The wallet
   823  // should be set before use.
   824  func unconnectedWallet(cfg *asset.WalletConfig, dcrCfg *walletConfig, chainParams *chaincfg.Params, logger dex.Logger, network dex.Network) (*ExchangeWallet, error) {
   825  	walletCfg, err := getExchangeWalletCfg(dcrCfg, logger)
   826  	if err != nil {
   827  		return nil, err
   828  	}
   829  
   830  	dir := filepath.Join(cfg.DataDir, chainParams.Name)
   831  	if err := os.MkdirAll(dir, 0755); err != nil {
   832  		return nil, fmt.Errorf("unable to create wallet dir: %v", err)
   833  	}
   834  
   835  	vspFilepath := filepath.Join(dir, vspFileName)
   836  
   837  	w := &ExchangeWallet{
   838  		log:                 logger,
   839  		chainParams:         chainParams,
   840  		network:             network,
   841  		emit:                cfg.Emit,
   842  		peersChange:         cfg.PeersChange,
   843  		fundingCoins:        make(map[outPoint]*fundingCoin),
   844  		findRedemptionQueue: make(map[outPoint]*findRedemptionReq),
   845  		externalTxCache:     make(map[chainhash.Hash]*externalTx),
   846  		oracleFees:          make(map[uint64]feeStamped),
   847  		mempoolRedeems:      make(map[[32]byte]*mempoolRedeem),
   848  		vspFilepath:         vspFilepath,
   849  		walletType:          cfg.Type,
   850  		subsidyCache:        blockchain.NewSubsidyCache(chainParams),
   851  		pendingTxs:          make(map[chainhash.Hash]*btc.ExtendedWalletTx),
   852  		walletDir:           dir,
   853  	}
   854  
   855  	if b, err := os.ReadFile(vspFilepath); err == nil {
   856  		var v vsp
   857  		err = json.Unmarshal(b, &v)
   858  		if err != nil {
   859  			return nil, fmt.Errorf("unable to unmarshal vsp file: %v", err)
   860  		}
   861  		w.vspV.Store(&v)
   862  	} else if !errors.Is(err, os.ErrNotExist) {
   863  		return nil, fmt.Errorf("unable to read vsp file: %v", err)
   864  	}
   865  
   866  	w.cfgV.Store(walletCfg)
   867  
   868  	return w, nil
   869  }
   870  
   871  // openSPVWallet opens the previously created native SPV wallet.
   872  func openSPVWallet(dataDir string, gapLimit uint32, chainParams *chaincfg.Params, log dex.Logger) (*spvWallet, error) {
   873  	dir := filepath.Join(dataDir, chainParams.Name, "spv")
   874  	if exists, err := walletExists(dir); err != nil {
   875  		return nil, err
   876  	} else if !exists {
   877  		return nil, fmt.Errorf("wallet at %q doesn't exists", dir)
   878  	}
   879  
   880  	return &spvWallet{
   881  		dir:         dir,
   882  		chainParams: chainParams,
   883  		log:         log.SubLogger("SPV"),
   884  		blockCache: blockCache{
   885  			blocks: make(map[chainhash.Hash]*cachedBlock),
   886  		},
   887  		tipChan:  make(chan *block, 16),
   888  		gapLimit: gapLimit,
   889  	}, nil
   890  }
   891  
   892  // Info returns basic information about the wallet and asset.
   893  func (dcr *ExchangeWallet) Info() *asset.WalletInfo {
   894  	return WalletInfo
   895  }
   896  
   897  // var logup uint32
   898  
   899  // func rpclog(log dex.Logger) {
   900  // 	if atomic.CompareAndSwapUint32(&logup, 0, 1) {
   901  // 		rpcclient.UseLogger(log)
   902  // 	}
   903  // }
   904  
   905  func (dcr *ExchangeWallet) txHistoryDBPath(walletID string) string {
   906  	return filepath.Join(dcr.walletDir, fmt.Sprintf("txhistorydb-%s", walletID))
   907  }
   908  
   909  // findExistingAddressBasedTxHistoryDB finds the path of a tx history db that
   910  // was created using an address controlled by the wallet. This should only be
   911  // used for RPC wallets, as SPV wallets are able to get the first address
   912  // generated by the wallet.
   913  func (dcr *ExchangeWallet) findExistingAddressBasedTxHistoryDB() (string, error) {
   914  	dir, err := os.Open(dcr.walletDir)
   915  	if err != nil {
   916  		return "", fmt.Errorf("error opening wallet directory: %w", err)
   917  	}
   918  	defer dir.Close()
   919  
   920  	entries, err := dir.Readdir(0)
   921  	if err != nil {
   922  		return "", fmt.Errorf("error reading wallet directory: %w", err)
   923  	}
   924  
   925  	pattern := regexp.MustCompile(`^txhistorydb-(.+)$`)
   926  
   927  	for _, entry := range entries {
   928  		if !entry.IsDir() {
   929  			continue
   930  		}
   931  
   932  		match := pattern.FindStringSubmatch(entry.Name())
   933  		if match == nil {
   934  			continue
   935  		}
   936  
   937  		address := match[1]
   938  
   939  		decodedAddr, err := stdaddr.DecodeAddress(address, dcr.chainParams)
   940  		if err != nil {
   941  			continue
   942  		}
   943  		owns, err := dcr.wallet.WalletOwnsAddress(dcr.ctx, decodedAddr)
   944  		if err != nil {
   945  			continue
   946  		}
   947  		if owns {
   948  			return filepath.Join(dcr.walletDir, entry.Name()), nil
   949  		}
   950  	}
   951  
   952  	return "", nil
   953  }
   954  
   955  func (dcr *ExchangeWallet) startTxHistoryDB(ctx context.Context) (*dex.ConnectionMaster, error) {
   956  	var dbPath string
   957  	if spvWallet, ok := dcr.wallet.(*spvWallet); ok {
   958  		initialAddress, err := spvWallet.InitialAddress(ctx)
   959  		if err != nil {
   960  			return nil, err
   961  		}
   962  
   963  		dbPath = dcr.txHistoryDBPath(initialAddress)
   964  	}
   965  
   966  	if dbPath == "" {
   967  		addressPath, err := dcr.findExistingAddressBasedTxHistoryDB()
   968  		if err != nil {
   969  			return nil, err
   970  		}
   971  		if addressPath != "" {
   972  			dbPath = addressPath
   973  		}
   974  	}
   975  
   976  	if dbPath == "" {
   977  		depositAddr, err := dcr.DepositAddress()
   978  		if err != nil {
   979  			return nil, fmt.Errorf("error getting deposit address: %w", err)
   980  		}
   981  		dbPath = dcr.txHistoryDBPath(depositAddr)
   982  	}
   983  
   984  	dcr.log.Debugf("Using tx history db at %s", dbPath)
   985  
   986  	db := btc.NewBadgerTxDB(dbPath, dcr.log)
   987  	dcr.txHistoryDB.Store(db)
   988  
   989  	cm := dex.NewConnectionMaster(db)
   990  	if err := cm.ConnectOnce(ctx); err != nil {
   991  		return nil, fmt.Errorf("error connecting to tx history db: %w", err)
   992  	}
   993  
   994  	var success bool
   995  	defer func() {
   996  		if !success {
   997  			cm.Disconnect()
   998  		}
   999  	}()
  1000  
  1001  	pendingTxs, err := db.GetPendingTxs()
  1002  	if err != nil {
  1003  		return nil, fmt.Errorf("failed to load unconfirmed txs: %v", err)
  1004  	}
  1005  
  1006  	dcr.pendingTxsMtx.Lock()
  1007  	for _, tx := range pendingTxs {
  1008  		txHash, err := chainhash.NewHashFromStr(tx.ID)
  1009  		if err != nil {
  1010  			dcr.log.Errorf("Invalid txid %v from tx history db: %v", tx.ID, err)
  1011  			continue
  1012  		}
  1013  		dcr.pendingTxs[*txHash] = tx
  1014  	}
  1015  	dcr.pendingTxsMtx.Unlock()
  1016  
  1017  	lastQuery, err := db.GetLastReceiveTxQuery()
  1018  	if errors.Is(err, btc.ErrNeverQueried) {
  1019  		lastQuery = 0
  1020  	} else if err != nil {
  1021  		return nil, fmt.Errorf("failed to load last query time: %v", err)
  1022  	}
  1023  
  1024  	dcr.receiveTxLastQuery.Store(lastQuery)
  1025  
  1026  	success = true
  1027  	return cm, nil
  1028  }
  1029  
  1030  // Connect connects the wallet to the RPC server. Satisfies the dex.Connector
  1031  // interface.
  1032  func (dcr *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) {
  1033  	// rpclog(dcr.log)
  1034  	dcr.ctx = ctx
  1035  
  1036  	err := dcr.wallet.Connect(ctx)
  1037  	if err != nil {
  1038  		return nil, err
  1039  	}
  1040  
  1041  	// The wallet is connected now, so if any of the following checks
  1042  	// fails and we return with a non-nil error, we must disconnect the
  1043  	// wallet.
  1044  	// This is especially important as the wallet may be using an rpc
  1045  	// connection which was established above and if we do not disconnect,
  1046  	// subsequent reconnect attempts will be met with "websocket client
  1047  	// has already connected".
  1048  	var success bool
  1049  	defer func() {
  1050  		if !success {
  1051  			dcr.wallet.Disconnect()
  1052  		}
  1053  	}()
  1054  
  1055  	// Validate accounts early on to prevent errors later.
  1056  	for _, acct := range dcr.allAccounts() {
  1057  		if acct == "" {
  1058  			continue
  1059  		}
  1060  		_, err = dcr.wallet.AccountUnlocked(ctx, acct)
  1061  		if err != nil {
  1062  			return nil, fmt.Errorf("unexpected AccountUnlocked error for %q account: %w", acct, err)
  1063  		}
  1064  	}
  1065  
  1066  	// Initialize the best block.
  1067  	tip, err := dcr.getBestBlock(ctx)
  1068  	if err != nil {
  1069  		return nil, fmt.Errorf("error initializing best block for DCR: %w", err)
  1070  	}
  1071  	dcr.currentTip.Store(tip)
  1072  	dcr.startingBlocks.Store(uint64(tip.height))
  1073  
  1074  	dbCM, err := dcr.startTxHistoryDB(ctx)
  1075  	if err != nil {
  1076  		return nil, err
  1077  	}
  1078  
  1079  	success = true // All good, don't disconnect the wallet when this method returns.
  1080  	dcr.connected.Store(true)
  1081  
  1082  	dcr.wg.Add(1)
  1083  	go func() {
  1084  		defer dcr.wg.Done()
  1085  		defer dbCM.Disconnect()
  1086  		dcr.monitorBlocks(ctx)
  1087  		dcr.shutdown()
  1088  	}()
  1089  
  1090  	dcr.wg.Add(1)
  1091  	go func() {
  1092  		defer dcr.wg.Done()
  1093  		dcr.monitorPeers(ctx)
  1094  	}()
  1095  
  1096  	dcr.wg.Add(1)
  1097  	go func() {
  1098  		defer dcr.wg.Done()
  1099  		dcr.syncTxHistory(ctx, uint64(tip.height))
  1100  	}()
  1101  
  1102  	return &dcr.wg, nil
  1103  }
  1104  
  1105  // Reconfigure attempts to reconfigure the wallet.
  1106  func (dcr *ExchangeWallet) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, currentAddress string) (restart bool, err error) {
  1107  	dcrCfg := new(walletConfig)
  1108  	_, err = loadConfig(cfg.Settings, dcr.network, dcrCfg)
  1109  	if err != nil {
  1110  		return false, err
  1111  	}
  1112  
  1113  	restart, err = dcr.wallet.Reconfigure(ctx, cfg, dcr.network, currentAddress)
  1114  	if err != nil || restart {
  1115  		return restart, err
  1116  	}
  1117  
  1118  	exchangeWalletCfg, err := getExchangeWalletCfg(dcrCfg, dcr.log)
  1119  	if err != nil {
  1120  		return false, err
  1121  	}
  1122  	dcr.cfgV.Store(exchangeWalletCfg)
  1123  	return false, nil
  1124  }
  1125  
  1126  // depositAccount returns the account that may be used to receive funds into
  1127  // the wallet, either by a direct deposit action or via redemption or refund.
  1128  func (dcr *ExchangeWallet) depositAccount() string {
  1129  	accts := dcr.wallet.Accounts()
  1130  	if accts.UnmixedAccount != "" {
  1131  		return accts.UnmixedAccount
  1132  	}
  1133  	return accts.PrimaryAccount
  1134  }
  1135  
  1136  // fundingAccounts returns the primary account along with any configured trading
  1137  // account which may contain spendable outputs (split tx outputs or chained swap
  1138  // change).
  1139  func (dcr *ExchangeWallet) fundingAccounts() []string {
  1140  	accts := dcr.wallet.Accounts()
  1141  	if accts.UnmixedAccount == "" {
  1142  		return []string{accts.PrimaryAccount}
  1143  	}
  1144  	return []string{accts.PrimaryAccount, accts.TradingAccount}
  1145  }
  1146  
  1147  func (dcr *ExchangeWallet) allAccounts() []string {
  1148  	accts := dcr.wallet.Accounts()
  1149  	if accts.UnmixedAccount == "" {
  1150  		return []string{accts.PrimaryAccount}
  1151  	}
  1152  	return []string{accts.PrimaryAccount, accts.TradingAccount, accts.UnmixedAccount}
  1153  }
  1154  
  1155  // OwnsDepositAddress indicates if the provided address can be used to deposit
  1156  // funds into the wallet.
  1157  func (dcr *ExchangeWallet) OwnsDepositAddress(address string) (bool, error) {
  1158  	addr, err := stdaddr.DecodeAddress(address, dcr.chainParams)
  1159  	if err != nil {
  1160  		return false, err
  1161  	}
  1162  	return dcr.wallet.AccountOwnsAddress(dcr.ctx, addr, dcr.depositAccount())
  1163  }
  1164  
  1165  func (dcr *ExchangeWallet) balance() (*asset.Balance, error) {
  1166  	accts := dcr.wallet.Accounts()
  1167  
  1168  	locked, err := dcr.lockedAtoms(accts.PrimaryAccount)
  1169  	if err != nil {
  1170  		return nil, err
  1171  	}
  1172  	ab, err := dcr.wallet.AccountBalance(dcr.ctx, 0, accts.PrimaryAccount)
  1173  	if err != nil {
  1174  		return nil, err
  1175  	}
  1176  	bal := &asset.Balance{
  1177  		Available: toAtoms(ab.Spendable) - locked,
  1178  		Immature: toAtoms(ab.ImmatureCoinbaseRewards) +
  1179  			toAtoms(ab.ImmatureStakeGeneration),
  1180  		Locked: locked + toAtoms(ab.LockedByTickets),
  1181  		Other:  make(map[asset.BalanceCategory]asset.CustomBalance),
  1182  	}
  1183  
  1184  	bal.Other[asset.BalanceCategoryStaked] = asset.CustomBalance{
  1185  		Amount: toAtoms(ab.LockedByTickets),
  1186  	}
  1187  
  1188  	if accts.UnmixedAccount == "" {
  1189  		return bal, nil
  1190  	}
  1191  
  1192  	// Mixing is enabled, consider ...
  1193  	// 1) trading account spendable (-locked) as available,
  1194  	// 2) all unmixed funds as immature, and
  1195  	// 3) all locked utxos in the trading account as locked (for swapping).
  1196  	tradingAcctBal, err := dcr.wallet.AccountBalance(dcr.ctx, 0, accts.TradingAccount)
  1197  	if err != nil {
  1198  		return nil, err
  1199  	}
  1200  	tradingAcctLocked, err := dcr.lockedAtoms(accts.TradingAccount)
  1201  	if err != nil {
  1202  		return nil, err
  1203  	}
  1204  	unmixedAcctBal, err := dcr.wallet.AccountBalance(dcr.ctx, 0, accts.UnmixedAccount)
  1205  	if err != nil {
  1206  		return nil, err
  1207  	}
  1208  
  1209  	bal.Available += toAtoms(tradingAcctBal.Spendable) - tradingAcctLocked
  1210  	bal.Immature += toAtoms(unmixedAcctBal.Total)
  1211  	bal.Locked += tradingAcctLocked
  1212  
  1213  	bal.Other[asset.BalanceCategoryUnmixed] = asset.CustomBalance{
  1214  		Amount: toAtoms(unmixedAcctBal.Total),
  1215  	}
  1216  
  1217  	return bal, nil
  1218  }
  1219  
  1220  // Balance should return the total available funds in the wallet.
  1221  func (dcr *ExchangeWallet) Balance() (*asset.Balance, error) {
  1222  	bal, err := dcr.balance()
  1223  	if err != nil {
  1224  		return nil, err
  1225  	}
  1226  
  1227  	reserves := dcr.bondReserves.Load()
  1228  	if reserves > bal.Available { // unmixed (immature) probably needs to trickle in
  1229  		dcr.log.Warnf("Available balance is below configured reserves: %f < %f",
  1230  			toDCR(bal.Available), toDCR(reserves))
  1231  		bal.ReservesDeficit = reserves - bal.Available
  1232  		reserves = bal.Available
  1233  	}
  1234  
  1235  	bal.BondReserves = reserves
  1236  	bal.Available -= reserves
  1237  	bal.Locked += reserves
  1238  
  1239  	return bal, nil
  1240  }
  1241  
  1242  func bondsFeeBuffer(highFeeRate uint64) uint64 {
  1243  	const inputCount uint64 = 12 // plan for lots of inputs
  1244  	largeBondTxSize := dexdcr.MsgTxOverhead + dexdcr.P2SHOutputSize + 1 + dexdcr.BondPushDataSize +
  1245  		dexdcr.P2PKHOutputSize + inputCount*dexdcr.P2PKHInputSize
  1246  	// Normally we can plan on just 2 parallel "tracks" (single bond overlap
  1247  	// when bonds are expired and waiting to refund) but that may increase
  1248  	// temporarily if target tier is adjusted up.
  1249  	const parallelTracks uint64 = 4
  1250  	return parallelTracks * largeBondTxSize * highFeeRate
  1251  }
  1252  
  1253  // BondsFeeBuffer suggests how much extra may be required for the transaction
  1254  // fees part of required bond reserves when bond rotation is enabled.
  1255  func (dcr *ExchangeWallet) BondsFeeBuffer(feeRate uint64) uint64 {
  1256  	if feeRate == 0 {
  1257  		feeRate = dcr.targetFeeRateWithFallback(2, 0)
  1258  	}
  1259  	feeRate *= 2 // double the current live fee rate estimate
  1260  	return bondsFeeBuffer(feeRate)
  1261  }
  1262  
  1263  func (dcr *ExchangeWallet) SetBondReserves(reserves uint64) {
  1264  	dcr.bondReserves.Store(reserves)
  1265  }
  1266  
  1267  // FeeRate satisfies asset.FeeRater.
  1268  func (dcr *ExchangeWallet) FeeRate() uint64 {
  1269  	const confTarget = 2 // 1 historically gives crazy rates
  1270  	rate, err := dcr.feeRate(confTarget)
  1271  	if err != nil && dcr.network != dex.Simnet { // log and return 0
  1272  		dcr.log.Errorf("feeRate error: %v", err)
  1273  	}
  1274  	return rate
  1275  }
  1276  
  1277  // feeRate returns the current optimal fee rate in atoms / byte.
  1278  func (dcr *ExchangeWallet) feeRate(confTarget uint64) (uint64, error) {
  1279  	if dcr.ctx == nil {
  1280  		return 0, errors.New("not connected")
  1281  	}
  1282  	if feeEstimator, is := dcr.wallet.(FeeRateEstimator); is && !dcr.wallet.SpvMode() {
  1283  		dcrPerKB, err := feeEstimator.EstimateSmartFeeRate(dcr.ctx, int64(confTarget), chainjson.EstimateSmartFeeConservative)
  1284  		if err == nil && dcrPerKB > 0 {
  1285  			return dcrPerKBToAtomsPerByte(dcrPerKB)
  1286  		}
  1287  		if err != nil {
  1288  			dcr.log.Warnf("Failed to get local fee rate estimate: %v", err)
  1289  		} else { // dcrPerKB == 0
  1290  			dcr.log.Warnf("Local fee estimate is zero.")
  1291  		}
  1292  	}
  1293  
  1294  	cfg := dcr.config()
  1295  
  1296  	// Either SPV wallet or EstimateSmartFeeRate failed.
  1297  	if !cfg.apiFeeFallback {
  1298  		return 0, fmt.Errorf("fee rate estimation unavailable and external API is disabled")
  1299  	}
  1300  
  1301  	now := time.Now()
  1302  
  1303  	dcr.oracleFeesMtx.Lock()
  1304  	defer dcr.oracleFeesMtx.Unlock()
  1305  	oracleFee := dcr.oracleFees[confTarget]
  1306  	if now.Sub(oracleFee.stamp) < freshFeeAge {
  1307  		return oracleFee.rate, nil
  1308  	}
  1309  	if dcr.oracleFailing {
  1310  		return 0, errors.New("fee rate oracle is in a temporary failing state")
  1311  	}
  1312  
  1313  	dcr.log.Tracef("Retrieving fee rate from external fee oracle for %d target blocks", confTarget)
  1314  	dcrPerKB, err := fetchFeeFromOracle(dcr.ctx, dcr.network, confTarget)
  1315  	if err != nil {
  1316  		// Just log it and return zero. If we return an error, it's just logged
  1317  		// anyway, and we want to meter these logs.
  1318  		dcr.log.Meter("feeRate.fetch.fail", time.Hour).Errorf("external fee rate API failure: %v", err)
  1319  		// Flag the oracle as failing so subsequent requests don't also try and
  1320  		// fail after the request timeout. Remove the flag after a bit.
  1321  		dcr.oracleFailing = true
  1322  		time.AfterFunc(freshFeeAge, func() {
  1323  			dcr.oracleFeesMtx.Lock()
  1324  			dcr.oracleFailing = false
  1325  			dcr.oracleFeesMtx.Unlock()
  1326  		})
  1327  		return 0, nil
  1328  	}
  1329  	if dcrPerKB <= 0 {
  1330  		return 0, fmt.Errorf("invalid fee rate %f from fee oracle", dcrPerKB)
  1331  	}
  1332  	// Convert to atoms/B and error if it is greater than fee rate limit.
  1333  	atomsPerByte, err := dcrPerKBToAtomsPerByte(dcrPerKB)
  1334  	if err != nil {
  1335  		return 0, err
  1336  	}
  1337  	if atomsPerByte > cfg.feeRateLimit {
  1338  		return 0, fmt.Errorf("fee rate from external API greater than fee rate limit: %v > %v",
  1339  			atomsPerByte, cfg.feeRateLimit)
  1340  	}
  1341  	dcr.oracleFees[confTarget] = feeStamped{atomsPerByte, now}
  1342  	return atomsPerByte, nil
  1343  }
  1344  
  1345  // dcrPerKBToAtomsPerByte converts a estimated feeRate from dcr/KB to atoms/B.
  1346  func dcrPerKBToAtomsPerByte(dcrPerkB float64) (uint64, error) {
  1347  	// The caller should check for non-positive numbers, but don't allow
  1348  	// underflow when converting to an unsigned integer.
  1349  	if dcrPerkB < 0 {
  1350  		return 0, fmt.Errorf("negative fee rate")
  1351  	}
  1352  	// dcrPerkB * 1e8 / 1e3 => atomsPerB
  1353  	atomsPerKB, err := dcrutil.NewAmount(dcrPerkB)
  1354  	if err != nil {
  1355  		return 0, err
  1356  	}
  1357  	return uint64(dex.IntDivUp(int64(atomsPerKB), 1000)), nil
  1358  }
  1359  
  1360  // fetchFeeFromOracle gets the fee rate from the external API.
  1361  func fetchFeeFromOracle(ctx context.Context, net dex.Network, nb uint64) (float64, error) {
  1362  	var uri string
  1363  	if net == dex.Testnet {
  1364  		uri = fmt.Sprintf("https://testnet.dcrdata.org/insight/api/utils/estimatefee?nbBlocks=%d", nb)
  1365  	} else { // mainnet and simnet
  1366  		uri = fmt.Sprintf("https://explorer.dcrdata.org/insight/api/utils/estimatefee?nbBlocks=%d", nb)
  1367  	}
  1368  	ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
  1369  	defer cancel()
  1370  	var resp map[uint64]float64
  1371  	if err := dexnet.Get(ctx, uri, &resp); err != nil {
  1372  		return 0, err
  1373  	}
  1374  	if resp == nil {
  1375  		return 0, errors.New("null response")
  1376  	}
  1377  	dcrPerKB, ok := resp[nb]
  1378  	if !ok {
  1379  		return 0, errors.New("no fee rate for requested number of blocks")
  1380  	}
  1381  	return dcrPerKB, nil
  1382  }
  1383  
  1384  // targetFeeRateWithFallback attempts to get a fresh fee rate for the target
  1385  // number of confirmations, but falls back to the suggestion or fallbackFeeRate
  1386  // via feeRateWithFallback.
  1387  func (dcr *ExchangeWallet) targetFeeRateWithFallback(confTarget, feeSuggestion uint64) uint64 {
  1388  	feeRate, err := dcr.feeRate(confTarget)
  1389  	if err != nil {
  1390  		dcr.log.Errorf("Failed to get fee rate: %v", err)
  1391  	} else if feeRate != 0 {
  1392  		dcr.log.Tracef("Obtained estimate for %d-conf fee rate, %d", confTarget, feeRate)
  1393  		return feeRate
  1394  	}
  1395  
  1396  	return dcr.feeRateWithFallback(feeSuggestion)
  1397  }
  1398  
  1399  // feeRateWithFallback filters the suggested fee rate by ensuring it is within
  1400  // limits. If not, the configured fallbackFeeRate is returned and a warning
  1401  // logged.
  1402  func (dcr *ExchangeWallet) feeRateWithFallback(feeSuggestion uint64) uint64 {
  1403  	cfg := dcr.config()
  1404  
  1405  	if feeSuggestion > 0 && feeSuggestion < cfg.feeRateLimit {
  1406  		dcr.log.Tracef("Using caller's suggestion for fee rate, %d", feeSuggestion)
  1407  		return feeSuggestion
  1408  	}
  1409  	dcr.log.Warnf("No usable fee rate suggestion. Using fallback of %d", cfg.fallbackFeeRate)
  1410  	return cfg.fallbackFeeRate
  1411  }
  1412  
  1413  type amount uint64
  1414  
  1415  func (a amount) String() string {
  1416  	return strconv.FormatFloat(dcrutil.Amount(a).ToCoin(), 'f', -1, 64) // dec, but no trailing zeros
  1417  }
  1418  
  1419  // MaxOrder generates information about the maximum order size and associated
  1420  // fees that the wallet can support for the given DEX configuration. The
  1421  // provided FeeSuggestion is used directly, and should be an estimate based on
  1422  // current network conditions. For quote assets, the caller will have to
  1423  // calculate lotSize based on a rate conversion from the base asset's lot size.
  1424  // lotSize must not be zero and will cause a panic if so.
  1425  func (dcr *ExchangeWallet) MaxOrder(ord *asset.MaxOrderForm) (*asset.SwapEstimate, error) {
  1426  	_, est, err := dcr.maxOrder(ord.LotSize, ord.FeeSuggestion, ord.MaxFeeRate)
  1427  	return est, err
  1428  }
  1429  
  1430  // maxOrder gets the estimate for MaxOrder, and also returns the
  1431  // []*compositeUTXO and network fee rate to be used for further order estimation
  1432  // without additional calls to listunspent.
  1433  func (dcr *ExchangeWallet) maxOrder(lotSize, feeSuggestion, maxFeeRate uint64) (utxos []*compositeUTXO, est *asset.SwapEstimate, err error) {
  1434  	if lotSize == 0 {
  1435  		return nil, nil, errors.New("cannot divide by lotSize zero")
  1436  	}
  1437  
  1438  	utxos, err = dcr.spendableUTXOs()
  1439  	if err != nil {
  1440  		return nil, nil, fmt.Errorf("error parsing unspent outputs: %w", err)
  1441  	}
  1442  	avail := sumUTXOs(utxos)
  1443  
  1444  	// Start by attempting max lots with a basic fee.
  1445  	basicFee := dexdcr.InitTxSize * maxFeeRate
  1446  	// NOTE: Split tx is an order-time option. The max order is generally
  1447  	// attainable when split is used, regardless of whether they choose it on
  1448  	// the order form. Allow the split for max order purposes.
  1449  	trySplitTx := true
  1450  
  1451  	// Find the max lots we can fund.
  1452  	maxLotsInt := int(avail / (lotSize + basicFee))
  1453  	oneLotTooMany := sort.Search(maxLotsInt+1, func(lots int) bool {
  1454  		_, _, _, err = dcr.estimateSwap(uint64(lots), lotSize, feeSuggestion, maxFeeRate, utxos, trySplitTx, 1.0)
  1455  		// The only failure mode of estimateSwap -> dcr.fund is when there is
  1456  		// not enough funds.
  1457  		return err != nil
  1458  	})
  1459  
  1460  	maxLots := uint64(oneLotTooMany - 1)
  1461  	if oneLotTooMany == 0 {
  1462  		maxLots = 0
  1463  	}
  1464  
  1465  	if maxLots > 0 {
  1466  		est, _, _, err = dcr.estimateSwap(maxLots, lotSize, feeSuggestion, maxFeeRate, utxos, trySplitTx, 1.0)
  1467  		return utxos, est, err
  1468  	}
  1469  
  1470  	return nil, &asset.SwapEstimate{
  1471  		FeeReservesPerLot: basicFee,
  1472  	}, nil
  1473  }
  1474  
  1475  // estimateSwap prepares an *asset.SwapEstimate.
  1476  func (dcr *ExchangeWallet) estimateSwap(lots, lotSize, feeSuggestion, maxFeeRate uint64, utxos []*compositeUTXO,
  1477  	trySplit bool, feeBump float64) (*asset.SwapEstimate, bool /*split used*/, uint64 /* locked */, error) {
  1478  	// If there is a fee bump, the networkFeeRate can be higher than the
  1479  	// MaxFeeRate
  1480  	bumpedMaxRate := maxFeeRate
  1481  	bumpedNetRate := feeSuggestion
  1482  	if feeBump > 1 {
  1483  		bumpedMaxRate = uint64(math.Ceil(float64(bumpedMaxRate) * feeBump))
  1484  		bumpedNetRate = uint64(math.Ceil(float64(bumpedNetRate) * feeBump))
  1485  	}
  1486  
  1487  	feeReservesPerLot := bumpedMaxRate * dexdcr.InitTxSize
  1488  
  1489  	val := lots * lotSize
  1490  	// The orderEnough func does not account for a split transaction at the
  1491  	// start, so it is possible that funding for trySplit would actually choose
  1492  	// more UTXOs. Actual order funding accounts for this. For this estimate, we
  1493  	// will just not use a split tx if the split-adjusted required funds exceeds
  1494  	// the total value of the UTXO selected with this enough closure.
  1495  	sum, _, inputsSize, _, _, _, err := tryFund(utxos, orderEnough(val, lots, bumpedMaxRate, trySplit))
  1496  	if err != nil {
  1497  		return nil, false, 0, err
  1498  	}
  1499  
  1500  	avail := sumUTXOs(utxos)
  1501  	reserves := dcr.bondReserves.Load()
  1502  
  1503  	digestInputs := func(inputsSize uint32) (reqFunds, maxFees, estHighFees, estLowFees uint64) {
  1504  		// NOTE: reqFunds = val + fees, so change (extra) will be sum-reqFunds
  1505  		reqFunds = calc.RequiredOrderFunds(val, uint64(inputsSize), lots,
  1506  			dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedMaxRate) // as in tryFund's enough func
  1507  		maxFees = reqFunds - val
  1508  
  1509  		estHighFunds := calc.RequiredOrderFunds(val, uint64(inputsSize), lots,
  1510  			dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedNetRate)
  1511  		estHighFees = estHighFunds - val
  1512  
  1513  		estLowFunds := calc.RequiredOrderFunds(val, uint64(inputsSize), 1,
  1514  			dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedNetRate) // best means single multi-lot match, even better than batch
  1515  		estLowFees = estLowFunds - val
  1516  		return
  1517  	}
  1518  
  1519  	reqFunds, maxFees, estHighFees, estLowFees := digestInputs(inputsSize)
  1520  
  1521  	// Math for split transactions is a little different.
  1522  	if trySplit {
  1523  		splitMaxFees := splitTxBaggage * bumpedMaxRate
  1524  		splitFees := splitTxBaggage * bumpedNetRate
  1525  		reqTotal := reqFunds + splitMaxFees // ~ rather than actually fund()ing again
  1526  		// We must consider splitMaxFees otherwise we'd skip the split on
  1527  		// account of excess baggage.
  1528  		if reqTotal <= sum && sum-reqTotal >= reserves { // avail-sum+extra > reserves
  1529  			return &asset.SwapEstimate{
  1530  				Lots:               lots,
  1531  				Value:              val,
  1532  				MaxFees:            maxFees + splitMaxFees,
  1533  				RealisticBestCase:  estLowFees + splitFees,
  1534  				RealisticWorstCase: estHighFees + splitFees,
  1535  				FeeReservesPerLot:  feeReservesPerLot,
  1536  			}, true, reqFunds, nil // requires reqTotal, but locks reqFunds in the split output
  1537  		}
  1538  	}
  1539  
  1540  	if sum > avail-reserves { // no split means no change available for reserves
  1541  		if trySplit { // if we already tried with a split, that's the best we can do
  1542  			return nil, false, 0, errors.New("balance too low to both fund order and maintain bond reserves")
  1543  		}
  1544  		// Like the fund() method, try with some utxos taken out of the mix for
  1545  		// reserves, as precise in value as possible.
  1546  		kept := leastOverFund(reserveEnough(reserves), utxos)
  1547  		utxos = utxoSetDiff(utxos, kept)
  1548  		sum, _, inputsSize, _, _, _, err = tryFund(utxos, orderEnough(val, lots, bumpedMaxRate, false))
  1549  		if err != nil { // no joy with the reduced set
  1550  			return nil, false, 0, err
  1551  		}
  1552  		_, maxFees, estHighFees, estLowFees = digestInputs(inputsSize)
  1553  	}
  1554  
  1555  	// No split transaction.
  1556  	return &asset.SwapEstimate{
  1557  		Lots:               lots,
  1558  		Value:              val,
  1559  		MaxFees:            maxFees,
  1560  		RealisticBestCase:  estLowFees,
  1561  		RealisticWorstCase: estHighFees,
  1562  		FeeReservesPerLot:  feeReservesPerLot,
  1563  	}, false, sum, nil
  1564  }
  1565  
  1566  // PreSwap get order estimates based on the available funds and the wallet
  1567  // configuration.
  1568  func (dcr *ExchangeWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) {
  1569  	// Start with the maxOrder at the default configuration. This gets us the
  1570  	// utxo set, the network fee rate, and the wallet's maximum order size.
  1571  	// The utxo set can then be used repeatedly in estimateSwap at virtually
  1572  	// zero cost since there are no more RPC calls.
  1573  	// The utxo set is only used once right now, but when order-time options are
  1574  	// implemented, the utxos will be used to calculate option availability and
  1575  	// fees.
  1576  	utxos, maxEst, err := dcr.maxOrder(req.LotSize, req.FeeSuggestion, req.MaxFeeRate)
  1577  	if err != nil {
  1578  		return nil, err
  1579  	}
  1580  	if maxEst.Lots < req.Lots { // changing options isn't going to fix this, only lots
  1581  		return nil, fmt.Errorf("%d lots available for %d-lot order", maxEst.Lots, req.Lots)
  1582  	}
  1583  
  1584  	// Load the user's selected order-time options.
  1585  	customCfg := new(swapOptions)
  1586  	err = config.Unmapify(req.SelectedOptions, customCfg)
  1587  	if err != nil {
  1588  		return nil, fmt.Errorf("error parsing selected swap options: %w", err)
  1589  	}
  1590  
  1591  	// Parse the configured split transaction.
  1592  	cfg := dcr.config()
  1593  	split := cfg.useSplitTx
  1594  	if customCfg.Split != nil {
  1595  		split = *customCfg.Split
  1596  	}
  1597  
  1598  	// Parse the configured fee bump.
  1599  	bump, err := customCfg.feeBump()
  1600  	if err != nil {
  1601  		return nil, err
  1602  	}
  1603  
  1604  	// Get the estimate for the requested number of lots.
  1605  	est, _, _, err := dcr.estimateSwap(req.Lots, req.LotSize, req.FeeSuggestion,
  1606  		req.MaxFeeRate, utxos, split, bump)
  1607  	if err != nil {
  1608  		dcr.log.Warnf("estimateSwap failure: %v", err)
  1609  	}
  1610  
  1611  	// Always offer the split option, even for non-standing orders since
  1612  	// immediately spendable change many be desirable regardless.
  1613  	opts := []*asset.OrderOption{dcr.splitOption(req, utxos, bump)}
  1614  
  1615  	// Figure out what our maximum available fee bump is, within our 2x hard
  1616  	// limit.
  1617  	var maxBump float64
  1618  	var maxBumpEst *asset.SwapEstimate
  1619  	for maxBump = 2.0; maxBump > 1.01; maxBump -= 0.1 {
  1620  		if est == nil {
  1621  			break
  1622  		}
  1623  		tryEst, splitUsed, _, err := dcr.estimateSwap(req.Lots, req.LotSize,
  1624  			req.FeeSuggestion, req.MaxFeeRate, utxos, split, maxBump)
  1625  		// If the split used wasn't the configured value, this option is not
  1626  		// available.
  1627  		if err == nil && split == splitUsed {
  1628  			maxBumpEst = tryEst
  1629  			break
  1630  		}
  1631  	}
  1632  
  1633  	if maxBumpEst != nil {
  1634  		noBumpEst, _, _, err := dcr.estimateSwap(req.Lots, req.LotSize, req.FeeSuggestion,
  1635  			req.MaxFeeRate, utxos, split, 1.0)
  1636  		if err != nil {
  1637  			// shouldn't be possible, since we already succeeded with a higher bump.
  1638  			return nil, fmt.Errorf("error getting no-bump estimate: %w", err)
  1639  		}
  1640  
  1641  		bumpLabel := "2X"
  1642  		if maxBump < 2.0 {
  1643  			bumpLabel = strconv.FormatFloat(maxBump, 'f', 1, 64) + "X"
  1644  		}
  1645  
  1646  		extraFees := maxBumpEst.RealisticWorstCase - noBumpEst.RealisticWorstCase
  1647  		desc := fmt.Sprintf("Add a fee multiplier up to %.1fx (up to ~%s DCR more) for faster settlement when network traffic is high.",
  1648  			maxBump, amount(extraFees))
  1649  
  1650  		opts = append(opts, &asset.OrderOption{
  1651  			ConfigOption: asset.ConfigOption{
  1652  				Key:          swapFeeBumpKey,
  1653  				DisplayName:  "Faster Swaps",
  1654  				Description:  desc,
  1655  				DefaultValue: 1.0,
  1656  			},
  1657  			XYRange: &asset.XYRange{
  1658  				Start: asset.XYRangePoint{
  1659  					Label: "1X",
  1660  					X:     1.0,
  1661  					Y:     float64(req.FeeSuggestion),
  1662  				},
  1663  				End: asset.XYRangePoint{
  1664  					Label: bumpLabel,
  1665  					X:     maxBump,
  1666  					Y:     float64(req.FeeSuggestion) * maxBump,
  1667  				},
  1668  				XUnit: "X",
  1669  				YUnit: "atoms/B",
  1670  			},
  1671  		})
  1672  	}
  1673  
  1674  	return &asset.PreSwap{
  1675  		Estimate: est, // may be nil so we can present options, which in turn affect estimate feasibility
  1676  		Options:  opts,
  1677  	}, nil
  1678  }
  1679  
  1680  // SingleLotSwapRefundFees returns the fees for a swap and refund transaction
  1681  // for a single lot.
  1682  func (dcr *ExchangeWallet) SingleLotSwapRefundFees(_ uint32, feeSuggestion uint64, useSafeTxSize bool) (swapFees uint64, refundFees uint64, err error) {
  1683  	var numInputs uint64
  1684  	if useSafeTxSize {
  1685  		numInputs = 12
  1686  	} else {
  1687  		numInputs = 2
  1688  	}
  1689  	swapTxSize := dexdcr.InitTxSizeBase + (numInputs * dexdcr.P2PKHInputSize)
  1690  	refundTxSize := dexdcr.MsgTxOverhead + dexdcr.TxInOverhead + dexdcr.RefundSigScriptSize + dexdcr.P2PKHOutputSize
  1691  	return swapTxSize * feeSuggestion, uint64(refundTxSize) * feeSuggestion, nil
  1692  }
  1693  
  1694  // MaxFundingFees returns the maximum funding fees for an order/multi-order.
  1695  func (dcr *ExchangeWallet) MaxFundingFees(numTrades uint32, feeRate uint64, options map[string]string) uint64 {
  1696  	customCfg, err := decodeFundMultiOptions(options)
  1697  	if err != nil {
  1698  		dcr.log.Errorf("Error decoding multi-fund settings: %v", err)
  1699  		return 0
  1700  	}
  1701  	if !customCfg.Split {
  1702  		return 0
  1703  	}
  1704  
  1705  	const numInputs = 12 // plan for lots of inputs to get a safe estimate
  1706  	splitTxSize := dexdcr.MsgTxOverhead + (numInputs * dexdcr.P2PKHInputSize) + (uint64(numTrades+1) * dexdcr.P2PKHOutputSize)
  1707  	return splitTxSize * dcr.config().feeRateLimit
  1708  }
  1709  
  1710  // splitOption constructs an *asset.OrderOption with customized text based on the
  1711  // difference in fees between the configured and test split condition.
  1712  func (dcr *ExchangeWallet) splitOption(req *asset.PreSwapForm, utxos []*compositeUTXO, bump float64) *asset.OrderOption {
  1713  	opt := &asset.OrderOption{
  1714  		ConfigOption: asset.ConfigOption{
  1715  			Key:           splitKey,
  1716  			DisplayName:   "Pre-size Funds",
  1717  			IsBoolean:     true,
  1718  			DefaultValue:  dcr.config().useSplitTx, // not nil interface
  1719  			ShowByDefault: true,
  1720  		},
  1721  		Boolean: &asset.BooleanConfig{},
  1722  	}
  1723  
  1724  	noSplitEst, _, noSplitLocked, err := dcr.estimateSwap(req.Lots, req.LotSize,
  1725  		req.FeeSuggestion, req.MaxFeeRate, utxos, false, bump)
  1726  	if err != nil {
  1727  		dcr.log.Errorf("estimateSwap (no split) error: %v", err)
  1728  		opt.Boolean.Reason = fmt.Sprintf("estimate without a split failed with \"%v\"", err)
  1729  		return opt // utility and overlock report unavailable, but show the option
  1730  	}
  1731  	splitEst, splitUsed, splitLocked, err := dcr.estimateSwap(req.Lots, req.LotSize,
  1732  		req.FeeSuggestion, req.MaxFeeRate, utxos, true, bump)
  1733  	if err != nil {
  1734  		dcr.log.Errorf("estimateSwap (with split) error: %v", err)
  1735  		opt.Boolean.Reason = fmt.Sprintf("estimate with a split failed with \"%v\"", err)
  1736  		return opt // utility and overlock report unavailable, but show the option
  1737  	}
  1738  
  1739  	if !splitUsed || splitLocked >= noSplitLocked { // locked check should be redundant
  1740  		opt.Boolean.Reason = "avoids no DCR overlock for this order (ignored)"
  1741  		opt.Description = "A split transaction for this order avoids no DCR overlock, but adds additional fees."
  1742  		opt.DefaultValue = false
  1743  		return opt // not enabled by default, but explain why
  1744  	}
  1745  
  1746  	overlock := noSplitLocked - splitLocked
  1747  	pctChange := (float64(splitEst.RealisticWorstCase)/float64(noSplitEst.RealisticWorstCase) - 1) * 100
  1748  	if pctChange > 1 {
  1749  		opt.Boolean.Reason = fmt.Sprintf("+%d%% fees, avoids %s DCR overlock", int(math.Round(pctChange)), amount(overlock))
  1750  	} else {
  1751  		opt.Boolean.Reason = fmt.Sprintf("+%.1f%% fees, avoids %s DCR overlock", pctChange, amount(overlock))
  1752  	}
  1753  
  1754  	xtraFees := splitEst.RealisticWorstCase - noSplitEst.RealisticWorstCase
  1755  	opt.Description = fmt.Sprintf("Using a split transaction may prevent temporary overlock of %s DCR, but for additional fees of %s DCR",
  1756  		amount(overlock), amount(xtraFees))
  1757  
  1758  	return opt
  1759  }
  1760  
  1761  func (dcr *ExchangeWallet) preRedeem(numLots, feeSuggestion uint64, options map[string]string) (*asset.PreRedeem, error) {
  1762  	cfg := dcr.config()
  1763  
  1764  	feeRate := feeSuggestion
  1765  	if feeRate == 0 { // or just document that the caller must set it?
  1766  		feeRate = dcr.targetFeeRateWithFallback(cfg.redeemConfTarget, feeSuggestion)
  1767  	}
  1768  	// Best is one transaction with req.Lots inputs and 1 output.
  1769  	var best uint64 = dexdcr.MsgTxOverhead
  1770  	// Worst is req.Lots transactions, each with one input and one output.
  1771  	var worst uint64 = dexdcr.MsgTxOverhead * numLots
  1772  	var inputSize uint64 = dexdcr.TxInOverhead + dexdcr.RedeemSwapSigScriptSize
  1773  	var outputSize uint64 = dexdcr.P2PKHOutputSize
  1774  	best += inputSize*numLots + outputSize
  1775  	worst += (inputSize + outputSize) * numLots
  1776  
  1777  	// Read the order options.
  1778  	customCfg := new(redeemOptions)
  1779  	err := config.Unmapify(options, customCfg)
  1780  	if err != nil {
  1781  		return nil, fmt.Errorf("error parsing selected options: %w", err)
  1782  	}
  1783  
  1784  	// Parse the configured fee bump.
  1785  	var currentBump float64 = 1.0
  1786  	if customCfg.FeeBump != nil {
  1787  		bump := *customCfg.FeeBump
  1788  		if bump < 1.0 || bump > 2.0 {
  1789  			return nil, fmt.Errorf("invalid fee bump: %f", bump)
  1790  		}
  1791  		currentBump = bump
  1792  	}
  1793  
  1794  	opts := []*asset.OrderOption{{
  1795  		ConfigOption: asset.ConfigOption{
  1796  			Key:          redeemFeeBumpFee,
  1797  			DisplayName:  "Faster Redemption",
  1798  			Description:  "Bump the redemption transaction fees up to 2x for faster confirmations on your redemption transaction.",
  1799  			DefaultValue: 1.0,
  1800  		},
  1801  		XYRange: &asset.XYRange{
  1802  			Start: asset.XYRangePoint{
  1803  				Label: "1X",
  1804  				X:     1.0,
  1805  				Y:     float64(feeRate),
  1806  			},
  1807  			End: asset.XYRangePoint{
  1808  				Label: "2X",
  1809  				X:     2.0,
  1810  				Y:     float64(feeRate * 2),
  1811  			},
  1812  			YUnit: "atoms/B",
  1813  			XUnit: "X",
  1814  		},
  1815  	}}
  1816  
  1817  	return &asset.PreRedeem{
  1818  		Estimate: &asset.RedeemEstimate{
  1819  			RealisticWorstCase: uint64(math.Round(float64(worst*feeRate) * currentBump)),
  1820  			RealisticBestCase:  uint64(math.Round(float64(best*feeRate) * currentBump)),
  1821  		},
  1822  		Options: opts,
  1823  	}, nil
  1824  }
  1825  
  1826  // PreRedeem generates an estimate of the range of redemption fees that could
  1827  // be assessed.
  1828  func (dcr *ExchangeWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, error) {
  1829  	return dcr.preRedeem(req.Lots, req.FeeSuggestion, req.SelectedOptions)
  1830  }
  1831  
  1832  // SingleLotRedeemFees returns the fees for a redeem transaction for a single lot.
  1833  func (dcr *ExchangeWallet) SingleLotRedeemFees(_ uint32, feeSuggestion uint64) (uint64, error) {
  1834  	preRedeem, err := dcr.preRedeem(1, feeSuggestion, nil)
  1835  	if err != nil {
  1836  		return 0, err
  1837  	}
  1838  
  1839  	dcr.log.Tracef("SingleLotRedeemFees: worst case = %d, feeSuggestion = %d", preRedeem.Estimate.RealisticWorstCase, feeSuggestion)
  1840  
  1841  	return preRedeem.Estimate.RealisticWorstCase, nil
  1842  }
  1843  
  1844  // FundOrder selects coins for use in an order. The coins will be locked, and
  1845  // will not be returned in subsequent calls to FundOrder or calculated in calls
  1846  // to Available, unless they are unlocked with ReturnCoins.
  1847  // The returned []dex.Bytes contains the redeem scripts for the selected coins.
  1848  // Equal number of coins and redeemed scripts must be returned. A nil or empty
  1849  // dex.Bytes should be appended to the redeem scripts collection for coins with
  1850  // no redeem script.
  1851  func (dcr *ExchangeWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint64, error) {
  1852  	cfg := dcr.config()
  1853  
  1854  	// Consumer checks dex asset version, so maybe this is not our job:
  1855  	// if ord.DEXConfig.Version != dcr.Info().Version {
  1856  	// 	return nil, nil, fmt.Errorf("asset version mismatch: server = %d, client = %d",
  1857  	// 		ord.DEXConfig.Version, dcr.Info().Version)
  1858  	// }
  1859  	if ord.Value == 0 {
  1860  		return nil, nil, 0, fmt.Errorf("cannot fund value = 0")
  1861  	}
  1862  	if ord.MaxSwapCount == 0 {
  1863  		return nil, nil, 0, fmt.Errorf("cannot fund a zero-lot order")
  1864  	}
  1865  	if ord.FeeSuggestion > ord.MaxFeeRate {
  1866  		return nil, nil, 0, fmt.Errorf("fee suggestion %d > max fee rate %d", ord.FeeSuggestion, ord.MaxFeeRate)
  1867  	}
  1868  	if ord.FeeSuggestion > cfg.feeRateLimit {
  1869  		return nil, nil, 0, fmt.Errorf("suggested fee > configured limit. %d > %d", ord.FeeSuggestion, cfg.feeRateLimit)
  1870  	}
  1871  	// Check wallet's fee rate limit against server's max fee rate
  1872  	if cfg.feeRateLimit < ord.MaxFeeRate {
  1873  		return nil, nil, 0, fmt.Errorf(
  1874  			"%v: server's max fee rate %v higher than configured fee rate limit %v",
  1875  			dex.BipIDSymbol(BipID), ord.MaxFeeRate, cfg.feeRateLimit)
  1876  	}
  1877  
  1878  	customCfg := new(swapOptions)
  1879  	err := config.Unmapify(ord.Options, customCfg)
  1880  	if err != nil {
  1881  		return nil, nil, 0, fmt.Errorf("Error parsing swap options")
  1882  	}
  1883  
  1884  	// Check ord.Options for a FeeBump here
  1885  	bumpedMaxRate, err := calcBumpedRate(ord.MaxFeeRate, customCfg.FeeBump)
  1886  	if err != nil {
  1887  		dcr.log.Errorf("calcBumpRate error: %v", err)
  1888  	}
  1889  
  1890  	// If a split is not requested, but is forced, create an extra output from
  1891  	// the split tx to help avoid a forced split in subsequent orders.
  1892  	var extraSplitOutput uint64
  1893  	useSplit := cfg.useSplitTx
  1894  	if customCfg.Split != nil {
  1895  		useSplit = *customCfg.Split
  1896  	}
  1897  
  1898  	changeForReserves := useSplit && dcr.wallet.Accounts().UnmixedAccount == ""
  1899  	reserves := dcr.bondReserves.Load()
  1900  	coins, redeemScripts, sum, inputsSize, err := dcr.fund(reserves,
  1901  		orderEnough(ord.Value, ord.MaxSwapCount, bumpedMaxRate, changeForReserves))
  1902  	if err != nil {
  1903  		if !changeForReserves && reserves > 0 { // split not selected, or it's a mixing account where change isn't usable
  1904  			// Force a split if funding failure may be due to reserves.
  1905  			dcr.log.Infof("Retrying order funding with a forced split transaction to help respect reserves.")
  1906  			useSplit = true
  1907  			keepForSplitToo := reserves + (bumpedMaxRate * dexdcr.P2PKHInputSize) // so we fail before split() if it's really that tight
  1908  			coins, redeemScripts, sum, inputsSize, err = dcr.fund(keepForSplitToo,
  1909  				orderEnough(ord.Value, ord.MaxSwapCount, bumpedMaxRate, useSplit))
  1910  			// And make an extra output for the reserves amount plus additional
  1911  			// fee buffer (double) to help avoid this for a while in the future.
  1912  			// This also deals with mixing wallets not having usable change.
  1913  			extraSplitOutput = reserves + bondsFeeBuffer(cfg.feeRateLimit)
  1914  		}
  1915  		if err != nil {
  1916  			return nil, nil, 0, fmt.Errorf("error funding order value of %s DCR: %w",
  1917  				amount(ord.Value), err)
  1918  		}
  1919  	}
  1920  
  1921  	// Send a split, if preferred or required.
  1922  	if useSplit {
  1923  		// We apply the bumped fee rate to the split transaction when the
  1924  		// PreSwap is created, so we use that bumped rate here too.
  1925  		// But first, check that it's within bounds.
  1926  		rawFeeRate := ord.FeeSuggestion
  1927  		if rawFeeRate == 0 {
  1928  			// TODO
  1929  			// 1.0: Error when no suggestion.
  1930  			// return nil, false, fmt.Errorf("cannot do a split transaction without a fee rate suggestion from the server")
  1931  			rawFeeRate = dcr.targetFeeRateWithFallback(cfg.redeemConfTarget, 0)
  1932  			// We PreOrder checked this as <= MaxFeeRate, so use that as an
  1933  			// upper limit.
  1934  			if rawFeeRate > ord.MaxFeeRate {
  1935  				rawFeeRate = ord.MaxFeeRate
  1936  			}
  1937  		}
  1938  		splitFeeRate, err := calcBumpedRate(rawFeeRate, customCfg.FeeBump)
  1939  		if err != nil {
  1940  			dcr.log.Errorf("calcBumpRate error: %v", err)
  1941  		}
  1942  
  1943  		splitCoins, split, fees, err := dcr.split(ord.Value, ord.MaxSwapCount, coins,
  1944  			inputsSize, splitFeeRate, bumpedMaxRate, extraSplitOutput)
  1945  		if err != nil { // potentially try again with extraSplitOutput=0 if it wasn't already
  1946  			if _, errRet := dcr.returnCoins(coins); errRet != nil {
  1947  				dcr.log.Warnf("Failed to unlock funding coins %v: %v", coins, errRet)
  1948  			}
  1949  			return nil, nil, 0, err
  1950  		}
  1951  		if split {
  1952  			return splitCoins, []dex.Bytes{nil}, fees, nil // no redeem script required for split tx output
  1953  		}
  1954  		return splitCoins, redeemScripts, 0, nil // splitCoins == coins
  1955  	}
  1956  
  1957  	dcr.log.Infof("Funding %s DCR order with coins %v worth %s", amount(ord.Value), coins, amount(sum))
  1958  
  1959  	return coins, redeemScripts, 0, nil
  1960  }
  1961  
  1962  // fundMultiOptions are the possible order options when calling FundMultiOrder.
  1963  type fundMultiOptions struct {
  1964  	// Split, if true, and multi-order cannot be funded with the existing UTXOs
  1965  	// in the wallet without going over the maxLock limit, a split transaction
  1966  	// will be created with one output per order.
  1967  	//
  1968  	// Use the multiSplitKey const defined above in the options map to set this option.
  1969  	Split bool `ini:"multisplit"`
  1970  	// SplitBuffer, if set, will instruct the wallet to add a buffer onto each
  1971  	// output of the multi-order split transaction (if the split is needed).
  1972  	// SplitBuffer is defined as a percentage of the output. If a .1 BTC output
  1973  	// is required for an order and SplitBuffer is set to 5, a .105 BTC output
  1974  	// will be created.
  1975  	//
  1976  	// The motivation for this is to assist market makers in having to do the
  1977  	// least amount of splits as possible. It is useful when DCR is the quote
  1978  	// asset on a market, and the price is increasing. During a market maker's
  1979  	// operation, it will frequently have to cancel and replace orders as the
  1980  	// rate moves. If BTC is the quote asset on a market, and the rate has
  1981  	// lightly increased, the market maker will need to lock slightly more of
  1982  	// the quote asset for the same amount of lots of the base asset. If there
  1983  	// is no split buffer, this may necessitate a new split transaction.
  1984  	//
  1985  	// Use the multiSplitBufferKey const defined above in the options map to set this.
  1986  	SplitBuffer float64 `ini:"multisplitbuffer"`
  1987  }
  1988  
  1989  func decodeFundMultiOptions(options map[string]string) (*fundMultiOptions, error) {
  1990  	opts := new(fundMultiOptions)
  1991  	return opts, config.Unmapify(options, opts)
  1992  }
  1993  
  1994  // orderWithLeastOverFund returns the index of the order from a slice of orders
  1995  // that requires the least over-funding without using more than maxLock. It
  1996  // also returns the UTXOs that were used to fund the order. If none can be
  1997  // funded without using more than maxLock, -1 is returned.
  1998  func (dcr *ExchangeWallet) orderWithLeastOverFund(maxLock, feeRate uint64, orders []*asset.MultiOrderValue, utxos []*compositeUTXO) (orderIndex int, leastOverFundingUTXOs []*compositeUTXO) {
  1999  	minOverFund := uint64(math.MaxUint64)
  2000  	orderIndex = -1
  2001  	for i, value := range orders {
  2002  		enough := orderEnough(value.Value, value.MaxSwapCount, feeRate, false)
  2003  		var fundingUTXOs []*compositeUTXO
  2004  		if maxLock > 0 {
  2005  			fundingUTXOs = leastOverFundWithLimit(enough, maxLock, utxos)
  2006  		} else {
  2007  			fundingUTXOs = leastOverFund(enough, utxos)
  2008  		}
  2009  		if len(fundingUTXOs) == 0 {
  2010  			continue
  2011  		}
  2012  		sum := sumUTXOs(fundingUTXOs)
  2013  		overFund := sum - value.Value
  2014  		if overFund < minOverFund {
  2015  			minOverFund = overFund
  2016  			orderIndex = i
  2017  			leastOverFundingUTXOs = fundingUTXOs
  2018  		}
  2019  	}
  2020  	return
  2021  }
  2022  
  2023  // fundsRequiredForMultiOrders returns an slice of the required funds for each
  2024  // of a slice of orders and the total required funds.
  2025  func (dcr *ExchangeWallet) fundsRequiredForMultiOrders(orders []*asset.MultiOrderValue, feeRate uint64, splitBuffer float64) ([]uint64, uint64) {
  2026  	requiredForOrders := make([]uint64, len(orders))
  2027  	var totalRequired uint64
  2028  
  2029  	for i, value := range orders {
  2030  		req := calc.RequiredOrderFunds(value.Value, dexdcr.P2PKHInputSize, value.MaxSwapCount,
  2031  			dexdcr.InitTxSizeBase, dexdcr.InitTxSize, feeRate)
  2032  		req = uint64(math.Round(float64(req) * (100 + splitBuffer) / 100))
  2033  		requiredForOrders[i] = req
  2034  		totalRequired += req
  2035  	}
  2036  
  2037  	return requiredForOrders, totalRequired
  2038  }
  2039  
  2040  // fundMultiBestEffort makes a best effort to fund every order. If it is not
  2041  // possible, it returns coins for the orders that could be funded. The coins
  2042  // that fund each order are returned in the same order as the values that were
  2043  // passed in. If a split is allowed and all orders cannot be funded, nil slices
  2044  // are returned.
  2045  func (dcr *ExchangeWallet) fundMultiBestEffort(keep, maxLock uint64, values []*asset.MultiOrderValue,
  2046  	maxFeeRate uint64, splitAllowed bool) ([]asset.Coins, [][]dex.Bytes, []*fundingCoin, error) {
  2047  	utxos, err := dcr.spendableUTXOs()
  2048  	if err != nil {
  2049  		return nil, nil, nil, fmt.Errorf("error getting spendable utxos: %w", err)
  2050  	}
  2051  
  2052  	var avail uint64
  2053  	for _, utxo := range utxos {
  2054  		avail += toAtoms(utxo.rpc.Amount)
  2055  	}
  2056  
  2057  	fundAllOrders := func() [][]*compositeUTXO {
  2058  		indexToFundingCoins := make(map[int][]*compositeUTXO, len(values))
  2059  		remainingUTXOs := utxos
  2060  		remainingOrders := values
  2061  		remainingIndexes := make([]int, len(values))
  2062  		for i := range remainingIndexes {
  2063  			remainingIndexes[i] = i
  2064  		}
  2065  		var totalFunded uint64
  2066  		for range values {
  2067  			orderIndex, fundingUTXOs := dcr.orderWithLeastOverFund(maxLock-totalFunded, maxFeeRate, remainingOrders, remainingUTXOs)
  2068  			if orderIndex == -1 {
  2069  				return nil
  2070  			}
  2071  			totalFunded += sumUTXOs(fundingUTXOs)
  2072  			if totalFunded > avail-keep {
  2073  				return nil
  2074  			}
  2075  			newRemainingOrders := make([]*asset.MultiOrderValue, 0, len(remainingOrders)-1)
  2076  			newRemainingIndexes := make([]int, 0, len(remainingOrders)-1)
  2077  			for j := range remainingOrders {
  2078  				if j != orderIndex {
  2079  					newRemainingOrders = append(newRemainingOrders, remainingOrders[j])
  2080  					newRemainingIndexes = append(newRemainingIndexes, remainingIndexes[j])
  2081  				}
  2082  			}
  2083  			indexToFundingCoins[remainingIndexes[orderIndex]] = fundingUTXOs
  2084  			remainingOrders = newRemainingOrders
  2085  			remainingIndexes = newRemainingIndexes
  2086  			remainingUTXOs = utxoSetDiff(remainingUTXOs, fundingUTXOs)
  2087  		}
  2088  		allFundingUTXOs := make([][]*compositeUTXO, len(values))
  2089  		for i := range values {
  2090  			allFundingUTXOs[i] = indexToFundingCoins[i]
  2091  		}
  2092  		return allFundingUTXOs
  2093  	}
  2094  
  2095  	fundInOrder := func(orderedValues []*asset.MultiOrderValue) [][]*compositeUTXO {
  2096  		allFundingUTXOs := make([][]*compositeUTXO, 0, len(orderedValues))
  2097  		remainingUTXOs := utxos
  2098  		var totalFunded uint64
  2099  		for _, value := range orderedValues {
  2100  			enough := orderEnough(value.Value, value.MaxSwapCount, maxFeeRate, false)
  2101  
  2102  			var fundingUTXOs []*compositeUTXO
  2103  			if maxLock > 0 {
  2104  				if maxLock < totalFunded {
  2105  					// Should never happen unless there is a bug in leastOverFundWithLimit
  2106  					dcr.log.Errorf("maxLock < totalFunded. %d < %d", maxLock, totalFunded)
  2107  					return allFundingUTXOs
  2108  				}
  2109  				fundingUTXOs = leastOverFundWithLimit(enough, maxLock-totalFunded, remainingUTXOs)
  2110  			} else {
  2111  				fundingUTXOs = leastOverFund(enough, remainingUTXOs)
  2112  			}
  2113  			if len(fundingUTXOs) == 0 {
  2114  				return allFundingUTXOs
  2115  			}
  2116  			totalFunded += sumUTXOs(fundingUTXOs)
  2117  			if totalFunded > avail-keep {
  2118  				return allFundingUTXOs
  2119  			}
  2120  			allFundingUTXOs = append(allFundingUTXOs, fundingUTXOs)
  2121  			remainingUTXOs = utxoSetDiff(remainingUTXOs, fundingUTXOs)
  2122  		}
  2123  		return allFundingUTXOs
  2124  	}
  2125  
  2126  	returnValues := func(allFundingUTXOs [][]*compositeUTXO) (coins []asset.Coins, redeemScripts [][]dex.Bytes, fundingCoins []*fundingCoin, err error) {
  2127  		coins = make([]asset.Coins, len(allFundingUTXOs))
  2128  		fundingCoins = make([]*fundingCoin, 0, len(allFundingUTXOs))
  2129  		redeemScripts = make([][]dex.Bytes, len(allFundingUTXOs))
  2130  		for i, fundingUTXOs := range allFundingUTXOs {
  2131  			coins[i] = make(asset.Coins, len(fundingUTXOs))
  2132  			redeemScripts[i] = make([]dex.Bytes, len(fundingUTXOs))
  2133  			for j, output := range fundingUTXOs {
  2134  				txHash, err := chainhash.NewHashFromStr(output.rpc.TxID)
  2135  				if err != nil {
  2136  					return nil, nil, nil, fmt.Errorf("error decoding txid: %w", err)
  2137  				}
  2138  				coins[i][j] = newOutput(txHash, output.rpc.Vout, toAtoms(output.rpc.Amount), output.rpc.Tree)
  2139  				fundingCoins = append(fundingCoins, &fundingCoin{
  2140  					op:   newOutput(txHash, output.rpc.Vout, toAtoms(output.rpc.Amount), output.rpc.Tree),
  2141  					addr: output.rpc.Address,
  2142  				})
  2143  				redeemScript, err := hex.DecodeString(output.rpc.RedeemScript)
  2144  				if err != nil {
  2145  					return nil, nil, nil, fmt.Errorf("error decoding redeem script for %s, script = %s: %w",
  2146  						txHash, output.rpc.RedeemScript, err)
  2147  				}
  2148  				redeemScripts[i][j] = redeemScript
  2149  			}
  2150  		}
  2151  		return
  2152  	}
  2153  
  2154  	// Attempt to fund all orders by selecting the order that requires the least
  2155  	// over funding, removing the funding utxos from the set of available utxos,
  2156  	// and continuing until all orders are funded.
  2157  	allFundingUTXOs := fundAllOrders()
  2158  	if allFundingUTXOs != nil {
  2159  		return returnValues(allFundingUTXOs)
  2160  	}
  2161  
  2162  	// Return nil if a split is allowed. There is no need to fund in priority
  2163  	// order if a split will be done regardless.
  2164  	if splitAllowed {
  2165  		return returnValues([][]*compositeUTXO{})
  2166  	}
  2167  
  2168  	// If could not fully fund, fund as much as possible in the priority
  2169  	// order.
  2170  	allFundingUTXOs = fundInOrder(values)
  2171  	return returnValues(allFundingUTXOs)
  2172  }
  2173  
  2174  // fundMultiSplitTx uses the utxos provided and attempts to fund a multi-split
  2175  // transaction to fund each of the orders. If successful, it returns the
  2176  // funding coins and outputs.
  2177  func (dcr *ExchangeWallet) fundMultiSplitTx(orders []*asset.MultiOrderValue, utxos []*compositeUTXO,
  2178  	splitTxFeeRate, maxFeeRate uint64, splitBuffer float64, keep, maxLock uint64) (bool, asset.Coins, []*fundingCoin) {
  2179  	_, totalOutputRequired := dcr.fundsRequiredForMultiOrders(orders, maxFeeRate, splitBuffer)
  2180  
  2181  	var splitTxSizeWithoutInputs uint32 = dexdcr.MsgTxOverhead
  2182  	numOutputs := len(orders)
  2183  	if keep > 0 {
  2184  		numOutputs++
  2185  	}
  2186  	splitTxSizeWithoutInputs += uint32(dexdcr.P2PKHOutputSize * numOutputs)
  2187  
  2188  	enough := func(sum uint64, size uint32, utxo *compositeUTXO) (bool, uint64) {
  2189  		totalSum := sum + toAtoms(utxo.rpc.Amount)
  2190  		totalSize := size + utxo.input.Size()
  2191  		splitTxFee := uint64(splitTxSizeWithoutInputs+totalSize) * splitTxFeeRate
  2192  		req := totalOutputRequired + splitTxFee
  2193  		return totalSum >= req, totalSum - req
  2194  	}
  2195  
  2196  	var avail uint64
  2197  	for _, utxo := range utxos {
  2198  		avail += toAtoms(utxo.rpc.Amount)
  2199  	}
  2200  
  2201  	fundSplitCoins, _, spents, _, inputsSize, err := dcr.fundInternalWithUTXOs(utxos, keep, enough, false)
  2202  	if err != nil {
  2203  		return false, nil, nil
  2204  	}
  2205  
  2206  	if maxLock > 0 {
  2207  		totalSize := inputsSize + uint64(splitTxSizeWithoutInputs)
  2208  		if totalOutputRequired+(totalSize*splitTxFeeRate) > maxLock {
  2209  			return false, nil, nil
  2210  		}
  2211  	}
  2212  
  2213  	return true, fundSplitCoins, spents
  2214  }
  2215  
  2216  // submitMultiSplitTx creates a multi-split transaction using fundingCoins with
  2217  // one output for each order, and submits it to the network.
  2218  func (dcr *ExchangeWallet) submitMultiSplitTx(fundingCoins asset.Coins, _ /* spents */ []*fundingCoin, orders []*asset.MultiOrderValue,
  2219  	maxFeeRate, splitTxFeeRate uint64, splitBuffer float64) ([]asset.Coins, uint64, error) {
  2220  	baseTx := wire.NewMsgTx()
  2221  	_, err := dcr.addInputCoins(baseTx, fundingCoins)
  2222  	if err != nil {
  2223  		return nil, 0, err
  2224  	}
  2225  
  2226  	accts := dcr.wallet.Accounts()
  2227  	getAddr := func() (stdaddr.Address, error) {
  2228  		if accts.TradingAccount != "" {
  2229  			return dcr.wallet.ExternalAddress(dcr.ctx, accts.TradingAccount)
  2230  		}
  2231  		return dcr.wallet.ExternalAddress(dcr.ctx, accts.PrimaryAccount)
  2232  	}
  2233  
  2234  	requiredForOrders, _ := dcr.fundsRequiredForMultiOrders(orders, maxFeeRate, splitBuffer)
  2235  	outputAddresses := make([]stdaddr.Address, len(orders))
  2236  	for i, req := range requiredForOrders {
  2237  		outputAddr, err := getAddr()
  2238  		if err != nil {
  2239  			return nil, 0, err
  2240  		}
  2241  		outputAddresses[i] = outputAddr
  2242  		payScriptVer, payScript := outputAddr.PaymentScript()
  2243  		txOut := newTxOut(int64(req), payScriptVer, payScript)
  2244  		baseTx.AddTxOut(txOut)
  2245  	}
  2246  
  2247  	tx, err := dcr.sendWithReturn(baseTx, splitTxFeeRate, -1)
  2248  	if err != nil {
  2249  		return nil, 0, err
  2250  	}
  2251  
  2252  	coins := make([]asset.Coins, len(orders))
  2253  	fcs := make([]*fundingCoin, len(orders))
  2254  	for i := range coins {
  2255  		coins[i] = asset.Coins{newOutput(tx.CachedTxHash(), uint32(i), uint64(tx.TxOut[i].Value), wire.TxTreeRegular)}
  2256  		fcs[i] = &fundingCoin{
  2257  			op:   newOutput(tx.CachedTxHash(), uint32(i), uint64(tx.TxOut[i].Value), wire.TxTreeRegular),
  2258  			addr: outputAddresses[i].String(),
  2259  		}
  2260  	}
  2261  	dcr.lockFundingCoins(fcs)
  2262  
  2263  	var totalOut uint64
  2264  	for _, txOut := range tx.TxOut {
  2265  		totalOut += uint64(txOut.Value)
  2266  	}
  2267  
  2268  	var totalIn uint64
  2269  	for _, txIn := range fundingCoins {
  2270  		totalIn += txIn.Value()
  2271  	}
  2272  
  2273  	txHash := tx.CachedTxHash()
  2274  	dcr.addTxToHistory(&asset.WalletTransaction{
  2275  		Type: asset.Split,
  2276  		ID:   txHash.String(),
  2277  		Fees: totalIn - totalOut,
  2278  	}, txHash, true)
  2279  
  2280  	return coins, totalIn - totalOut, nil
  2281  }
  2282  
  2283  // fundMultiWithSplit creates a split transaction to fund multiple orders. It
  2284  // attempts to fund as many of the orders as possible without a split transaction,
  2285  // and only creates a split transaction for the remaining orders. This is only
  2286  // called after it has been determined that all of the orders cannot be funded
  2287  // without a split transaction.
  2288  func (dcr *ExchangeWallet) fundMultiWithSplit(keep, maxLock uint64, values []*asset.MultiOrderValue,
  2289  	splitTxFeeRate, maxFeeRate uint64, splitBuffer float64) ([]asset.Coins, [][]dex.Bytes, uint64, error) {
  2290  	utxos, err := dcr.spendableUTXOs()
  2291  	if err != nil {
  2292  		return nil, nil, 0, fmt.Errorf("error getting spendable utxos: %w", err)
  2293  	}
  2294  
  2295  	var avail uint64
  2296  	for _, utxo := range utxos {
  2297  		avail += toAtoms(utxo.rpc.Amount)
  2298  	}
  2299  
  2300  	canFund, splitCoins, splitSpents := dcr.fundMultiSplitTx(values, utxos, splitTxFeeRate, maxFeeRate, splitBuffer, keep, maxLock)
  2301  	if !canFund {
  2302  		return nil, nil, 0, fmt.Errorf("cannot fund all with split")
  2303  	}
  2304  
  2305  	remainingUTXOs := utxos
  2306  	remainingOrders := values
  2307  
  2308  	// The return values must be in the same order as the values that were
  2309  	// passed in, so we keep track of the original indexes here.
  2310  	indexToFundingCoins := make(map[int][]*compositeUTXO, len(values))
  2311  	remainingIndexes := make([]int, len(values))
  2312  	for i := range remainingIndexes {
  2313  		remainingIndexes[i] = i
  2314  	}
  2315  
  2316  	var totalFunded uint64
  2317  
  2318  	// Find each of the orders that can be funded without being included
  2319  	// in the split transaction.
  2320  	for range values {
  2321  		// First find the order the can be funded with the least overlock.
  2322  		// If there is no order that can be funded without going over the
  2323  		// maxLock limit, or not leaving enough for bond reserves, then all
  2324  		// of the remaining orders must be funded with the split transaction.
  2325  		orderIndex, fundingUTXOs := dcr.orderWithLeastOverFund(maxLock-totalFunded, maxFeeRate, remainingOrders, remainingUTXOs)
  2326  		if orderIndex == -1 {
  2327  			break
  2328  		}
  2329  		totalFunded += sumUTXOs(fundingUTXOs)
  2330  		if totalFunded > avail-keep {
  2331  			break
  2332  		}
  2333  
  2334  		newRemainingOrders := make([]*asset.MultiOrderValue, 0, len(remainingOrders)-1)
  2335  		newRemainingIndexes := make([]int, 0, len(remainingOrders)-1)
  2336  		for j := range remainingOrders {
  2337  			if j != orderIndex {
  2338  				newRemainingOrders = append(newRemainingOrders, remainingOrders[j])
  2339  				newRemainingIndexes = append(newRemainingIndexes, remainingIndexes[j])
  2340  			}
  2341  		}
  2342  		remainingUTXOs = utxoSetDiff(remainingUTXOs, fundingUTXOs)
  2343  
  2344  		// Then we make sure that a split transaction can be created for
  2345  		// any remaining orders without using the utxos returned by
  2346  		// orderWithLeastOverFund.
  2347  		if len(newRemainingOrders) > 0 {
  2348  			canFund, newSplitCoins, newSpents := dcr.fundMultiSplitTx(newRemainingOrders, remainingUTXOs,
  2349  				splitTxFeeRate, maxFeeRate, splitBuffer, keep, maxLock-totalFunded)
  2350  			if !canFund {
  2351  				break
  2352  			}
  2353  			splitCoins = newSplitCoins
  2354  			splitSpents = newSpents
  2355  		}
  2356  
  2357  		indexToFundingCoins[remainingIndexes[orderIndex]] = fundingUTXOs
  2358  		remainingOrders = newRemainingOrders
  2359  		remainingIndexes = newRemainingIndexes
  2360  	}
  2361  
  2362  	var splitOutputCoins []asset.Coins
  2363  	var splitFees uint64
  2364  
  2365  	// This should always be true, otherwise this function would not have been
  2366  	// called.
  2367  	if len(remainingOrders) > 0 {
  2368  		splitOutputCoins, splitFees, err = dcr.submitMultiSplitTx(splitCoins,
  2369  			splitSpents, remainingOrders, maxFeeRate, splitTxFeeRate, splitBuffer)
  2370  		if err != nil {
  2371  			return nil, nil, 0, fmt.Errorf("error creating split transaction: %w", err)
  2372  		}
  2373  	}
  2374  
  2375  	coins := make([]asset.Coins, len(values))
  2376  	redeemScripts := make([][]dex.Bytes, len(values))
  2377  	spents := make([]*fundingCoin, 0, len(values))
  2378  
  2379  	var splitIndex int
  2380  
  2381  	for i := range values {
  2382  		if fundingUTXOs, ok := indexToFundingCoins[i]; ok {
  2383  			coins[i] = make(asset.Coins, len(fundingUTXOs))
  2384  			redeemScripts[i] = make([]dex.Bytes, len(fundingUTXOs))
  2385  			for j, unspent := range fundingUTXOs {
  2386  				txHash, err := chainhash.NewHashFromStr(unspent.rpc.TxID)
  2387  				if err != nil {
  2388  					return nil, nil, 0, fmt.Errorf("error decoding txid from rpc server %s: %w", unspent.rpc.TxID, err)
  2389  				}
  2390  				output := newOutput(txHash, unspent.rpc.Vout, toAtoms(unspent.rpc.Amount), unspent.rpc.Tree)
  2391  				coins[i][j] = output
  2392  				fc := &fundingCoin{
  2393  					op:   output,
  2394  					addr: unspent.rpc.Address,
  2395  				}
  2396  				spents = append(spents, fc)
  2397  				redeemScript, err := hex.DecodeString(unspent.rpc.RedeemScript)
  2398  				if err != nil {
  2399  					return nil, nil, 0, fmt.Errorf("error decoding redeem script for %s, script = %s: %w",
  2400  						txHash, unspent.rpc.RedeemScript, err)
  2401  				}
  2402  				redeemScripts[i][j] = redeemScript
  2403  			}
  2404  		} else {
  2405  			coins[i] = splitOutputCoins[splitIndex]
  2406  			redeemScripts[i] = []dex.Bytes{nil}
  2407  			splitIndex++
  2408  		}
  2409  	}
  2410  
  2411  	dcr.lockFundingCoins(spents)
  2412  
  2413  	return coins, redeemScripts, splitFees, nil
  2414  }
  2415  
  2416  // fundMulti first attempts to fund each of the orders with with the available
  2417  // UTXOs. If a split is not allowed, it will fund the orders that it was able
  2418  // to fund. If splitting is allowed, a split transaction will be created to fund
  2419  // all of the orders.
  2420  func (dcr *ExchangeWallet) fundMulti(maxLock uint64, values []*asset.MultiOrderValue, splitTxFeeRate, maxFeeRate uint64, allowSplit bool, splitBuffer float64) ([]asset.Coins, [][]dex.Bytes, uint64, error) {
  2421  	dcr.fundingMtx.Lock()
  2422  	defer dcr.fundingMtx.Unlock()
  2423  
  2424  	reserves := dcr.bondReserves.Load()
  2425  
  2426  	coins, redeemScripts, fundingCoins, err := dcr.fundMultiBestEffort(reserves, maxLock, values, maxFeeRate, allowSplit)
  2427  	if err != nil {
  2428  		return nil, nil, 0, err
  2429  	}
  2430  	if len(coins) == len(values) || !allowSplit {
  2431  		dcr.lockFundingCoins(fundingCoins)
  2432  		return coins, redeemScripts, 0, nil
  2433  	}
  2434  
  2435  	return dcr.fundMultiWithSplit(reserves, maxLock, values, splitTxFeeRate, maxFeeRate, splitBuffer)
  2436  }
  2437  
  2438  func (dcr *ExchangeWallet) FundMultiOrder(mo *asset.MultiOrder, maxLock uint64) (coins []asset.Coins, redeemScripts [][]dex.Bytes, fundingFees uint64, err error) {
  2439  	var totalRequiredForOrders uint64
  2440  	for _, value := range mo.Values {
  2441  		if value.Value == 0 {
  2442  			return nil, nil, 0, fmt.Errorf("cannot fund value = 0")
  2443  		}
  2444  		if value.MaxSwapCount == 0 {
  2445  			return nil, nil, 0, fmt.Errorf("cannot fund zero-lot order")
  2446  		}
  2447  		req := calc.RequiredOrderFunds(value.Value, dexdcr.P2PKHInputSize, value.MaxSwapCount,
  2448  			dexdcr.InitTxSizeBase, dexdcr.InitTxSize, mo.MaxFeeRate)
  2449  		totalRequiredForOrders += req
  2450  	}
  2451  
  2452  	if maxLock < totalRequiredForOrders && maxLock != 0 {
  2453  		return nil, nil, 0, fmt.Errorf("maxLock < totalRequiredForOrders (%d < %d)", maxLock, totalRequiredForOrders)
  2454  	}
  2455  
  2456  	if mo.FeeSuggestion > mo.MaxFeeRate {
  2457  		return nil, nil, 0, fmt.Errorf("fee suggestion %d > max fee rate %d", mo.FeeSuggestion, mo.MaxFeeRate)
  2458  	}
  2459  
  2460  	cfg := dcr.config()
  2461  	if cfg.feeRateLimit < mo.MaxFeeRate {
  2462  		return nil, nil, 0, fmt.Errorf(
  2463  			"%v: server's max fee rate %v higher than configured fee rate limit %v",
  2464  			dex.BipIDSymbol(BipID), mo.MaxFeeRate, cfg.feeRateLimit)
  2465  	}
  2466  
  2467  	bal, err := dcr.Balance()
  2468  	if err != nil {
  2469  		return nil, nil, 0, fmt.Errorf("error getting balance: %w", err)
  2470  	}
  2471  	if bal.Available < totalRequiredForOrders {
  2472  		return nil, nil, 0, fmt.Errorf("insufficient funds. %d < %d", bal.Available, totalRequiredForOrders)
  2473  	}
  2474  
  2475  	customCfg, err := decodeFundMultiOptions(mo.Options)
  2476  	if err != nil {
  2477  		return nil, nil, 0, fmt.Errorf("error decoding options: %w", err)
  2478  	}
  2479  
  2480  	return dcr.fundMulti(maxLock, mo.Values, mo.FeeSuggestion, mo.MaxFeeRate, customCfg.Split, customCfg.SplitBuffer)
  2481  }
  2482  
  2483  // fundOrder finds coins from a set of UTXOs for a specified value. This method
  2484  // is the same as "fund", except the UTXOs must be passed in, and fundingMtx
  2485  // must be held by the caller.
  2486  func (dcr *ExchangeWallet) fundInternalWithUTXOs(utxos []*compositeUTXO, keep uint64, // leave utxos for this reserve amt
  2487  	enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64), lock bool) (
  2488  	coins asset.Coins, redeemScripts []dex.Bytes, spents []*fundingCoin, sum, size uint64, err error) {
  2489  	avail := sumUTXOs(utxos)
  2490  	if keep > avail { // skip utxo selection if we can't possibly make reserves
  2491  		return nil, nil, nil, 0, 0, asset.ErrInsufficientBalance
  2492  	}
  2493  
  2494  	var sz uint32
  2495  
  2496  	// First take some UTXOs out of the mix for any keep amount. Select these
  2497  	// with the objective of being as close to the amount as possible, unlike
  2498  	// tryFund that minimizes the number of UTXOs chosen. By doing this first,
  2499  	// we may be making the order spend a larger number of UTXOs, but we
  2500  	// mitigate subsequent order funding failure due to reserves because we know
  2501  	// this order will leave behind sufficient UTXOs without relying on change.
  2502  	if keep > 0 {
  2503  		kept := leastOverFund(reserveEnough(keep), utxos)
  2504  		dcr.log.Debugf("Setting aside %v DCR in %d UTXOs to respect the %v DCR reserved amount",
  2505  			toDCR(sumUTXOs(kept)), len(kept), toDCR(keep))
  2506  		utxosPruned := utxoSetDiff(utxos, kept)
  2507  		sum, _, sz, coins, spents, redeemScripts, err = tryFund(utxosPruned, enough)
  2508  		if err != nil { // try with the full set
  2509  			dcr.log.Debugf("Unable to fund order with UTXOs set aside (%v), trying again with full UTXO set.", err)
  2510  		} // else spents is populated
  2511  	}
  2512  	if len(spents) == 0 { // either keep is zero or it failed with utxosPruned
  2513  		// Without utxos set aside for keep, we have to consider any spendable
  2514  		// change (extra) that the enough func grants us.
  2515  		var extra uint64
  2516  		sum, extra, sz, coins, spents, redeemScripts, err = tryFund(utxos, enough)
  2517  		if err != nil {
  2518  			return nil, nil, nil, 0, 0, err
  2519  		}
  2520  		if avail-sum+extra < keep {
  2521  			return nil, nil, nil, 0, 0, asset.ErrInsufficientBalance
  2522  		}
  2523  		// else we got lucky with the legacy funding approach and there was
  2524  		// either available unspent or the enough func granted spendable change.
  2525  		if keep > 0 && extra > 0 {
  2526  			dcr.log.Debugf("Funding succeeded with %f DCR in spendable change.", toDCR(extra))
  2527  		}
  2528  	}
  2529  
  2530  	if lock {
  2531  		err = dcr.lockFundingCoins(spents)
  2532  		if err != nil {
  2533  			return nil, nil, nil, 0, 0, err
  2534  		}
  2535  	}
  2536  	return coins, redeemScripts, spents, sum, uint64(sz), nil
  2537  }
  2538  
  2539  // fund finds coins for the specified value. A function is provided that can
  2540  // check whether adding the provided output would be enough to satisfy the
  2541  // needed value. Preference is given to selecting coins with 1 or more confs,
  2542  // falling back to 0-conf coins where there are not enough 1+ confs coins. If
  2543  // change should not be considered "kept" (e.g. no preceding split txn, or
  2544  // mixing sends change to umixed account where it is unusable for reserves),
  2545  // caller should return 0 extra from enough func.
  2546  func (dcr *ExchangeWallet) fund(keep uint64, // leave utxos for this reserve amt
  2547  	enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64)) (
  2548  	coins asset.Coins, redeemScripts []dex.Bytes, sum, size uint64, err error) {
  2549  
  2550  	// Keep a consistent view of spendable and locked coins in the wallet and
  2551  	// the fundingCoins map to make this safe for concurrent use.
  2552  	dcr.fundingMtx.Lock()         // before listing unspents in wallet
  2553  	defer dcr.fundingMtx.Unlock() // hold until lockFundingCoins (wallet and map)
  2554  
  2555  	utxos, err := dcr.spendableUTXOs()
  2556  	if err != nil {
  2557  		return nil, nil, 0, 0, err
  2558  	}
  2559  
  2560  	coins, redeemScripts, _, sum, size, err = dcr.fundInternalWithUTXOs(utxos, keep, enough, true)
  2561  	return coins, redeemScripts, sum, size, err
  2562  }
  2563  
  2564  // spendableUTXOs generates a slice of spendable *compositeUTXO.
  2565  func (dcr *ExchangeWallet) spendableUTXOs() ([]*compositeUTXO, error) {
  2566  	accts := dcr.wallet.Accounts()
  2567  	unspents, err := dcr.wallet.Unspents(dcr.ctx, accts.PrimaryAccount)
  2568  	if err != nil {
  2569  		return nil, err
  2570  	}
  2571  	if accts.TradingAccount != "" {
  2572  		// Trading account may contain spendable utxos such as unspent split tx
  2573  		// outputs that are unlocked/returned. TODO: Care should probably be
  2574  		// taken to ensure only unspent split tx outputs are selected and other
  2575  		// unmixed outputs in the trading account are ignored.
  2576  		tradingAcctSpendables, err := dcr.wallet.Unspents(dcr.ctx, accts.TradingAccount)
  2577  		if err != nil {
  2578  			return nil, err
  2579  		}
  2580  		unspents = append(unspents, tradingAcctSpendables...)
  2581  	}
  2582  	if len(unspents) == 0 {
  2583  		return nil, fmt.Errorf("insufficient funds. 0 DCR available to spend in account %q", accts.PrimaryAccount)
  2584  	}
  2585  
  2586  	// Parse utxos to include script size for spending input. Returned utxos
  2587  	// will be sorted in ascending order by amount (smallest first).
  2588  	utxos, err := dcr.parseUTXOs(unspents)
  2589  	if err != nil {
  2590  		return nil, fmt.Errorf("error parsing unspent outputs: %w", err)
  2591  	}
  2592  	if len(utxos) == 0 {
  2593  		return nil, fmt.Errorf("no funds available")
  2594  	}
  2595  	return utxos, nil
  2596  }
  2597  
  2598  // tryFund attempts to use the provided UTXO set to satisfy the enough function
  2599  // with the fewest number of inputs. The selected utxos are not locked. If the
  2600  // requirement can be satisfied without 0-conf utxos, that set will be selected
  2601  // regardless of whether the 0-conf inclusive case would be cheaper. The
  2602  // provided UTXOs must be sorted in ascending order by value.
  2603  func tryFund(utxos []*compositeUTXO,
  2604  	enough func(sum uint64, size uint32, unspent *compositeUTXO) (bool, uint64)) (
  2605  	sum, extra uint64, size uint32, coins asset.Coins, spents []*fundingCoin, redeemScripts []dex.Bytes, err error) {
  2606  
  2607  	addUTXO := func(unspent *compositeUTXO) error {
  2608  		txHash, err := chainhash.NewHashFromStr(unspent.rpc.TxID)
  2609  		if err != nil {
  2610  			return fmt.Errorf("error decoding txid: %w", err)
  2611  		}
  2612  		v := toAtoms(unspent.rpc.Amount)
  2613  		redeemScript, err := hex.DecodeString(unspent.rpc.RedeemScript)
  2614  		if err != nil {
  2615  			return fmt.Errorf("error decoding redeem script for %s, script = %s: %w",
  2616  				unspent.rpc.TxID, unspent.rpc.RedeemScript, err)
  2617  		}
  2618  		op := newOutput(txHash, unspent.rpc.Vout, v, unspent.rpc.Tree)
  2619  		coins = append(coins, op)
  2620  		spents = append(spents, &fundingCoin{
  2621  			op:   op,
  2622  			addr: unspent.rpc.Address,
  2623  		})
  2624  		redeemScripts = append(redeemScripts, redeemScript)
  2625  		size += unspent.input.Size()
  2626  		sum += v
  2627  		return nil
  2628  	}
  2629  
  2630  	isEnoughWith := func(utxo *compositeUTXO) bool {
  2631  		ok, _ := enough(sum, size, utxo)
  2632  		return ok
  2633  	}
  2634  
  2635  	tryUTXOs := func(minconf int64) (ok bool, err error) {
  2636  		sum, size = 0, 0 // size is only sum of inputs size, not including tx overhead or outputs
  2637  		coins, spents, redeemScripts = nil, nil, nil
  2638  
  2639  		okUTXOs := make([]*compositeUTXO, 0, len(utxos)) // over-allocate
  2640  		for _, cu := range utxos {
  2641  			if cu.confs >= minconf && cu.rpc.Spendable {
  2642  				okUTXOs = append(okUTXOs, cu)
  2643  			}
  2644  		}
  2645  
  2646  		for {
  2647  			// If there are none left, we don't have enough.
  2648  			if len(okUTXOs) == 0 {
  2649  				return false, nil
  2650  			}
  2651  
  2652  			// Check if the largest output is too small.
  2653  			lastUTXO := okUTXOs[len(okUTXOs)-1]
  2654  			if !isEnoughWith(lastUTXO) {
  2655  				if err = addUTXO(lastUTXO); err != nil {
  2656  					return false, err
  2657  				}
  2658  				okUTXOs = okUTXOs[0 : len(okUTXOs)-1]
  2659  				continue
  2660  			}
  2661  
  2662  			// We only need one then. Find it.
  2663  			idx := sort.Search(len(okUTXOs), func(i int) bool {
  2664  				return isEnoughWith(okUTXOs[i])
  2665  			})
  2666  			// No need to check idx == n. We already verified that the last
  2667  			// utxo passes above.
  2668  			final := okUTXOs[idx]
  2669  			_, extra = enough(sum, size, final) // sort.Search might not have called isEnough for this utxo last
  2670  			if err = addUTXO(final); err != nil {
  2671  				return false, err
  2672  			}
  2673  			return true, nil
  2674  		}
  2675  	}
  2676  
  2677  	// First try with confs>0.
  2678  	ok, err := tryUTXOs(1)
  2679  	if err != nil {
  2680  		return 0, 0, 0, nil, nil, nil, err
  2681  	}
  2682  
  2683  	// Fallback to allowing 0-conf outputs.
  2684  	if !ok {
  2685  		ok, err = tryUTXOs(0)
  2686  		if err != nil {
  2687  			return 0, 0, 0, nil, nil, nil, err
  2688  		}
  2689  		if !ok {
  2690  			return 0, 0, 0, nil, nil, nil, fmt.Errorf("not enough to cover requested funds. "+
  2691  				"%s DCR available in %d UTXOs (%w)", amount(sum), len(coins), asset.ErrInsufficientBalance)
  2692  		}
  2693  	}
  2694  
  2695  	return
  2696  }
  2697  
  2698  // split will send a split transaction and return the sized output. If the
  2699  // split transaction is determined to be un-economical, it will not be sent,
  2700  // there is no error, and the input coins will be returned unmodified, but an
  2701  // info message will be logged. The returned bool indicates if a split tx was
  2702  // sent (true) or if the original coins were returned unmodified (false).
  2703  //
  2704  // A split transaction nets additional network bytes consisting of
  2705  //   - overhead from 1 transaction
  2706  //   - 1 extra signed p2pkh-spending input. The split tx has the fundingCoins as
  2707  //     inputs now, but we'll add the input that spends the sized coin that will go
  2708  //     into the first swap
  2709  //   - 2 additional p2pkh outputs for the split tx sized output and change
  2710  //
  2711  // If the fees associated with this extra baggage are more than the excess
  2712  // amount that would be locked if a split transaction were not used, then the
  2713  // split transaction is pointless. This might be common, for instance, if an
  2714  // order is canceled partially filled, and then the remainder resubmitted. We
  2715  // would already have an output of just the right size, and that would be
  2716  // recognized here.
  2717  func (dcr *ExchangeWallet) split(value uint64, lots uint64, coins asset.Coins, inputsSize uint64,
  2718  	splitFeeRate, bumpedMaxRate, extraOutput uint64) (asset.Coins, bool, uint64, error) {
  2719  
  2720  	// Calculate the extra fees associated with the additional inputs, outputs,
  2721  	// and transaction overhead, and compare to the excess that would be locked.
  2722  	baggageFees := bumpedMaxRate * splitTxBaggage
  2723  	if extraOutput > 0 {
  2724  		baggageFees += bumpedMaxRate * dexdcr.P2PKHOutputSize
  2725  	}
  2726  
  2727  	var coinSum uint64
  2728  	for _, coin := range coins {
  2729  		coinSum += coin.Value()
  2730  	}
  2731  
  2732  	valStr := amount(value).String()
  2733  
  2734  	excess := coinSum - calc.RequiredOrderFunds(value, inputsSize, lots,
  2735  		dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedMaxRate)
  2736  
  2737  	if baggageFees > excess {
  2738  		dcr.log.Debugf("Skipping split transaction because cost is greater than potential over-lock. %s > %s.",
  2739  			amount(baggageFees), amount(excess))
  2740  		dcr.log.Infof("Funding %s DCR order with coins %v worth %s", valStr, coins, amount(coinSum))
  2741  		// NOTE: The caller may be expecting a split to happen to maintain
  2742  		// reserves via the change from the split, but the amount held locked
  2743  		// when skipping the split in this case is roughly equivalent to the
  2744  		// loss to fees in a split. This trivial amount is of no concern because
  2745  		// the reserves should be buffered for amounts much larger than the fees
  2746  		// on a single transaction.
  2747  		return coins, false, 0, nil
  2748  	}
  2749  
  2750  	// Generate an address to receive the sized outputs. If mixing is enabled on
  2751  	// the wallet, generate the address from the external branch of the trading
  2752  	// account. The external branch is used so that if this split output isn't
  2753  	// spent, it won't be transferred to the unmixed account for re-mixing.
  2754  	// Instead, it'll simply be unlocked in the trading account and can thus be
  2755  	// used to fund future orders.
  2756  	accts := dcr.wallet.Accounts()
  2757  	getAddr := func() (stdaddr.Address, error) {
  2758  		if accts.TradingAccount != "" {
  2759  			return dcr.wallet.ExternalAddress(dcr.ctx, accts.TradingAccount)
  2760  		}
  2761  		return dcr.wallet.ExternalAddress(dcr.ctx, accts.PrimaryAccount)
  2762  	}
  2763  	addr, err := getAddr()
  2764  	if err != nil {
  2765  		return nil, false, 0, fmt.Errorf("error creating split transaction address: %w", err)
  2766  	}
  2767  
  2768  	var addr2 stdaddr.Address
  2769  	if extraOutput > 0 {
  2770  		addr2, err = getAddr()
  2771  		if err != nil {
  2772  			return nil, false, 0, fmt.Errorf("error creating secondary split transaction address: %w", err)
  2773  		}
  2774  	}
  2775  
  2776  	reqFunds := calc.RequiredOrderFunds(value, dexdcr.P2PKHInputSize, lots,
  2777  		dexdcr.InitTxSizeBase, dexdcr.InitTxSize, bumpedMaxRate)
  2778  
  2779  	dcr.fundingMtx.Lock()         // before generating the new output in sendCoins
  2780  	defer dcr.fundingMtx.Unlock() // after locking it (wallet and map)
  2781  
  2782  	msgTx, sentVal, err := dcr.sendCoins(coins, addr, addr2, reqFunds, extraOutput, splitFeeRate, false)
  2783  	if err != nil {
  2784  		return nil, false, 0, fmt.Errorf("error sending split transaction: %w", err)
  2785  	}
  2786  
  2787  	if sentVal != reqFunds {
  2788  		dcr.log.Errorf("split - total sent %.8f does not match expected %.8f", toDCR(sentVal), toDCR(reqFunds))
  2789  	}
  2790  
  2791  	op := newOutput(msgTx.CachedTxHash(), 0, sentVal, wire.TxTreeRegular)
  2792  
  2793  	// Lock the funding coin.
  2794  	err = dcr.lockFundingCoins([]*fundingCoin{{
  2795  		op:   op,
  2796  		addr: addr.String(),
  2797  	}})
  2798  	if err != nil {
  2799  		dcr.log.Errorf("error locking funding coin from split transaction %s", op)
  2800  	}
  2801  
  2802  	// Unlock the spent coins.
  2803  	_, err = dcr.returnCoins(coins)
  2804  	if err != nil {
  2805  		dcr.log.Errorf("error returning coins spent in split transaction %v", coins)
  2806  	}
  2807  
  2808  	totalOut := uint64(0)
  2809  	for i := 0; i < len(msgTx.TxOut); i++ {
  2810  		totalOut += uint64(msgTx.TxOut[i].Value)
  2811  	}
  2812  
  2813  	txHash := msgTx.CachedTxHash()
  2814  	dcr.addTxToHistory(&asset.WalletTransaction{
  2815  		Type: asset.Split,
  2816  		ID:   txHash.String(),
  2817  		Fees: coinSum - totalOut,
  2818  	}, txHash, true)
  2819  
  2820  	dcr.log.Infof("Funding %s DCR order with split output coin %v from original coins %v", valStr, op, coins)
  2821  	dcr.log.Infof("Sent split transaction %s to accommodate swap of size %s + fees = %s DCR",
  2822  		op.txHash(), valStr, amount(reqFunds))
  2823  
  2824  	return asset.Coins{op}, true, coinSum - totalOut, nil
  2825  }
  2826  
  2827  // lockFundingCoins locks the funding coins via RPC and stores them in the map.
  2828  // This function is not safe for concurrent use. The caller should lock
  2829  // dcr.fundingMtx.
  2830  func (dcr *ExchangeWallet) lockFundingCoins(fCoins []*fundingCoin) error {
  2831  	wireOPs := make([]*wire.OutPoint, 0, len(fCoins))
  2832  	for _, c := range fCoins {
  2833  		wireOPs = append(wireOPs, wire.NewOutPoint(c.op.txHash(), c.op.vout(), c.op.tree))
  2834  	}
  2835  	err := dcr.wallet.LockUnspent(dcr.ctx, false, wireOPs)
  2836  	if err != nil {
  2837  		return err
  2838  	}
  2839  	for _, c := range fCoins {
  2840  		dcr.fundingCoins[c.op.pt] = c
  2841  	}
  2842  	return nil
  2843  }
  2844  
  2845  func (dcr *ExchangeWallet) unlockFundingCoins(fCoins []*fundingCoin) error {
  2846  	wireOPs := make([]*wire.OutPoint, 0, len(fCoins))
  2847  	for _, c := range fCoins {
  2848  		wireOPs = append(wireOPs, wire.NewOutPoint(c.op.txHash(), c.op.vout(), c.op.tree))
  2849  	}
  2850  	err := dcr.wallet.LockUnspent(dcr.ctx, true, wireOPs)
  2851  	if err != nil {
  2852  		return err
  2853  	}
  2854  	for _, c := range fCoins {
  2855  		delete(dcr.fundingCoins, c.op.pt)
  2856  	}
  2857  	return nil
  2858  }
  2859  
  2860  // ReturnCoins unlocks coins. This would be necessary in the case of a canceled
  2861  // order. Coins belonging to the tradingAcct, if configured, are transferred to
  2862  // the unmixed account with the exception of unspent split tx outputs which are
  2863  // kept in the tradingAcct and may later be used to fund future orders. If
  2864  // called with a nil slice, all coins are returned and none are moved to the
  2865  // unmixed account.
  2866  func (dcr *ExchangeWallet) ReturnCoins(unspents asset.Coins) error {
  2867  	if unspents == nil { // not just empty to make this harder to do accidentally
  2868  		dcr.log.Debugf("Returning all coins.")
  2869  		dcr.fundingMtx.Lock()
  2870  		defer dcr.fundingMtx.Unlock()
  2871  		if err := dcr.wallet.LockUnspent(dcr.ctx, true, nil); err != nil {
  2872  			return err
  2873  		}
  2874  		dcr.fundingCoins = make(map[outPoint]*fundingCoin)
  2875  		return nil
  2876  	}
  2877  	if len(unspents) == 0 {
  2878  		return fmt.Errorf("cannot return zero coins")
  2879  	}
  2880  
  2881  	dcr.fundingMtx.Lock()
  2882  	returnedCoins, err := dcr.returnCoins(unspents)
  2883  	dcr.fundingMtx.Unlock()
  2884  	accts := dcr.wallet.Accounts()
  2885  	if err != nil || accts.UnmixedAccount == "" {
  2886  		return err
  2887  	}
  2888  
  2889  	// If any of these coins belong to the trading account, transfer them to the
  2890  	// unmixed account to be re-mixed into the primary account before being
  2891  	// re-selected for funding future orders. This doesn't apply to unspent
  2892  	// split tx outputs, which should remain in the trading account and be
  2893  	// selected from there for funding future orders.
  2894  	var coinsToTransfer []asset.Coin
  2895  	for _, coin := range returnedCoins {
  2896  		if coin.addr == "" {
  2897  			txOut, err := dcr.wallet.UnspentOutput(dcr.ctx, coin.op.txHash(), coin.op.vout(), coin.op.tree)
  2898  			if err != nil {
  2899  				dcr.log.Errorf("wallet.UnspentOutput error for returned coin %s: %v", coin.op, err)
  2900  				continue
  2901  			}
  2902  			if len(txOut.Addresses) == 0 {
  2903  				dcr.log.Errorf("no address in gettxout response for returned coin %s", coin.op)
  2904  				continue
  2905  			}
  2906  			coin.addr = txOut.Addresses[0]
  2907  		}
  2908  		addrInfo, err := dcr.wallet.AddressInfo(dcr.ctx, coin.addr)
  2909  		if err != nil {
  2910  			dcr.log.Errorf("wallet.AddressInfo error for returned coin %s: %v", coin.op, err)
  2911  			continue
  2912  		}
  2913  		// Move this coin to the unmixed account if it was sent to the internal
  2914  		// branch of the trading account. This excludes unspent split tx outputs
  2915  		// which are sent to the external branch of the trading account.
  2916  		if addrInfo.Branch == acctInternalBranch && addrInfo.Account == accts.TradingAccount {
  2917  			coinsToTransfer = append(coinsToTransfer, coin.op)
  2918  		}
  2919  	}
  2920  
  2921  	if len(coinsToTransfer) > 0 {
  2922  		tx, totalSent, err := dcr.sendAll(coinsToTransfer, accts.UnmixedAccount)
  2923  		if err != nil {
  2924  			dcr.log.Errorf("unable to transfer unlocked swapped change from temp trading "+
  2925  				"account to unmixed account: %v", err)
  2926  		} else {
  2927  			dcr.log.Infof("Transferred %s from temp trading account to unmixed account in tx %s.",
  2928  				dcrutil.Amount(totalSent), tx.TxHash())
  2929  		}
  2930  	}
  2931  
  2932  	return nil
  2933  }
  2934  
  2935  // returnCoins unlocks coins and removes them from the fundingCoins map.
  2936  // Requires fundingMtx to be write-locked.
  2937  func (dcr *ExchangeWallet) returnCoins(unspents asset.Coins) ([]*fundingCoin, error) {
  2938  	if len(unspents) == 0 {
  2939  		return nil, fmt.Errorf("cannot return zero coins")
  2940  	}
  2941  
  2942  	ops := make([]*wire.OutPoint, 0, len(unspents))
  2943  	fundingCoins := make([]*fundingCoin, 0, len(unspents))
  2944  
  2945  	dcr.log.Debugf("returning coins %s", unspents)
  2946  	for _, unspent := range unspents {
  2947  		op, err := dcr.convertCoin(unspent)
  2948  		if err != nil {
  2949  			return nil, fmt.Errorf("error converting coin: %w", err)
  2950  		}
  2951  		ops = append(ops, op.wireOutPoint()) // op.tree may be wire.TxTreeUnknown, but that's fine since wallet.LockUnspent doesn't rely on it
  2952  		if fCoin, ok := dcr.fundingCoins[op.pt]; ok {
  2953  			fundingCoins = append(fundingCoins, fCoin)
  2954  		} else {
  2955  			dcr.log.Warnf("returning coin %s that is not cached as a funding coin", op)
  2956  			fundingCoins = append(fundingCoins, &fundingCoin{op: op})
  2957  		}
  2958  	}
  2959  
  2960  	if err := dcr.wallet.LockUnspent(dcr.ctx, true, ops); err != nil {
  2961  		return nil, err
  2962  	}
  2963  
  2964  	for _, fCoin := range fundingCoins {
  2965  		delete(dcr.fundingCoins, fCoin.op.pt)
  2966  	}
  2967  
  2968  	return fundingCoins, nil
  2969  }
  2970  
  2971  // FundingCoins gets funding coins for the coin IDs. The coins are locked. This
  2972  // method might be called to reinitialize an order from data stored externally.
  2973  // This method will only return funding coins, e.g. unspent transaction outputs.
  2974  func (dcr *ExchangeWallet) FundingCoins(ids []dex.Bytes) (asset.Coins, error) {
  2975  	// First check if we have the coins in cache.
  2976  	coins := make(asset.Coins, 0, len(ids))
  2977  	notFound := make(map[outPoint]bool)
  2978  	dcr.fundingMtx.Lock()
  2979  	defer dcr.fundingMtx.Unlock() // stay locked until we update the map and lock them in the wallet
  2980  	for _, id := range ids {
  2981  		txHash, vout, err := decodeCoinID(id)
  2982  		if err != nil {
  2983  			return nil, err
  2984  		}
  2985  		pt := newOutPoint(txHash, vout)
  2986  		fundingCoin, found := dcr.fundingCoins[pt]
  2987  		if found {
  2988  			coins = append(coins, fundingCoin.op)
  2989  			continue
  2990  		}
  2991  		notFound[pt] = true
  2992  	}
  2993  	if len(notFound) == 0 {
  2994  		return coins, nil
  2995  	}
  2996  
  2997  	// Check locked outputs for not found coins.
  2998  	for _, acct := range dcr.fundingAccounts() {
  2999  		lockedOutputs, err := dcr.wallet.LockedOutputs(dcr.ctx, acct)
  3000  		if err != nil {
  3001  			return nil, err
  3002  		}
  3003  		for _, output := range lockedOutputs {
  3004  			txHash, err := chainhash.NewHashFromStr(output.Txid)
  3005  			if err != nil {
  3006  				return nil, fmt.Errorf("error decoding txid from rpc server %s: %w", output.Txid, err)
  3007  			}
  3008  			pt := newOutPoint(txHash, output.Vout)
  3009  			if !notFound[pt] {
  3010  				continue
  3011  			}
  3012  			txOut, err := dcr.wallet.UnspentOutput(dcr.ctx, txHash, output.Vout, output.Tree)
  3013  			if err != nil {
  3014  				return nil, fmt.Errorf("gettxout error for locked output %v: %w", pt.String(), err)
  3015  			}
  3016  			var address string
  3017  			if len(txOut.Addresses) > 0 {
  3018  				address = txOut.Addresses[0]
  3019  			}
  3020  			coin := newOutput(txHash, output.Vout, toAtoms(output.Amount), output.Tree)
  3021  			coins = append(coins, coin)
  3022  			dcr.fundingCoins[pt] = &fundingCoin{
  3023  				op:   coin,
  3024  				addr: address,
  3025  			}
  3026  			delete(notFound, pt)
  3027  			if len(notFound) == 0 {
  3028  				return coins, nil
  3029  			}
  3030  		}
  3031  	}
  3032  
  3033  	// Some funding coins still not found after checking locked outputs.
  3034  	// Check wallet unspent outputs as last resort. Lock the coins if found.
  3035  	coinsToLock := make([]*wire.OutPoint, 0, len(notFound))
  3036  	for _, acct := range dcr.fundingAccounts() {
  3037  		unspents, err := dcr.wallet.Unspents(dcr.ctx, acct)
  3038  		if err != nil {
  3039  			return nil, err
  3040  		}
  3041  		for _, txout := range unspents {
  3042  			txHash, err := chainhash.NewHashFromStr(txout.TxID)
  3043  			if err != nil {
  3044  				return nil, fmt.Errorf("error decoding txid from rpc server %s: %w", txout.TxID, err)
  3045  			}
  3046  			pt := newOutPoint(txHash, txout.Vout)
  3047  			if !notFound[pt] {
  3048  				continue
  3049  			}
  3050  			coinsToLock = append(coinsToLock, wire.NewOutPoint(txHash, txout.Vout, txout.Tree))
  3051  			coin := newOutput(txHash, txout.Vout, toAtoms(txout.Amount), txout.Tree)
  3052  			coins = append(coins, coin)
  3053  			dcr.fundingCoins[pt] = &fundingCoin{
  3054  				op:   coin,
  3055  				addr: txout.Address,
  3056  			}
  3057  			delete(notFound, pt)
  3058  			if len(notFound) == 0 {
  3059  				break
  3060  			}
  3061  		}
  3062  	}
  3063  
  3064  	// Return an error if some coins are still not found.
  3065  	if len(notFound) != 0 {
  3066  		ids := make([]string, 0, len(notFound))
  3067  		for pt := range notFound {
  3068  			ids = append(ids, pt.String())
  3069  		}
  3070  		return nil, fmt.Errorf("funding coins not found: %s", strings.Join(ids, ", "))
  3071  	}
  3072  
  3073  	dcr.log.Debugf("Locking funding coins that were unlocked %v", coinsToLock)
  3074  	err := dcr.wallet.LockUnspent(dcr.ctx, false, coinsToLock)
  3075  	if err != nil {
  3076  		return nil, err
  3077  	}
  3078  
  3079  	return coins, nil
  3080  }
  3081  
  3082  // Swap sends the swaps in a single transaction. The Receipts returned can be
  3083  // used to refund a failed transaction. The Input coins are manually unlocked
  3084  // because they're not auto-unlocked by the wallet and therefore inaccurately
  3085  // included as part of the locked balance despite being spent.
  3086  func (dcr *ExchangeWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint64, error) {
  3087  	if swaps.FeeRate == 0 {
  3088  		return nil, nil, 0, fmt.Errorf("cannot send swap with with zero fee rate")
  3089  	}
  3090  
  3091  	var totalOut uint64
  3092  	// Start with an empty MsgTx.
  3093  	baseTx := wire.NewMsgTx()
  3094  	// Add the funding utxos.
  3095  	totalIn, err := dcr.addInputCoins(baseTx, swaps.Inputs)
  3096  	if err != nil {
  3097  		return nil, nil, 0, err
  3098  	}
  3099  
  3100  	customCfg := new(swapOptions)
  3101  	err = config.Unmapify(swaps.Options, customCfg)
  3102  	if err != nil {
  3103  		return nil, nil, 0, fmt.Errorf("error parsing swap options: %w", err)
  3104  	}
  3105  
  3106  	contracts := make([][]byte, 0, len(swaps.Contracts))
  3107  	refundAddrs := make([]stdaddr.Address, 0, len(swaps.Contracts))
  3108  	// Add the contract outputs.
  3109  	for _, contract := range swaps.Contracts {
  3110  		totalOut += contract.Value
  3111  		// revokeAddrV2 is the address belonging to the key that will be
  3112  		// used to sign and refund a swap past its encoded refund locktime.
  3113  		revokeAddrV2, err := dcr.wallet.ExternalAddress(dcr.ctx, dcr.depositAccount())
  3114  		if err != nil {
  3115  			return nil, nil, 0, fmt.Errorf("error creating revocation address: %w", err)
  3116  		}
  3117  		refundAddrs = append(refundAddrs, revokeAddrV2)
  3118  		// Create the contract, a P2SH redeem script.
  3119  		contractScript, err := dexdcr.MakeContract(contract.Address, revokeAddrV2.String(), contract.SecretHash, int64(contract.LockTime), dcr.chainParams)
  3120  		if err != nil {
  3121  			return nil, nil, 0, fmt.Errorf("unable to create pubkey script for address %s: %w", contract.Address, err)
  3122  		}
  3123  		contracts = append(contracts, contractScript)
  3124  		// Make the P2SH address and pubkey script.
  3125  		scriptAddr, err := stdaddr.NewAddressScriptHashV0(contractScript, dcr.chainParams)
  3126  		if err != nil {
  3127  			return nil, nil, 0, fmt.Errorf("error encoding script address: %w", err)
  3128  		}
  3129  		p2shScriptVer, p2shScript := scriptAddr.PaymentScript()
  3130  		// Add the transaction output.
  3131  		txOut := newTxOut(int64(contract.Value), p2shScriptVer, p2shScript)
  3132  		baseTx.AddTxOut(txOut)
  3133  	}
  3134  	if totalIn < totalOut {
  3135  		return nil, nil, 0, fmt.Errorf("unfunded contract. %d < %d", totalIn, totalOut)
  3136  	}
  3137  
  3138  	// Ensure we have enough outputs before broadcasting.
  3139  	swapCount := len(swaps.Contracts)
  3140  	if len(baseTx.TxOut) < swapCount {
  3141  		return nil, nil, 0, fmt.Errorf("fewer outputs than swaps. %d < %d", len(baseTx.TxOut), swapCount)
  3142  	}
  3143  
  3144  	feeRate, err := calcBumpedRate(swaps.FeeRate, customCfg.FeeBump)
  3145  	if err != nil {
  3146  		dcr.log.Errorf("ignoring invalid fee bump factor, %s: %v", float64PtrStr(customCfg.FeeBump), err)
  3147  	}
  3148  
  3149  	// Add change, sign, and send the transaction.
  3150  	dcr.fundingMtx.Lock()         // before generating change output
  3151  	defer dcr.fundingMtx.Unlock() // hold until after returnCoins and lockFundingCoins(change)
  3152  	// Sign the tx but don't send the transaction yet until
  3153  	// the individual swap refund txs are prepared and signed.
  3154  	changeAcct := dcr.depositAccount()
  3155  	tradingAccount := dcr.wallet.Accounts().TradingAccount
  3156  	if swaps.LockChange && tradingAccount != "" {
  3157  		// Change will likely be used to fund more swaps, send to trading
  3158  		// account.
  3159  		changeAcct = tradingAccount
  3160  	}
  3161  	msgTx, change, changeAddr, fees, err := dcr.signTxAndAddChange(baseTx, feeRate, -1, changeAcct)
  3162  	if err != nil {
  3163  		return nil, nil, 0, err
  3164  	}
  3165  
  3166  	receipts := make([]asset.Receipt, 0, swapCount)
  3167  	txHash := msgTx.CachedTxHash()
  3168  	for i, contract := range swaps.Contracts {
  3169  		output := newOutput(txHash, uint32(i), contract.Value, wire.TxTreeRegular)
  3170  		signedRefundTx, _, _, err := dcr.refundTx(output.ID(), contracts[i], contract.Value, refundAddrs[i], swaps.FeeRate)
  3171  		if err != nil {
  3172  			return nil, nil, 0, fmt.Errorf("error creating refund tx: %w", err)
  3173  		}
  3174  		refundB, err := signedRefundTx.Bytes()
  3175  		if err != nil {
  3176  			return nil, nil, 0, fmt.Errorf("error serializing refund tx: %w", err)
  3177  		}
  3178  		receipts = append(receipts, &swapReceipt{
  3179  			output:       output,
  3180  			contract:     contracts[i],
  3181  			expiration:   time.Unix(int64(contract.LockTime), 0).UTC(),
  3182  			signedRefund: refundB,
  3183  		})
  3184  	}
  3185  
  3186  	// Refund txs prepared and signed. Can now broadcast the swap(s).
  3187  	_, err = dcr.broadcastTx(msgTx)
  3188  	if err != nil {
  3189  		return nil, nil, 0, err
  3190  	}
  3191  
  3192  	dcr.addTxToHistory(&asset.WalletTransaction{
  3193  		Type:   asset.Swap,
  3194  		ID:     txHash.String(),
  3195  		Amount: totalOut,
  3196  		Fees:   fees,
  3197  	}, txHash, true)
  3198  
  3199  	// Return spent outputs.
  3200  	_, err = dcr.returnCoins(swaps.Inputs)
  3201  	if err != nil {
  3202  		dcr.log.Errorf("error unlocking swapped coins", swaps.Inputs)
  3203  	}
  3204  
  3205  	// Lock the change coin, if requested.
  3206  	if swaps.LockChange {
  3207  		dcr.log.Debugf("locking change coin %s", change)
  3208  		err = dcr.lockFundingCoins([]*fundingCoin{{
  3209  			op:   change,
  3210  			addr: changeAddr,
  3211  		}})
  3212  		if err != nil {
  3213  			dcr.log.Warnf("Failed to lock dcr change coin %s", change)
  3214  		}
  3215  	}
  3216  
  3217  	// If change is nil, return a nil asset.Coin.
  3218  	var changeCoin asset.Coin
  3219  	if change != nil {
  3220  		changeCoin = change
  3221  	}
  3222  	return receipts, changeCoin, fees, nil
  3223  }
  3224  
  3225  // Redeem sends the redemption transaction, which may contain more than one
  3226  // redemption. FeeSuggestion is just a fallback if an internal estimate using
  3227  // the wallet's redeem confirm block target setting is not available.
  3228  func (dcr *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) {
  3229  	// Create a transaction that spends the referenced contract.
  3230  	msgTx := wire.NewMsgTx()
  3231  	var totalIn uint64
  3232  	var contracts [][]byte
  3233  	var addresses []stdaddr.Address
  3234  	for _, r := range form.Redemptions {
  3235  		if r.Spends == nil {
  3236  			return nil, nil, 0, fmt.Errorf("no audit info")
  3237  		}
  3238  
  3239  		cinfo, err := convertAuditInfo(r.Spends, dcr.chainParams)
  3240  		if err != nil {
  3241  			return nil, nil, 0, err
  3242  		}
  3243  
  3244  		// Extract the swap contract recipient and secret hash and check the secret
  3245  		// hash against the hash of the provided secret.
  3246  		contract := cinfo.contract
  3247  		_, receiver, _, secretHash, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams)
  3248  		if err != nil {
  3249  			return nil, nil, 0, fmt.Errorf("error extracting swap addresses: %w", err)
  3250  		}
  3251  		checkSecretHash := sha256.Sum256(r.Secret)
  3252  		if !bytes.Equal(checkSecretHash[:], secretHash) {
  3253  			return nil, nil, 0, fmt.Errorf("secret hash mismatch. %x != %x", checkSecretHash[:], secretHash)
  3254  		}
  3255  		addresses = append(addresses, receiver)
  3256  		contracts = append(contracts, contract)
  3257  		prevOut := cinfo.output.wireOutPoint()
  3258  		txIn := wire.NewTxIn(prevOut, int64(cinfo.output.value), []byte{})
  3259  		msgTx.AddTxIn(txIn)
  3260  		totalIn += cinfo.output.value
  3261  	}
  3262  
  3263  	// Calculate the size and the fees.
  3264  	size := msgTx.SerializeSize() + dexdcr.RedeemSwapSigScriptSize*len(form.Redemptions) + dexdcr.P2PKHOutputSize
  3265  
  3266  	customCfg := new(redeemOptions)
  3267  	err := config.Unmapify(form.Options, customCfg)
  3268  	if err != nil {
  3269  		return nil, nil, 0, fmt.Errorf("error parsing selected swap options: %w", err)
  3270  	}
  3271  
  3272  	rawFeeRate := dcr.targetFeeRateWithFallback(dcr.config().redeemConfTarget, form.FeeSuggestion)
  3273  	feeRate, err := calcBumpedRate(rawFeeRate, customCfg.FeeBump)
  3274  	if err != nil {
  3275  		dcr.log.Errorf("calcBumpRate error: %v", err)
  3276  	}
  3277  	fee := feeRate * uint64(size)
  3278  	if fee > totalIn {
  3279  		// Double check that the fee bump isn't the issue.
  3280  		feeRate = rawFeeRate
  3281  		fee = feeRate * uint64(size)
  3282  		if fee > totalIn {
  3283  			return nil, nil, 0, fmt.Errorf("redeem tx not worth the fees")
  3284  		}
  3285  		dcr.log.Warnf("Ignoring fee bump (%v) resulting in fees > redemption", float64PtrStr(customCfg.FeeBump))
  3286  	}
  3287  
  3288  	// Send the funds back to the exchange wallet.
  3289  	txOut, _, err := dcr.makeExternalOut(dcr.depositAccount(), totalIn-fee)
  3290  	if err != nil {
  3291  		return nil, nil, 0, err
  3292  	}
  3293  	// One last check for dust.
  3294  	if dexdcr.IsDust(txOut, feeRate) {
  3295  		return nil, nil, 0, fmt.Errorf("redeem output is dust")
  3296  	}
  3297  	msgTx.AddTxOut(txOut)
  3298  	// Sign the inputs.
  3299  	for i, r := range form.Redemptions {
  3300  		contract := contracts[i]
  3301  		redeemSig, redeemPubKey, err := dcr.createSig(msgTx, i, contract, addresses[i])
  3302  		if err != nil {
  3303  			return nil, nil, 0, err
  3304  		}
  3305  		redeemSigScript, err := dexdcr.RedeemP2SHContract(contract, redeemSig, redeemPubKey, r.Secret)
  3306  		if err != nil {
  3307  			return nil, nil, 0, err
  3308  		}
  3309  		msgTx.TxIn[i].SignatureScript = redeemSigScript
  3310  	}
  3311  	// Send the transaction.
  3312  	txHash, err := dcr.broadcastTx(msgTx)
  3313  	if err != nil {
  3314  		return nil, nil, 0, err
  3315  	}
  3316  
  3317  	dcr.addTxToHistory(&asset.WalletTransaction{
  3318  		Type:   asset.Redeem,
  3319  		ID:     txHash.String(),
  3320  		Amount: totalIn,
  3321  		Fees:   fee,
  3322  	}, txHash, true)
  3323  
  3324  	coinIDs := make([]dex.Bytes, 0, len(form.Redemptions))
  3325  	dcr.mempoolRedeemsMtx.Lock()
  3326  	for i := range form.Redemptions {
  3327  		coinIDs = append(coinIDs, toCoinID(txHash, uint32(i)))
  3328  		var secretHash [32]byte
  3329  		copy(secretHash[:], form.Redemptions[i].Spends.SecretHash)
  3330  		dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: *txHash, firstSeen: time.Now()}
  3331  	}
  3332  	dcr.mempoolRedeemsMtx.Unlock()
  3333  	return coinIDs, newOutput(txHash, 0, uint64(txOut.Value), wire.TxTreeRegular), fee, nil
  3334  }
  3335  
  3336  // SignMessage signs the message with the private key associated with the
  3337  // specified funding Coin. A slice of pubkeys required to spend the Coin and a
  3338  // signature for each pubkey are returned.
  3339  func (dcr *ExchangeWallet) SignMessage(coin asset.Coin, msg dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) {
  3340  	op, err := dcr.convertCoin(coin)
  3341  	if err != nil {
  3342  		return nil, nil, fmt.Errorf("error converting coin: %w", err)
  3343  	}
  3344  
  3345  	// First check if we have the funding coin cached. If so, grab the address
  3346  	// from there.
  3347  	dcr.fundingMtx.RLock()
  3348  	fCoin, found := dcr.fundingCoins[op.pt]
  3349  	dcr.fundingMtx.RUnlock()
  3350  	var addr string
  3351  	if found {
  3352  		addr = fCoin.addr
  3353  	} else {
  3354  		// Check if we can get the address from wallet.UnspentOutput.
  3355  		// op.tree may be wire.TxTreeUnknown but wallet.UnspentOutput is
  3356  		// able to deal with that and find the actual tree.
  3357  		txOut, err := dcr.wallet.UnspentOutput(dcr.ctx, op.txHash(), op.vout(), op.tree)
  3358  		if err != nil {
  3359  			dcr.log.Errorf("gettxout error for SignMessage coin %s: %v", op, err)
  3360  		} else if txOut != nil {
  3361  			if len(txOut.Addresses) != 1 {
  3362  				// TODO: SignMessage is usually called for coins selected by
  3363  				// FundOrder. Should consider rejecting/ignoring multisig ops
  3364  				// in FundOrder to prevent this SignMessage error from killing
  3365  				// order placements.
  3366  				return nil, nil, fmt.Errorf("multi-sig not supported")
  3367  			}
  3368  			addr = txOut.Addresses[0]
  3369  			found = true
  3370  		}
  3371  	}
  3372  	// Could also try the gettransaction endpoint, which is supposed to return
  3373  	// information about wallet transactions, but which (I think?) doesn't list
  3374  	// ssgen outputs.
  3375  	if !found {
  3376  		return nil, nil, fmt.Errorf("did not locate coin %s. is this a coin returned from Fund?", coin)
  3377  	}
  3378  	address, err := stdaddr.DecodeAddress(addr, dcr.chainParams)
  3379  	if err != nil {
  3380  		return nil, nil, fmt.Errorf("error decoding address: %w", err)
  3381  	}
  3382  	priv, err := dcr.wallet.AddressPrivKey(dcr.ctx, address)
  3383  	if err != nil {
  3384  		return nil, nil, err
  3385  	}
  3386  	defer priv.Zero()
  3387  	hash := chainhash.HashB(msg) // legacy servers will not accept this signature!
  3388  	signature := ecdsa.Sign(priv, hash)
  3389  	pubkeys = append(pubkeys, priv.PubKey().SerializeCompressed())
  3390  	sigs = append(sigs, signature.Serialize()) // DER format
  3391  	return pubkeys, sigs, nil
  3392  }
  3393  
  3394  // AuditContract retrieves information about a swap contract from the provided
  3395  // txData if it represents a valid transaction that pays to the contract at the
  3396  // specified coinID. The txData may be empty to attempt retrieval of the
  3397  // transaction output from the network, but it is only ensured to succeed for a
  3398  // full node or, if the tx is confirmed, an SPV wallet. Normally the server
  3399  // should communicate this txData, and the caller can decide to require it. The
  3400  // ability to work with an empty txData is a convenience for recovery tools and
  3401  // testing, and it may change in the future if a GetTxData method is added for
  3402  // this purpose. Optionally, attempt is also made to broadcasted the txData to
  3403  // the blockchain network but it is not necessary that the broadcast succeeds
  3404  // since the contract may have already been broadcasted.
  3405  func (dcr *ExchangeWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroadcast bool) (*asset.AuditInfo, error) {
  3406  	txHash, vout, err := decodeCoinID(coinID)
  3407  	if err != nil {
  3408  		return nil, err
  3409  	}
  3410  
  3411  	// Get the receiving address.
  3412  	_, receiver, stamp, secretHash, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams)
  3413  	if err != nil {
  3414  		return nil, fmt.Errorf("error extracting swap addresses: %w", err)
  3415  	}
  3416  
  3417  	// If no tx data is provided, attempt to get the required data (the txOut)
  3418  	// from the wallet. If this is a full node wallet, a simple gettxout RPC is
  3419  	// sufficient with no pkScript or "since" time. If this is an SPV wallet,
  3420  	// only a confirmed counterparty contract can be located, and only one
  3421  	// within ContractSearchLimit. As such, this mode of operation is not
  3422  	// intended for normal server-coordinated operation.
  3423  	var contractTx *wire.MsgTx
  3424  	var contractTxOut *wire.TxOut
  3425  	var txTree int8
  3426  	if len(txData) == 0 {
  3427  		// Fall back to gettxout, but we won't have the tx to rebroadcast.
  3428  		output, err := dcr.wallet.UnspentOutput(dcr.ctx, txHash, vout, wire.TxTreeUnknown)
  3429  		if err == nil {
  3430  			contractTxOut = output.TxOut
  3431  			txTree = output.Tree
  3432  		} else {
  3433  			// Next, try a block filters scan.
  3434  			scriptAddr, err := stdaddr.NewAddressScriptHashV0(contract, dcr.chainParams)
  3435  			if err != nil {
  3436  				return nil, fmt.Errorf("error encoding script address: %w", err)
  3437  			}
  3438  			_, pkScript := scriptAddr.PaymentScript()
  3439  			outFound, _, err := dcr.externalTxOutput(dcr.ctx, newOutPoint(txHash, vout),
  3440  				pkScript, time.Now().Add(-ContractSearchLimit))
  3441  			if err != nil {
  3442  				return nil, fmt.Errorf("error finding unspent contract: %s:%d : %w", txHash, vout, err)
  3443  			}
  3444  			contractTxOut = outFound.TxOut
  3445  			txTree = outFound.tree
  3446  		}
  3447  	} else {
  3448  		contractTx, err = msgTxFromBytes(txData)
  3449  		if err != nil {
  3450  			return nil, fmt.Errorf("invalid contract tx data: %w", err)
  3451  		}
  3452  		if err = blockchain.CheckTransactionSanity(contractTx, uint64(dcr.chainParams.MaxTxSize)); err != nil {
  3453  			return nil, fmt.Errorf("invalid contract tx data: %w", err)
  3454  		}
  3455  		if checkHash := contractTx.TxHash(); checkHash != *txHash {
  3456  			return nil, fmt.Errorf("invalid contract tx data: expected hash %s, got %s", txHash, checkHash)
  3457  		}
  3458  		if int(vout) >= len(contractTx.TxOut) {
  3459  			return nil, fmt.Errorf("invalid contract tx data: no output at %d", vout)
  3460  		}
  3461  		contractTxOut = contractTx.TxOut[vout]
  3462  		txTree = determineTxTree(contractTx)
  3463  	}
  3464  
  3465  	// Validate contract output.
  3466  	// Script must be P2SH, with 1 address and 1 required signature.
  3467  	scriptClass, addrs := stdscript.ExtractAddrs(contractTxOut.Version, contractTxOut.PkScript, dcr.chainParams)
  3468  	if scriptClass != stdscript.STScriptHash {
  3469  		return nil, fmt.Errorf("unexpected script class %d", scriptClass)
  3470  	}
  3471  	if len(addrs) != 1 {
  3472  		return nil, fmt.Errorf("unexpected number of addresses for P2SH script: %d", len(addrs))
  3473  	}
  3474  	// Compare the contract hash to the P2SH address.
  3475  	contractHash := dcrutil.Hash160(contract)
  3476  	addr := addrs[0]
  3477  	addrScript, err := dexdcr.AddressScript(addr)
  3478  	if err != nil {
  3479  		return nil, err
  3480  	}
  3481  	if !bytes.Equal(contractHash, addrScript) {
  3482  		return nil, fmt.Errorf("contract hash doesn't match script address. %x != %x",
  3483  			contractHash, addrScript)
  3484  	}
  3485  
  3486  	// The counter-party should have broadcasted the contract tx but rebroadcast
  3487  	// just in case to ensure that the tx is sent to the network. Do not block
  3488  	// because this is not required and does not affect the audit result.
  3489  	if rebroadcast && contractTx != nil {
  3490  		go func() {
  3491  			if hashSent, err := dcr.wallet.SendRawTransaction(dcr.ctx, contractTx, true); err != nil {
  3492  				dcr.log.Debugf("Rebroadcasting counterparty contract %v (THIS MAY BE NORMAL): %v", txHash, err)
  3493  			} else if !hashSent.IsEqual(txHash) {
  3494  				dcr.log.Errorf("Counterparty contract %v was rebroadcast as %v!", txHash, hashSent)
  3495  			}
  3496  		}()
  3497  	}
  3498  
  3499  	return &asset.AuditInfo{
  3500  		Coin:       newOutput(txHash, vout, uint64(contractTxOut.Value), txTree),
  3501  		Contract:   contract,
  3502  		SecretHash: secretHash,
  3503  		Recipient:  receiver.String(),
  3504  		Expiration: time.Unix(int64(stamp), 0).UTC(),
  3505  	}, nil
  3506  }
  3507  
  3508  func determineTxTree(msgTx *wire.MsgTx) int8 {
  3509  	if stake.DetermineTxType(msgTx) != stake.TxTypeRegular {
  3510  		return wire.TxTreeStake
  3511  	}
  3512  	return wire.TxTreeRegular
  3513  }
  3514  
  3515  // lookupTxOutput attempts to find and return details for the specified output,
  3516  // first checking for an unspent output and if not found, checking wallet txs.
  3517  // Returns asset.CoinNotFoundError if the output is not found.
  3518  //
  3519  // NOTE: This method is only guaranteed to return results for outputs belonging
  3520  // to transactions that are tracked by the wallet, although full node wallets
  3521  // are able to look up non-wallet outputs that are unspent.
  3522  //
  3523  // If the value of the spent flag is -1, it could not be determined with the SPV
  3524  // wallet if it is spent, and the caller should perform a block filters scan to
  3525  // locate a (mined) spending transaction if needed.
  3526  func (dcr *ExchangeWallet) lookupTxOutput(ctx context.Context, txHash *chainhash.Hash, vout uint32) (txOut *wire.TxOut, confs uint32, spent int8, err error) {
  3527  	// Check for an unspent output.
  3528  	output, err := dcr.wallet.UnspentOutput(ctx, txHash, vout, wire.TxTreeUnknown)
  3529  	if err == nil {
  3530  		return output.TxOut, output.Confirmations, 0, nil
  3531  	} else if !errors.Is(err, asset.CoinNotFoundError) {
  3532  		return nil, 0, 0, err
  3533  	}
  3534  
  3535  	// Check wallet transactions.
  3536  	tx, err := dcr.wallet.GetTransaction(ctx, txHash)
  3537  	if err != nil {
  3538  		return nil, 0, 0, err // asset.CoinNotFoundError if not found
  3539  	}
  3540  	if int(vout) >= len(tx.MsgTx.TxOut) {
  3541  		return nil, 0, 0, fmt.Errorf("tx %s has no output at %d", txHash, vout)
  3542  	}
  3543  
  3544  	txOut = tx.MsgTx.TxOut[vout]
  3545  	confs = uint32(tx.Confirmations)
  3546  
  3547  	// We have the requested output. Check if it is spent.
  3548  	if confs == 0 {
  3549  		// Only counts as spent if spent in a mined transaction,
  3550  		// unconfirmed tx outputs can't be spent in a mined tx.
  3551  
  3552  		// There is a dcrwallet bug by which the user can shut down at the wrong
  3553  		// time and a tx will never be marked as confirmed. We'll force a
  3554  		// cfilter scan for unconfirmed txs until the bug is resolved.
  3555  		// https://github.com/decred/dcrdex/pull/2444
  3556  		if dcr.wallet.SpvMode() {
  3557  			return txOut, confs, -1, nil
  3558  		}
  3559  		return txOut, confs, 0, nil
  3560  	}
  3561  
  3562  	if !dcr.wallet.SpvMode() {
  3563  		// A mined output that is not found by wallet.UnspentOutput
  3564  		// is spent if the wallet is connected to a full node.
  3565  		dcr.log.Debugf("Output %s:%d that was not reported as unspent is considered SPENT, spv mode = false.",
  3566  			txHash, vout)
  3567  		return txOut, confs, 1, nil
  3568  	}
  3569  
  3570  	// For SPV wallets, only consider the output spent if it pays to the wallet
  3571  	// because outputs that don't pay to the wallet may be unspent but still not
  3572  	// found by wallet.UnspentOutput. NOTE: Swap contracts never pay to wallet
  3573  	// (p2sh with no imported redeem script), so this is not an expected outcome
  3574  	// for swap contract outputs!
  3575  	//
  3576  	// for _, details := range tx.Details {
  3577  	// 	if details.Vout == vout && details.Category == wallet.CreditReceive.String() {
  3578  	// 		dcr.log.Tracef("Output %s:%d was not reported as unspent, pays to the wallet and is considered SPENT.",
  3579  	// 			txHash, vout)
  3580  	// 		return txOut, confs, 1, nil
  3581  	// 	}
  3582  	// }
  3583  
  3584  	// Spend status is unknown.  Caller may scan block filters if needed.
  3585  	dcr.log.Tracef("Output %s:%d was not reported as unspent by SPV wallet. Spend status UNKNOWN.",
  3586  		txHash, vout)
  3587  	return txOut, confs, -1, nil // unknown spend status
  3588  }
  3589  
  3590  // LockTimeExpired returns true if the specified locktime has expired, making it
  3591  // possible to redeem the locked coins.
  3592  func (dcr *ExchangeWallet) LockTimeExpired(ctx context.Context, lockTime time.Time) (bool, error) {
  3593  	blockHash := dcr.cachedBestBlock().hash
  3594  	hdr, err := dcr.wallet.GetBlockHeader(ctx, blockHash)
  3595  	if err != nil {
  3596  		return false, fmt.Errorf("unable to retrieve the block header: %w", err)
  3597  	}
  3598  	return time.Unix(hdr.MedianTime, 0).After(lockTime), nil
  3599  }
  3600  
  3601  // ContractLockTimeExpired returns true if the specified contract's locktime has
  3602  // expired, making it possible to issue a Refund.
  3603  func (dcr *ExchangeWallet) ContractLockTimeExpired(ctx context.Context, contract dex.Bytes) (bool, time.Time, error) {
  3604  	_, _, locktime, _, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams)
  3605  	if err != nil {
  3606  		return false, time.Time{}, fmt.Errorf("error extracting contract locktime: %w", err)
  3607  	}
  3608  	contractExpiry := time.Unix(int64(locktime), 0).UTC()
  3609  	expired, err := dcr.LockTimeExpired(ctx, contractExpiry)
  3610  	if err != nil {
  3611  		return false, time.Time{}, err
  3612  	}
  3613  	return expired, contractExpiry, nil
  3614  }
  3615  
  3616  // FindRedemption watches for the input that spends the specified contract
  3617  // coin, and returns the spending input and the contract's secret key when it
  3618  // finds a spender.
  3619  //
  3620  // This method blocks until the redemption is found, an error occurs or the
  3621  // provided context is canceled.
  3622  func (dcr *ExchangeWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) {
  3623  	txHash, vout, err := decodeCoinID(coinID)
  3624  	if err != nil {
  3625  		return nil, nil, fmt.Errorf("cannot decode contract coin id: %w", err)
  3626  	}
  3627  
  3628  	// Add this contract to the findRedemptionQueue before performing
  3629  	// initial redemption search (see below). The initial search done
  3630  	// below only checks tx inputs in mempool and blocks starting from
  3631  	// the block in which the contract coin is mined up till the current
  3632  	// best block (for mined contracts, that is).
  3633  	// Adding this contract to the findRedemptionQueue now makes it
  3634  	// possible to find the redemption if the contract is redeemed in a
  3635  	// later transaction. Additional redemption searches are triggered
  3636  	// for all contracts in the findRedemptionQueue whenever a new block
  3637  	// or a re-org is observed in the dcr.monitorBlocks goroutine.
  3638  	// This contract will be removed from the findRedemptionQueue when
  3639  	// the redemption is found or if the provided context is canceled
  3640  	// before the redemption is found.
  3641  	contractOutpoint := newOutPoint(txHash, vout)
  3642  	resultChan, contractBlock, err := dcr.queueFindRedemptionRequest(ctx, contractOutpoint)
  3643  	if err != nil {
  3644  		return nil, nil, err
  3645  	}
  3646  
  3647  	// Run initial search for redemption. If this contract is unmined,
  3648  	// only scan mempool transactions as mempool contracts can only be
  3649  	// spent by another mempool tx. If the contract is mined, scan all
  3650  	// mined tx inputs starting from the block in which the contract is
  3651  	// mined, up till the current best block. If the redemption is not
  3652  	// found in that block range, proceed to check mempool.
  3653  	if contractBlock == nil {
  3654  		dcr.findRedemptionsInMempool([]outPoint{contractOutpoint})
  3655  	} else {
  3656  		bestBlock := dcr.cachedBestBlock()
  3657  		dcr.findRedemptionsInBlockRange(contractBlock.height, bestBlock.height, []outPoint{contractOutpoint})
  3658  	}
  3659  
  3660  	// Wait for a find redemption result or context cancellation.
  3661  	// If the context is cancelled during an active mempool or block
  3662  	// range search, the contract will be removed from the queue and
  3663  	// there will be no further redemption searches for the contract.
  3664  	// See findRedemptionsIn{Mempool,BlockRange} -> findRedemptionsInTx.
  3665  	// If there is no active redemption search for this contract and
  3666  	// the context is canceled while waiting for new blocks to search,
  3667  	// the context cancellation will be caught here and the contract
  3668  	// will be removed from queue to prevent further searches when new
  3669  	// blocks are observed.
  3670  	var result *findRedemptionResult
  3671  	select {
  3672  	case result = <-resultChan:
  3673  	case <-ctx.Done():
  3674  	}
  3675  
  3676  	// If this contract is still in the findRedemptionQueue, remove from the queue
  3677  	// to prevent further redemption search attempts for this contract.
  3678  	dcr.findRedemptionMtx.Lock()
  3679  	delete(dcr.findRedemptionQueue, contractOutpoint)
  3680  	dcr.findRedemptionMtx.Unlock()
  3681  
  3682  	// result would be nil if ctx is canceled or the result channel
  3683  	// is closed without data, which would happen if the redemption
  3684  	// search is aborted when this ExchangeWallet is shut down.
  3685  	if result != nil {
  3686  		return result.RedemptionCoinID, result.Secret, result.Err
  3687  	}
  3688  	return nil, nil, fmt.Errorf("aborted search for redemption of contract %s: %w",
  3689  		contractOutpoint, ctx.Err())
  3690  }
  3691  
  3692  // queueFindRedemptionRequest extracts the contract hash and tx block (if mined)
  3693  // of the provided contract outpoint, creates a find redemption request and adds
  3694  // it to the findRedemptionQueue. Returns error if a find redemption request is
  3695  // already queued for the contract or if the contract hash or block info cannot
  3696  // be extracted.
  3697  func (dcr *ExchangeWallet) queueFindRedemptionRequest(ctx context.Context, contractOutpoint outPoint) (chan *findRedemptionResult, *block, error) {
  3698  	dcr.findRedemptionMtx.Lock()
  3699  	defer dcr.findRedemptionMtx.Unlock()
  3700  
  3701  	if _, inQueue := dcr.findRedemptionQueue[contractOutpoint]; inQueue {
  3702  		return nil, nil, fmt.Errorf("duplicate find redemption request for %s", contractOutpoint.String())
  3703  	}
  3704  	txHash, vout := contractOutpoint.txHash, contractOutpoint.vout
  3705  	tx, err := dcr.wallet.GetTransaction(dcr.ctx, &txHash)
  3706  	if err != nil {
  3707  		return nil, nil, err
  3708  	}
  3709  	if int(vout) > len(tx.MsgTx.TxOut)-1 {
  3710  		return nil, nil, fmt.Errorf("vout index %d out of range for transaction %s", vout, txHash)
  3711  	}
  3712  	contractScript := tx.MsgTx.TxOut[vout].PkScript
  3713  	contractScriptVer := tx.MsgTx.TxOut[vout].Version
  3714  	if !stdscript.IsScriptHashScript(contractScriptVer, contractScript) {
  3715  		return nil, nil, fmt.Errorf("coin %s not a valid contract", contractOutpoint.String())
  3716  	}
  3717  	var contractBlock *block
  3718  	if tx.BlockHash != "" {
  3719  		blockHash, err := chainhash.NewHashFromStr(tx.BlockHash)
  3720  		if err != nil {
  3721  			return nil, nil, fmt.Errorf("invalid blockhash %s for contract %s: %w", tx.BlockHash, contractOutpoint.String(), err)
  3722  		}
  3723  		header, err := dcr.wallet.GetBlockHeader(dcr.ctx, blockHash)
  3724  		if err != nil {
  3725  			return nil, nil, fmt.Errorf("error fetching block header %s for contract %s: %w",
  3726  				tx.BlockHash, contractOutpoint.String(), err)
  3727  		}
  3728  		contractBlock = &block{height: int64(header.Height), hash: blockHash}
  3729  	}
  3730  
  3731  	resultChan := make(chan *findRedemptionResult, 1)
  3732  	dcr.findRedemptionQueue[contractOutpoint] = &findRedemptionReq{
  3733  		ctx:                     ctx,
  3734  		contractP2SHScript:      contractScript,
  3735  		contractOutputScriptVer: contractScriptVer,
  3736  		resultChan:              resultChan,
  3737  	}
  3738  	return resultChan, contractBlock, nil
  3739  }
  3740  
  3741  // findRedemptionsInMempool attempts to find spending info for the specified
  3742  // contracts by searching every input of all txs in the mempool.
  3743  // If spending info is found for any contract, the contract is purged from the
  3744  // findRedemptionQueue and the contract's secret (if successfully parsed) or any
  3745  // error that occurs during parsing is returned to the redemption finder via the
  3746  // registered result chan.
  3747  func (dcr *ExchangeWallet) findRedemptionsInMempool(contractOutpoints []outPoint) {
  3748  	contractsCount := len(contractOutpoints)
  3749  	dcr.log.Debugf("finding redemptions for %d contracts in mempool", contractsCount)
  3750  
  3751  	var totalFound, totalCanceled int
  3752  	logAbandon := func(reason string) {
  3753  		// Do not remove the contracts from the findRedemptionQueue
  3754  		// as they could be subsequently redeemed in some mined tx(s),
  3755  		// which would be captured when a new tip is reported.
  3756  		if totalFound+totalCanceled > 0 {
  3757  			dcr.log.Debugf("%d redemptions found, %d canceled out of %d contracts in mempool",
  3758  				totalFound, totalCanceled, contractsCount)
  3759  		}
  3760  		dcr.log.Errorf("abandoning mempool redemption search for %d contracts because of %s",
  3761  			contractsCount-totalFound-totalCanceled, reason)
  3762  	}
  3763  
  3764  	mempooler, is := dcr.wallet.(Mempooler)
  3765  	if !is || dcr.wallet.SpvMode() {
  3766  		return
  3767  	}
  3768  
  3769  	mempoolTxs, err := mempooler.GetRawMempool(dcr.ctx)
  3770  	if err != nil {
  3771  		logAbandon(fmt.Sprintf("error retrieving transactions: %v", err))
  3772  		return
  3773  	}
  3774  
  3775  	for _, txHash := range mempoolTxs {
  3776  		tx, err := dcr.wallet.GetTransaction(dcr.ctx, txHash)
  3777  		if err != nil {
  3778  			logAbandon(fmt.Sprintf("getrawtransaction error for tx hash %v: %v", txHash, err))
  3779  			return
  3780  		}
  3781  		found, canceled := dcr.findRedemptionsInTx("mempool", tx.MsgTx, contractOutpoints)
  3782  		totalFound += found
  3783  		totalCanceled += canceled
  3784  		if totalFound+totalCanceled == contractsCount {
  3785  			break
  3786  		}
  3787  	}
  3788  
  3789  	dcr.log.Debugf("%d redemptions found, %d canceled out of %d contracts in mempool",
  3790  		totalFound, totalCanceled, contractsCount)
  3791  }
  3792  
  3793  // findRedemptionsInBlockRange attempts to find spending info for the specified
  3794  // contracts by checking the cfilters of each block in the provided range for
  3795  // likely inclusion of ANY of the specified contracts' P2SH script. If a block's
  3796  // cfilters reports possible inclusion of ANY of the contracts' P2SH script,
  3797  // all inputs of the matching block's txs are checked to determine if any of the
  3798  // inputs spends any of the provided contracts.
  3799  // If spending info is found for any contract, the contract is purged from the
  3800  // findRedemptionQueue and the contract's secret (if successfully parsed) or any
  3801  // error that occurs during parsing is returned to the redemption finder via the
  3802  // registered result chan.
  3803  // If spending info is not found for any of these contracts after checking the
  3804  // specified block range, a mempool search is triggered to attempt finding unmined
  3805  // redemptions for the remaining contracts.
  3806  // NOTE:
  3807  // Any error encountered while checking a block's cfilters or fetching a matching
  3808  // block's txs compromises the redemption search for this set of contracts because
  3809  // subsequent attempts to find these contracts' redemption will not repeat any
  3810  // block in the specified range unless the contracts are first removed from the
  3811  // findRedemptionQueue. Thus, any such error will cause this set of contracts to
  3812  // be purged from the findRedemptionQueue. The error will be propagated to the
  3813  // redemption finder(s) and these may re-call dcr.FindRedemption to restart find
  3814  // redemption attempts for any of these contracts.
  3815  func (dcr *ExchangeWallet) findRedemptionsInBlockRange(startBlockHeight, endBlockHeight int64, contractOutpoints []outPoint) {
  3816  	totalContracts := len(contractOutpoints)
  3817  	dcr.log.Debugf("finding redemptions for %d contracts in blocks %d - %d",
  3818  		totalContracts, startBlockHeight, endBlockHeight)
  3819  
  3820  	var lastScannedBlockHeight int64
  3821  	var totalFound, totalCanceled int
  3822  
  3823  rangeBlocks:
  3824  	for blockHeight := startBlockHeight; blockHeight <= endBlockHeight; blockHeight++ {
  3825  		// Get the hash for this block.
  3826  		blockHash, err := dcr.wallet.GetBlockHash(dcr.ctx, blockHeight)
  3827  		if err != nil { // unable to get block hash is a fatal error
  3828  			err = fmt.Errorf("unable to get hash for block %d: %w", blockHeight, err)
  3829  			dcr.fatalFindRedemptionsError(err, contractOutpoints)
  3830  			return
  3831  		}
  3832  
  3833  		// Combine the p2sh scripts for all contracts (excluding contracts whose redemption
  3834  		// have been found) to check against this block's cfilters.
  3835  		dcr.findRedemptionMtx.RLock()
  3836  		contractP2SHScripts := make([][]byte, 0)
  3837  		for _, contractOutpoint := range contractOutpoints {
  3838  			if req, stillInQueue := dcr.findRedemptionQueue[contractOutpoint]; stillInQueue {
  3839  				contractP2SHScripts = append(contractP2SHScripts, req.contractP2SHScript)
  3840  			}
  3841  		}
  3842  		dcr.findRedemptionMtx.RUnlock()
  3843  
  3844  		bingo, err := dcr.wallet.MatchAnyScript(dcr.ctx, blockHash, contractP2SHScripts)
  3845  		if err != nil { // error retrieving a block's cfilters is a fatal error
  3846  			err = fmt.Errorf("MatchAnyScript error for block %d (%s): %w", blockHeight, blockHash, err)
  3847  			dcr.fatalFindRedemptionsError(err, contractOutpoints)
  3848  			return
  3849  		}
  3850  
  3851  		if !bingo {
  3852  			lastScannedBlockHeight = blockHeight
  3853  			continue // block does not reference any of these contracts, continue to next block
  3854  		}
  3855  
  3856  		// Pull the block info to confirm if any of its inputs spends a contract of interest.
  3857  		blk, err := dcr.wallet.GetBlock(dcr.ctx, blockHash)
  3858  		if err != nil { // error pulling a matching block's transactions is a fatal error
  3859  			err = fmt.Errorf("error retrieving transactions for block %d (%s): %w",
  3860  				blockHeight, blockHash, err)
  3861  			dcr.fatalFindRedemptionsError(err, contractOutpoints)
  3862  			return
  3863  		}
  3864  
  3865  		lastScannedBlockHeight = blockHeight
  3866  		scanPoint := fmt.Sprintf("block %d", blockHeight)
  3867  		for _, tx := range append(blk.Transactions, blk.STransactions...) {
  3868  			found, canceled := dcr.findRedemptionsInTx(scanPoint, tx, contractOutpoints)
  3869  			totalFound += found
  3870  			totalCanceled += canceled
  3871  			if totalFound+totalCanceled == totalContracts {
  3872  				break rangeBlocks
  3873  			}
  3874  		}
  3875  	}
  3876  
  3877  	dcr.log.Debugf("%d redemptions found, %d canceled out of %d contracts in blocks %d to %d",
  3878  		totalFound, totalCanceled, totalContracts, startBlockHeight, lastScannedBlockHeight)
  3879  
  3880  	// Search for redemptions in mempool if there are yet unredeemed
  3881  	// contracts after searching this block range.
  3882  	pendingContractsCount := totalContracts - totalFound - totalCanceled
  3883  	if pendingContractsCount > 0 {
  3884  		dcr.findRedemptionMtx.RLock()
  3885  		pendingContracts := make([]outPoint, 0, pendingContractsCount)
  3886  		for _, contractOutpoint := range contractOutpoints {
  3887  			if _, pending := dcr.findRedemptionQueue[contractOutpoint]; pending {
  3888  				pendingContracts = append(pendingContracts, contractOutpoint)
  3889  			}
  3890  		}
  3891  		dcr.findRedemptionMtx.RUnlock()
  3892  		dcr.findRedemptionsInMempool(pendingContracts)
  3893  	}
  3894  }
  3895  
  3896  // findRedemptionsInTx checks if any input of the passed tx spends any of the
  3897  // specified contract outpoints. If spending info is found for any contract, the
  3898  // contract's secret or any error encountered while trying to parse the secret
  3899  // is returned to the redemption finder via the registered result chan; and the
  3900  // contract is purged from the findRedemptionQueue.
  3901  // Returns the number of redemptions found and canceled.
  3902  func (dcr *ExchangeWallet) findRedemptionsInTx(scanPoint string, tx *wire.MsgTx, contractOutpoints []outPoint) (found, cancelled int) {
  3903  	dcr.findRedemptionMtx.Lock()
  3904  	defer dcr.findRedemptionMtx.Unlock()
  3905  
  3906  	redeemTxHash := tx.TxHash()
  3907  
  3908  	for _, contractOutpoint := range contractOutpoints {
  3909  		req, exists := dcr.findRedemptionQueue[contractOutpoint]
  3910  		if !exists {
  3911  			continue // no find request for this outpoint (impossible now?)
  3912  		}
  3913  		if req.canceled() {
  3914  			cancelled++
  3915  			delete(dcr.findRedemptionQueue, contractOutpoint)
  3916  			continue // this find request has been cancelled
  3917  		}
  3918  
  3919  		for i, txIn := range tx.TxIn {
  3920  			prevOut := &txIn.PreviousOutPoint
  3921  			if prevOut.Index != contractOutpoint.vout || prevOut.Hash != contractOutpoint.txHash {
  3922  				continue // input doesn't redeem this contract, check next input
  3923  			}
  3924  			found++
  3925  
  3926  			scriptHash := dexdcr.ExtractScriptHash(req.contractOutputScriptVer, req.contractP2SHScript)
  3927  			secret, err := dexdcr.FindKeyPush(req.contractOutputScriptVer, txIn.SignatureScript, scriptHash, dcr.chainParams)
  3928  			if err != nil {
  3929  				dcr.log.Errorf("Error parsing contract secret for %s from tx input %s:%d in %s: %v",
  3930  					contractOutpoint.String(), redeemTxHash, i, scanPoint, err)
  3931  				req.resultChan <- &findRedemptionResult{
  3932  					Err: err,
  3933  				}
  3934  			} else {
  3935  				dcr.log.Infof("Redemption for contract %s found in tx input %s:%d in %s",
  3936  					contractOutpoint.String(), redeemTxHash, i, scanPoint)
  3937  				req.resultChan <- &findRedemptionResult{
  3938  					RedemptionCoinID: toCoinID(&redeemTxHash, uint32(i)),
  3939  					Secret:           secret,
  3940  				}
  3941  			}
  3942  
  3943  			delete(dcr.findRedemptionQueue, contractOutpoint)
  3944  			break // stop checking inputs for this contract
  3945  		}
  3946  	}
  3947  
  3948  	return
  3949  }
  3950  
  3951  // fatalFindRedemptionsError should be called when an error occurs that prevents
  3952  // redemption search for the specified contracts from continuing reliably. The
  3953  // error will be propagated to the seeker(s) of these contracts' redemptions via
  3954  // the registered result channels and the contracts will be removed from the
  3955  // findRedemptionQueue.
  3956  func (dcr *ExchangeWallet) fatalFindRedemptionsError(err error, contractOutpoints []outPoint) {
  3957  	dcr.findRedemptionMtx.Lock()
  3958  	dcr.log.Debugf("stopping redemption search for %d contracts in queue: %v", len(contractOutpoints), err)
  3959  	for _, contractOutpoint := range contractOutpoints {
  3960  		req, exists := dcr.findRedemptionQueue[contractOutpoint]
  3961  		if !exists {
  3962  			continue
  3963  		}
  3964  		req.resultChan <- &findRedemptionResult{
  3965  			Err: err,
  3966  		}
  3967  		delete(dcr.findRedemptionQueue, contractOutpoint)
  3968  	}
  3969  	dcr.findRedemptionMtx.Unlock()
  3970  }
  3971  
  3972  // Refund refunds a contract. This can only be used after the time lock has
  3973  // expired. This MUST return an asset.CoinNotFoundError error if the coin is
  3974  // spent. If the provided fee rate is zero, an internal estimate will be used,
  3975  // otherwise it will be used directly, but this behavior may change.
  3976  // NOTE: The contract cannot be retrieved from the unspent coin info as the
  3977  // wallet does not store it, even though it was known when the init transaction
  3978  // was created. The client should store this information for persistence across
  3979  // sessions.
  3980  func (dcr *ExchangeWallet) Refund(coinID, contract dex.Bytes, feeRate uint64) (dex.Bytes, error) {
  3981  	// Caller should provide a non-zero fee rate, so we could just do
  3982  	// dcr.feeRateWithFallback(feeRate), but be permissive for now.
  3983  	if feeRate == 0 {
  3984  		feeRate = dcr.targetFeeRateWithFallback(2, 0)
  3985  	}
  3986  	msgTx, refundVal, fee, err := dcr.refundTx(coinID, contract, 0, nil, feeRate)
  3987  	if err != nil {
  3988  		return nil, fmt.Errorf("error creating refund tx: %w", err)
  3989  	}
  3990  
  3991  	refundHash, err := dcr.broadcastTx(msgTx)
  3992  	if err != nil {
  3993  		return nil, err
  3994  	}
  3995  	dcr.addTxToHistory(&asset.WalletTransaction{
  3996  		Type:   asset.Refund,
  3997  		ID:     refundHash.String(),
  3998  		Amount: refundVal,
  3999  		Fees:   fee,
  4000  	}, refundHash, true)
  4001  
  4002  	return toCoinID(refundHash, 0), nil
  4003  }
  4004  
  4005  // refundTx crates and signs a contract's refund transaction. If refundAddr is
  4006  // not supplied, one will be requested from the wallet. If val is not supplied
  4007  // it will be retrieved with gettxout.
  4008  func (dcr *ExchangeWallet) refundTx(coinID, contract dex.Bytes, val uint64, refundAddr stdaddr.Address, feeRate uint64) (tx *wire.MsgTx, refundVal, txFee uint64, err error) {
  4009  	txHash, vout, err := decodeCoinID(coinID)
  4010  	if err != nil {
  4011  		return nil, 0, 0, err
  4012  	}
  4013  	// Grab the output, make sure it's unspent and get the value if not supplied.
  4014  	if val == 0 {
  4015  		utxo, _, spent, err := dcr.lookupTxOutput(dcr.ctx, txHash, vout)
  4016  		if err != nil {
  4017  			return nil, 0, 0, fmt.Errorf("error finding unspent contract: %w", err)
  4018  		}
  4019  		if utxo == nil {
  4020  			return nil, 0, 0, asset.CoinNotFoundError
  4021  		}
  4022  		val = uint64(utxo.Value)
  4023  
  4024  		switch spent {
  4025  		case 0: // unspent, proceed to create refund tx
  4026  		case 1, -1: // spent or unknown
  4027  			// Attempt to identify if it was manually refunded with the backup
  4028  			// transaction, in which case we can skip broadcast and record the
  4029  			// spending transaction we may locate as below.
  4030  
  4031  			// First find the block containing the output itself.
  4032  			scriptAddr, err := stdaddr.NewAddressScriptHashV0(contract, dcr.chainParams)
  4033  			if err != nil {
  4034  				return nil, 0, 0, fmt.Errorf("error encoding contract address: %w", err)
  4035  			}
  4036  			_, pkScript := scriptAddr.PaymentScript()
  4037  			outFound, _, err := dcr.externalTxOutput(dcr.ctx, newOutPoint(txHash, vout),
  4038  				pkScript, time.Now().Add(-60*24*time.Hour)) // search up to 60 days ago
  4039  			if err != nil {
  4040  				return nil, 0, 0, err // possibly the contract is still in mempool
  4041  			}
  4042  			// Try to find a transaction that spends it.
  4043  			spent, err := dcr.isOutputSpent(dcr.ctx, outFound) // => findTxOutSpender
  4044  			if err != nil {
  4045  				return nil, 0, 0, fmt.Errorf("error checking if contract %v:%d is spent: %w", txHash, vout, err)
  4046  			}
  4047  			if spent {
  4048  				spendTx := outFound.spenderTx
  4049  				// Refunds are not batched, so input 0 is always the spender.
  4050  				if dexdcr.IsRefundScript(utxo.Version, spendTx.TxIn[0].SignatureScript, contract) {
  4051  					return spendTx, 0, 0, nil
  4052  				} // otherwise it must be a redeem
  4053  				return nil, 0, 0, fmt.Errorf("contract %s:%d is spent in %v (%w)",
  4054  					txHash, vout, spendTx.TxHash(), asset.CoinNotFoundError)
  4055  			}
  4056  		}
  4057  	}
  4058  
  4059  	sender, _, lockTime, _, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams)
  4060  	if err != nil {
  4061  		return nil, 0, 0, fmt.Errorf("error extracting swap addresses: %w", err)
  4062  	}
  4063  
  4064  	// Create the transaction that spends the contract.
  4065  	msgTx := wire.NewMsgTx()
  4066  	msgTx.LockTime = uint32(lockTime)
  4067  	prevOut := wire.NewOutPoint(txHash, vout, wire.TxTreeRegular)
  4068  	txIn := wire.NewTxIn(prevOut, int64(val), []byte{})
  4069  	// Enable the OP_CHECKLOCKTIMEVERIFY opcode to be used.
  4070  	//
  4071  	// https://github.com/decred/dcrd/blob/8f5270b707daaa1ecf24a1ba02b3ff8a762674d3/txscript/opcode.go#L981-L998
  4072  	txIn.Sequence = wire.MaxTxInSequenceNum - 1
  4073  	msgTx.AddTxIn(txIn)
  4074  	// Calculate fees and add the change output.
  4075  	size := msgTx.SerializeSize() + dexdcr.RefundSigScriptSize + dexdcr.P2PKHOutputSize
  4076  	fee := feeRate * uint64(size)
  4077  	if fee > val {
  4078  		return nil, 0, 0, fmt.Errorf("refund tx not worth the fees")
  4079  	}
  4080  
  4081  	if refundAddr == nil {
  4082  		refundAddr, err = dcr.wallet.ExternalAddress(dcr.ctx, dcr.depositAccount())
  4083  		if err != nil {
  4084  			return nil, 0, 0, fmt.Errorf("error getting new address from the wallet: %w", err)
  4085  		}
  4086  	}
  4087  	pkScriptVer, pkScript := refundAddr.PaymentScript()
  4088  	txOut := newTxOut(int64(val-fee), pkScriptVer, pkScript)
  4089  	// One last check for dust.
  4090  	if dexdcr.IsDust(txOut, feeRate) {
  4091  		return nil, 0, 0, fmt.Errorf("refund output is dust")
  4092  	}
  4093  	msgTx.AddTxOut(txOut)
  4094  	// Sign it.
  4095  	refundSig, refundPubKey, err := dcr.createSig(msgTx, 0, contract, sender)
  4096  	if err != nil {
  4097  		return nil, 0, 0, err
  4098  	}
  4099  	redeemSigScript, err := dexdcr.RefundP2SHContract(contract, refundSig, refundPubKey)
  4100  	if err != nil {
  4101  		return nil, 0, 0, err
  4102  	}
  4103  	txIn.SignatureScript = redeemSigScript
  4104  	return msgTx, val, fee, nil
  4105  }
  4106  
  4107  // DepositAddress returns an address for depositing funds into the exchange
  4108  // wallet.
  4109  func (dcr *ExchangeWallet) DepositAddress() (string, error) {
  4110  	acct := dcr.depositAccount()
  4111  	addr, err := dcr.wallet.ExternalAddress(dcr.ctx, acct)
  4112  	if err != nil {
  4113  		return "", err
  4114  	}
  4115  	return addr.String(), nil
  4116  }
  4117  
  4118  // RedemptionAddress gets an address for use in redeeming the counterparty's
  4119  // swap. This would be included in their swap initialization.
  4120  func (dcr *ExchangeWallet) RedemptionAddress() (string, error) {
  4121  	return dcr.DepositAddress()
  4122  }
  4123  
  4124  // NewAddress returns a new address from the wallet. This satisfies the
  4125  // NewAddresser interface.
  4126  func (dcr *ExchangeWallet) NewAddress() (string, error) {
  4127  	return dcr.DepositAddress()
  4128  }
  4129  
  4130  // Unlock unlocks the exchange wallet.
  4131  func (dcr *ExchangeWallet) Unlock(pw []byte) error {
  4132  	// Older SPV wallet potentially need an upgrade while we have a password.
  4133  	acctsToUnlock := dcr.allAccounts()
  4134  	if upgrader, is := dcr.wallet.(interface {
  4135  		upgradeAccounts(ctx context.Context, pw []byte) error
  4136  	}); is {
  4137  		if err := upgrader.upgradeAccounts(dcr.ctx, pw); err != nil {
  4138  			return fmt.Errorf("error upgrading accounts: %w", err)
  4139  		}
  4140  		// For the native wallet, we unlock all accounts regardless. Otherwise,
  4141  		// the accounts won't be properly unlocked after ConfigureFundsMixer
  4142  		// is called. We could consider taking a password for
  4143  		// ConfigureFundsMixer OR have Core take the password and call Unlock
  4144  		// after ConfigureFundsMixer.
  4145  		acctsToUnlock = nativeAccounts
  4146  	}
  4147  
  4148  	// We must unlock all accounts, including any unmixed account, which is used
  4149  	// to supply keys to the refund path of the swap contract script.
  4150  	for _, acct := range acctsToUnlock {
  4151  		unlocked, err := dcr.wallet.AccountUnlocked(dcr.ctx, acct)
  4152  		if err != nil {
  4153  			return err
  4154  		}
  4155  		if unlocked {
  4156  			continue // attempt to unlock the other account
  4157  		}
  4158  
  4159  		err = dcr.wallet.UnlockAccount(dcr.ctx, pw, acct)
  4160  		if err != nil {
  4161  			return err
  4162  		}
  4163  	}
  4164  	return nil
  4165  }
  4166  
  4167  // Lock locks the exchange wallet.
  4168  func (dcr *ExchangeWallet) Lock() error {
  4169  	accts := dcr.wallet.Accounts()
  4170  	if accts.UnmixedAccount != "" {
  4171  		return fmt.Errorf("cannot lock RPC mixing wallet") // don't lock if mixing is enabled
  4172  	}
  4173  	return dcr.wallet.LockAccount(dcr.ctx, accts.PrimaryAccount)
  4174  }
  4175  
  4176  // Locked will be true if the wallet is currently locked.
  4177  // Q: why are we ignoring RPC errors in this?
  4178  func (dcr *ExchangeWallet) Locked() bool {
  4179  	for _, acct := range dcr.allAccounts() {
  4180  		unlocked, err := dcr.wallet.AccountUnlocked(dcr.ctx, acct)
  4181  		if err != nil {
  4182  			dcr.log.Errorf("error checking account lock status %v", err)
  4183  			unlocked = false // assume wallet is unlocked?
  4184  		}
  4185  		if !unlocked {
  4186  			return true // Locked is true if any of the funding accounts is locked.
  4187  		}
  4188  	}
  4189  	return false
  4190  }
  4191  
  4192  func bondPushDataScript(ver uint16, acctID []byte, lockTimeSec int64, pkh []byte) ([]byte, error) {
  4193  	pushData := make([]byte, 2+len(acctID)+4+20)
  4194  	var offset int
  4195  	binary.BigEndian.PutUint16(pushData[offset:], ver)
  4196  	offset += 2
  4197  	copy(pushData[offset:], acctID[:])
  4198  	offset += len(acctID)
  4199  	binary.BigEndian.PutUint32(pushData[offset:], uint32(lockTimeSec))
  4200  	offset += 4
  4201  	copy(pushData[offset:], pkh)
  4202  	return txscript.NewScriptBuilder().
  4203  		AddOp(txscript.OP_RETURN).
  4204  		AddData(pushData).
  4205  		Script()
  4206  }
  4207  
  4208  // MakeBondTx creates a time-locked fidelity bond transaction. The V0
  4209  // transaction has two required outputs:
  4210  //
  4211  // Output 0 is a the time-locked bond output of type P2SH with the provided
  4212  // value. The redeem script looks similar to the refund path of an atomic swap
  4213  // script, but with a pubkey hash:
  4214  //
  4215  //	<locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 <pubkeyhash[20]> OP_EQUALVERIFY OP_CHECKSIG
  4216  //
  4217  // The pubkey referenced by the script is provided by the caller.
  4218  //
  4219  // Output 1 is a DEX Account commitment. This is an OP_RETURN output that
  4220  // references the provided account ID.
  4221  //
  4222  //	OP_RETURN <2-byte version> <32-byte account ID> <4-byte locktime> <20-byte pubkey hash>
  4223  //
  4224  // Having the account ID in the raw allows the txn alone to identify the account
  4225  // without the bond output's redeem script.
  4226  //
  4227  // Output 2 is change, if any.
  4228  //
  4229  // The bond output's redeem script, which is needed to spend the bond output, is
  4230  // returned as the Data field of the Bond. The bond output pays to a pubkeyhash
  4231  // script for a wallet address. Bond.RedeemTx is a backup transaction that
  4232  // spends the bond output after lockTime passes, paying to an address for the
  4233  // current underlying wallet; the bond private key should normally be used to
  4234  // author a new transaction paying to a new address instead.
  4235  func (dcr *ExchangeWallet) MakeBondTx(ver uint16, amt, feeRate uint64, lockTime time.Time,
  4236  	bondKey *secp256k1.PrivateKey, acctID []byte) (*asset.Bond, func(), error) {
  4237  	if ver != 0 {
  4238  		return nil, nil, errors.New("only version 0 bonds supported")
  4239  	}
  4240  	if until := time.Until(lockTime); until >= 365*12*time.Hour /* ~6 months */ {
  4241  		return nil, nil, fmt.Errorf("that lock time is nuts: %v", lockTime)
  4242  	} else if until < 0 {
  4243  		return nil, nil, fmt.Errorf("that lock time is already passed: %v", lockTime)
  4244  	}
  4245  
  4246  	pk := bondKey.PubKey().SerializeCompressed()
  4247  	pkh := stdaddr.Hash160(pk)
  4248  
  4249  	feeRate = dcr.feeRateWithFallback(feeRate)
  4250  	baseTx := wire.NewMsgTx()
  4251  	const scriptVersion = 0
  4252  
  4253  	// TL output.
  4254  	lockTimeSec := lockTime.Unix()
  4255  	if lockTimeSec >= dexdcr.MaxCLTVScriptNum || lockTimeSec <= 0 {
  4256  		return nil, nil, fmt.Errorf("invalid lock time %v", lockTime)
  4257  	}
  4258  	bondScript, err := dexdcr.MakeBondScript(ver, uint32(lockTimeSec), pkh)
  4259  	if err != nil {
  4260  		return nil, nil, fmt.Errorf("failed to build bond output redeem script: %w", err)
  4261  	}
  4262  	bondAddr, err := stdaddr.NewAddressScriptHash(scriptVersion, bondScript, dcr.chainParams)
  4263  	if err != nil {
  4264  		return nil, nil, fmt.Errorf("failed to build bond output payment script: %w", err)
  4265  	}
  4266  	bondPkScriptVer, bondPkScript := bondAddr.PaymentScript()
  4267  	txOut := newTxOut(int64(amt), bondPkScriptVer, bondPkScript)
  4268  	if dexdcr.IsDust(txOut, feeRate) {
  4269  		return nil, nil, fmt.Errorf("bond output is dust")
  4270  	}
  4271  	baseTx.AddTxOut(txOut)
  4272  
  4273  	// Acct ID commitment and bond details output, v0. The integers are encoded
  4274  	// with big-endian byte order and a fixed number of bytes, unlike in Script,
  4275  	// for natural visual inspection of the version and lock time.
  4276  	commitPkScript, err := bondPushDataScript(ver, acctID, lockTimeSec, pkh)
  4277  	if err != nil {
  4278  		return nil, nil, fmt.Errorf("failed to build acct commit output script: %w", err)
  4279  	}
  4280  	acctOut := newTxOut(0, scriptVersion, commitPkScript) // value zero
  4281  	baseTx.AddTxOut(acctOut)
  4282  
  4283  	// NOTE: this "fund -> addInputCoins -> signTxAndAddChange -> lock prevouts"
  4284  	// sequence might be best encapsulated in a fundRawTransactionMethod.
  4285  	baseSize := uint32(baseTx.SerializeSize()) + dexdcr.P2PKHOutputSize // uint32(dexdcr.MsgTxOverhead + dexdcr.P2PKHOutputSize*3)
  4286  	enough := sendEnough(amt, feeRate, false, baseSize, true)
  4287  	coins, _, _, _, err := dcr.fund(0, enough)
  4288  	if err != nil {
  4289  		return nil, nil, fmt.Errorf("Unable to send %s DCR with fee rate of %d atoms/byte: %w",
  4290  			amount(amt), feeRate, err)
  4291  	}
  4292  
  4293  	var txIDToRemoveFromHistory *chainhash.Hash // will be non-nil if tx was added to history
  4294  
  4295  	abandon := func() { // if caller does not broadcast, or we fail in this method
  4296  		_, err := dcr.returnCoins(coins)
  4297  		if err != nil {
  4298  			dcr.log.Errorf("error returning coins for unused bond tx: %v", coins)
  4299  		}
  4300  		if txIDToRemoveFromHistory != nil {
  4301  			dcr.removeTxFromHistory(txIDToRemoveFromHistory)
  4302  		}
  4303  	}
  4304  
  4305  	var success bool
  4306  	defer func() {
  4307  		if !success {
  4308  			abandon()
  4309  		}
  4310  	}()
  4311  
  4312  	_, err = dcr.addInputCoins(baseTx, coins)
  4313  	if err != nil {
  4314  		return nil, nil, err
  4315  	}
  4316  
  4317  	signedTx, _, _, fee, err := dcr.signTxAndAddChange(baseTx, feeRate, -1, dcr.depositAccount())
  4318  	if err != nil {
  4319  		return nil, nil, err
  4320  	}
  4321  	txHash := signedTx.CachedTxHash() // spentAmt := amt + fees
  4322  
  4323  	signedTxBytes, err := signedTx.Bytes()
  4324  	if err != nil {
  4325  		return nil, nil, err
  4326  	}
  4327  	unsignedTxBytes, err := baseTx.Bytes()
  4328  	if err != nil {
  4329  		return nil, nil, err
  4330  	}
  4331  
  4332  	// Prep the redeem / refund tx.
  4333  	redeemMsgTx, err := dcr.makeBondRefundTxV0(txHash, 0, amt, bondScript, bondKey, feeRate)
  4334  	if err != nil {
  4335  		return nil, nil, fmt.Errorf("unable to create bond redemption tx: %w", err)
  4336  	}
  4337  	redeemTx, err := redeemMsgTx.Bytes()
  4338  	if err != nil {
  4339  		return nil, nil, fmt.Errorf("failed to serialize bond redemption tx: %w", err)
  4340  	}
  4341  
  4342  	bond := &asset.Bond{
  4343  		Version:    ver,
  4344  		AssetID:    BipID,
  4345  		Amount:     amt,
  4346  		CoinID:     toCoinID(txHash, 0),
  4347  		Data:       bondScript,
  4348  		SignedTx:   signedTxBytes,
  4349  		UnsignedTx: unsignedTxBytes,
  4350  		RedeemTx:   redeemTx,
  4351  	}
  4352  	success = true
  4353  
  4354  	bondInfo := &asset.BondTxInfo{
  4355  		AccountID: acctID,
  4356  		LockTime:  uint64(lockTimeSec),
  4357  		BondID:    pkh,
  4358  	}
  4359  	dcr.addTxToHistory(&asset.WalletTransaction{
  4360  		Type:     asset.CreateBond,
  4361  		ID:       txHash.String(),
  4362  		Amount:   amt,
  4363  		Fees:     fee,
  4364  		BondInfo: bondInfo,
  4365  	}, txHash, false)
  4366  
  4367  	txIDToRemoveFromHistory = txHash
  4368  
  4369  	return bond, abandon, nil
  4370  }
  4371  
  4372  func (dcr *ExchangeWallet) makeBondRefundTxV0(txid *chainhash.Hash, vout uint32, amt uint64,
  4373  	script []byte, priv *secp256k1.PrivateKey, feeRate uint64) (*wire.MsgTx, error) {
  4374  	lockTime, pkhPush, err := dexdcr.ExtractBondDetailsV0(0, script)
  4375  	if err != nil {
  4376  		return nil, err
  4377  	}
  4378  
  4379  	pk := priv.PubKey().SerializeCompressed()
  4380  	pkh := stdaddr.Hash160(pk)
  4381  	if !bytes.Equal(pkh, pkhPush) {
  4382  		return nil, asset.ErrIncorrectBondKey
  4383  	}
  4384  
  4385  	redeemMsgTx := wire.NewMsgTx()
  4386  	// Transaction LockTime must be <= spend time, and >= the CLTV lockTime, so
  4387  	// we use exactly the CLTV's value. This limits the CLTV value to 32-bits.
  4388  	redeemMsgTx.LockTime = lockTime
  4389  	bondPrevOut := wire.NewOutPoint(txid, vout, wire.TxTreeRegular)
  4390  	txIn := wire.NewTxIn(bondPrevOut, int64(amt), []byte{})
  4391  	txIn.Sequence = wire.MaxTxInSequenceNum - 1 // not finalized, do not disable cltv
  4392  	redeemMsgTx.AddTxIn(txIn)
  4393  
  4394  	// Calculate fees and add the refund output.
  4395  	redeemSize := redeemMsgTx.SerializeSize() + dexdcr.RedeemBondSigScriptSize + dexdcr.P2PKHOutputSize
  4396  	fee := feeRate * uint64(redeemSize)
  4397  	if fee > amt {
  4398  		return nil, fmt.Errorf("irredeemable bond at fee rate %d atoms/byte", feeRate)
  4399  	}
  4400  
  4401  	redeemAddr, err := dcr.wallet.ExternalAddress(dcr.ctx, dcr.wallet.Accounts().PrimaryAccount)
  4402  	if err != nil {
  4403  		return nil, fmt.Errorf("error getting new address from the wallet: %w", translateRPCCancelErr(err))
  4404  	}
  4405  	redeemScriptVer, redeemPkScript := redeemAddr.PaymentScript()
  4406  	redeemTxOut := newTxOut(int64(amt-fee), redeemScriptVer, redeemPkScript)
  4407  	if dexdcr.IsDust(redeemTxOut, feeRate) { // hard to imagine
  4408  		return nil, fmt.Errorf("redeem output is dust")
  4409  	}
  4410  	redeemMsgTx.AddTxOut(redeemTxOut)
  4411  
  4412  	// CalcSignatureHash and ecdsa.Sign with secp256k1 private key.
  4413  	redeemInSig, err := sign.RawTxInSignature(redeemMsgTx, 0, script, txscript.SigHashAll,
  4414  		priv.Serialize(), dcrec.STEcdsaSecp256k1)
  4415  	if err != nil {
  4416  		return nil, fmt.Errorf("error creating signature for bond redeem input script '%v': %w", redeemAddr, err)
  4417  	}
  4418  
  4419  	bondRedeemSigScript, err := dexdcr.RefundBondScript(script, redeemInSig, pk)
  4420  	if err != nil {
  4421  		return nil, fmt.Errorf("failed to build bond redeem input script: %w", err)
  4422  	}
  4423  	redeemMsgTx.TxIn[0].SignatureScript = bondRedeemSigScript
  4424  
  4425  	return redeemMsgTx, nil
  4426  }
  4427  
  4428  // RefundBond refunds a bond output to a new wallet address given the redeem
  4429  // script and private key. After broadcasting, the output paying to the wallet
  4430  // is returned.
  4431  func (dcr *ExchangeWallet) RefundBond(ctx context.Context, ver uint16, coinID, script []byte,
  4432  	amt uint64, privKey *secp256k1.PrivateKey) (asset.Coin, error) {
  4433  	if ver != 0 {
  4434  		return nil, errors.New("only version 0 bonds supported")
  4435  	}
  4436  	lockTime, pkhPush, err := dexdcr.ExtractBondDetailsV0(0, script)
  4437  	if err != nil {
  4438  		return nil, err
  4439  	}
  4440  	txHash, vout, err := decodeCoinID(coinID)
  4441  	if err != nil {
  4442  		return nil, err
  4443  	}
  4444  
  4445  	feeRate := dcr.targetFeeRateWithFallback(2, 0)
  4446  
  4447  	msgTx, err := dcr.makeBondRefundTxV0(txHash, vout, amt, script, privKey, feeRate)
  4448  	if err != nil {
  4449  		return nil, err
  4450  	}
  4451  
  4452  	redeemHash, err := dcr.wallet.SendRawTransaction(ctx, msgTx, false)
  4453  	if err != nil { // TODO: we need to be much smarter about these send error types/codes
  4454  		return nil, translateRPCCancelErr(err)
  4455  	}
  4456  
  4457  	refundAmt := msgTx.TxOut[0].Value
  4458  	bondInfo := &asset.BondTxInfo{
  4459  		LockTime: uint64(lockTime),
  4460  		BondID:   pkhPush,
  4461  	}
  4462  	dcr.addTxToHistory(&asset.WalletTransaction{
  4463  		Type:     asset.RedeemBond,
  4464  		ID:       redeemHash.String(),
  4465  		Amount:   amt,
  4466  		Fees:     amt - uint64(refundAmt),
  4467  		BondInfo: bondInfo,
  4468  	}, redeemHash, true)
  4469  
  4470  	return newOutput(redeemHash, 0, uint64(refundAmt), wire.TxTreeRegular), nil
  4471  
  4472  	/* If we need to find the actual unspent bond transaction for any of:
  4473  	   (1) the output amount, (2) the commitment output data, or (3) to ensure
  4474  	   it is unspent, we can locate it as follows:
  4475  
  4476  	// First try without cfilters (gettxout or gettransaction). If bond was
  4477  	// funded by this wallet or had a change output paying to this wallet, it
  4478  	// should be found here.
  4479  	txOut, _, spent, err := dcr.lookupTxOutput(ctx, txHash, vout)
  4480  	if err == nil {
  4481  		if spent {
  4482  			return nil, errors.New("bond already spent")
  4483  		}
  4484  		return dcr.makeBondRefundTxV0(txHash, vout, uint64(txOut.Value), script, privKey, feeRate)
  4485  	}
  4486  	if !errors.Is(err, asset.CoinNotFoundError) {
  4487  		dcr.log.Warnf("Unexpected error looking up bond output %v:%d", txHash, vout)
  4488  	}
  4489  
  4490  	// Try block filters. This would only be required if the bond tx is foreign.
  4491  	// In general, the bond should have been created with this wallet.
  4492  	// I was hesitant to even support this, but might as well cover this edge.
  4493  	// NOTE: An alternative is to have the caller provide the amount, which is
  4494  	// all we're getting from the located tx output!
  4495  	scriptAddr, err := stdaddr.NewAddressScriptHashV0(script, dcr.chainParams)
  4496  	if err != nil {
  4497  		return nil, fmt.Errorf("error encoding script address: %w", err)
  4498  	}
  4499  	_, pkScript := scriptAddr.PaymentScript()
  4500  	outFound, _, err := dcr.externalTxOutput(dcr.ctx, newOutPoint(txHash, vout),
  4501  		pkScript, time.Now().Add(-365*24*time.Hour)) // long!
  4502  	if err != nil {
  4503  		return nil, err // may be asset.CoinNotFoundError
  4504  	}
  4505  	txOut = outFound.TxOut // outFound.tree
  4506  	spent, err = dcr.isOutputSpent(ctx, outFound)
  4507  	if err != nil {
  4508  		return nil, fmt.Errorf("error checking if output %v:%d is spent: %w", txHash, vout, err)
  4509  	}
  4510  	if spent {
  4511  		return nil, errors.New("bond already spent")
  4512  	}
  4513  
  4514  	return dcr.makeBondRefundTxV0(txHash, vout, uint64(txOut.Value), script, privKey, feeRate)
  4515  	*/
  4516  }
  4517  
  4518  // FindBond finds the bond with coinID and returns the values used to create it.
  4519  func (dcr *ExchangeWallet) FindBond(ctx context.Context, coinID []byte, searchUntil time.Time) (bond *asset.BondDetails, err error) {
  4520  	txHash, vout, err := decodeCoinID(coinID)
  4521  	if err != nil {
  4522  		return nil, err
  4523  	}
  4524  
  4525  	decodeV0BondTx := func(msgTx *wire.MsgTx) (*asset.BondDetails, error) {
  4526  		if len(msgTx.TxOut) < 2 {
  4527  			return nil, fmt.Errorf("tx %s is not a v0 bond transaction: too few outputs", txHash)
  4528  		}
  4529  		_, lockTime, pkh, err := dexdcr.ExtractBondCommitDataV0(0, msgTx.TxOut[1].PkScript)
  4530  		if err != nil {
  4531  			return nil, fmt.Errorf("unable to extract bond commitment details from output 1 of %s: %v", txHash, err)
  4532  		}
  4533  		// Sanity check.
  4534  		bondScript, err := dexdcr.MakeBondScript(0, lockTime, pkh[:])
  4535  		if err != nil {
  4536  			return nil, fmt.Errorf("failed to build bond output redeem script: %w", err)
  4537  		}
  4538  		bondAddr, err := stdaddr.NewAddressScriptHash(0, bondScript, dcr.chainParams)
  4539  		if err != nil {
  4540  			return nil, fmt.Errorf("failed to build bond output payment script: %w", err)
  4541  		}
  4542  		_, bondScriptWOpcodes := bondAddr.PaymentScript()
  4543  		if !bytes.Equal(bondScriptWOpcodes, msgTx.TxOut[0].PkScript) {
  4544  			return nil, fmt.Errorf("bond script does not match commit data for %s: %x != %x",
  4545  				txHash, bondScript, msgTx.TxOut[0].PkScript)
  4546  		}
  4547  		return &asset.BondDetails{
  4548  			Bond: &asset.Bond{
  4549  				Version: 0,
  4550  				AssetID: BipID,
  4551  				Amount:  uint64(msgTx.TxOut[0].Value),
  4552  				CoinID:  coinID,
  4553  				Data:    bondScript,
  4554  				//
  4555  				// SignedTx and UnsignedTx not populated because this is
  4556  				// an already posted bond and these fields are no longer used.
  4557  				// SignedTx, UnsignedTx []byte
  4558  				//
  4559  				// RedeemTx cannot be populated because we do not have
  4560  				// the private key that only core knows. Core will need
  4561  				// the BondPKH to determine what the private key was.
  4562  				// RedeemTx []byte
  4563  			},
  4564  			LockTime: time.Unix(int64(lockTime), 0),
  4565  			CheckPrivKey: func(bondKey *secp256k1.PrivateKey) bool {
  4566  				pk := bondKey.PubKey().SerializeCompressed()
  4567  				pkhB := stdaddr.Hash160(pk)
  4568  				return bytes.Equal(pkh[:], pkhB)
  4569  			},
  4570  		}, nil
  4571  	}
  4572  
  4573  	// If the bond was funded by this wallet or had a change output paying
  4574  	// to this wallet, it should be found here.
  4575  	tx, err := dcr.wallet.GetTransaction(ctx, txHash)
  4576  	if err == nil {
  4577  		return decodeV0BondTx(tx.MsgTx)
  4578  	}
  4579  	if !errors.Is(err, asset.CoinNotFoundError) {
  4580  		dcr.log.Warnf("Unexpected error looking up bond output %v:%d", txHash, vout)
  4581  	}
  4582  
  4583  	// The bond was not funded by this wallet or had no change output when
  4584  	// restored from seed. This is not a problem. However, we are unable to
  4585  	// use filters because we don't know any output scripts. Brute force
  4586  	// finding the transaction.
  4587  	blockHash, _, err := dcr.wallet.GetBestBlock(ctx)
  4588  	if err != nil {
  4589  		return nil, fmt.Errorf("unable to get best hash: %v", err)
  4590  	}
  4591  	var (
  4592  		blk   *wire.MsgBlock
  4593  		msgTx *wire.MsgTx
  4594  	)
  4595  out:
  4596  	for {
  4597  		if err := ctx.Err(); err != nil {
  4598  			return nil, fmt.Errorf("bond search stopped: %w", err)
  4599  		}
  4600  		blk, err = dcr.wallet.GetBlock(ctx, blockHash)
  4601  		if err != nil {
  4602  			return nil, fmt.Errorf("error retrieving block %s: %w", blockHash, err)
  4603  		}
  4604  		if blk.Header.Timestamp.Before(searchUntil) {
  4605  			return nil, fmt.Errorf("searched blocks until %v but did not find the bond tx %s", searchUntil, txHash)
  4606  		}
  4607  		for _, tx := range blk.Transactions {
  4608  			if tx.TxHash() == *txHash {
  4609  				dcr.log.Debugf("Found mined tx %s in block %s.", txHash, blk.BlockHash())
  4610  				msgTx = tx
  4611  				break out
  4612  			}
  4613  		}
  4614  
  4615  		if string(blk.Header.PrevBlock[:]) == "" {
  4616  			return nil, fmt.Errorf("did not find the bond tx %s", txHash)
  4617  		}
  4618  
  4619  		blockHash = &blk.Header.PrevBlock
  4620  	}
  4621  	return decodeV0BondTx(msgTx)
  4622  }
  4623  
  4624  // SendTransaction broadcasts a valid fully-signed transaction.
  4625  func (dcr *ExchangeWallet) SendTransaction(rawTx []byte) ([]byte, error) {
  4626  	msgTx, err := msgTxFromBytes(rawTx)
  4627  	if err != nil {
  4628  		return nil, err
  4629  	}
  4630  	txHash, err := dcr.wallet.SendRawTransaction(dcr.ctx, msgTx, false)
  4631  	if err != nil {
  4632  		return nil, translateRPCCancelErr(err)
  4633  	}
  4634  	dcr.markTxAsSubmitted(txHash)
  4635  	return toCoinID(txHash, 0), nil
  4636  }
  4637  
  4638  // Withdraw withdraws funds to the specified address. Fees are subtracted from
  4639  // the value. feeRate is in units of atoms/byte.
  4640  // Withdraw satisfies asset.Withdrawer.
  4641  func (dcr *ExchangeWallet) Withdraw(address string, value, feeRate uint64) (asset.Coin, error) {
  4642  	addr, err := stdaddr.DecodeAddress(address, dcr.chainParams)
  4643  	if err != nil {
  4644  		return nil, fmt.Errorf("invalid address: %s", address)
  4645  	}
  4646  	msgTx, sentVal, err := dcr.withdraw(addr, value, dcr.feeRateWithFallback(feeRate))
  4647  	if err != nil {
  4648  		return nil, err
  4649  	}
  4650  
  4651  	selfSend, err := dcr.OwnsDepositAddress(address)
  4652  	if err != nil {
  4653  		dcr.log.Errorf("error checking if address %q is owned: %v", address, err)
  4654  	}
  4655  	txType := asset.Send
  4656  	if selfSend {
  4657  		txType = asset.SelfSend
  4658  	}
  4659  
  4660  	dcr.addTxToHistory(&asset.WalletTransaction{
  4661  		Type:      txType,
  4662  		ID:        msgTx.CachedTxHash().String(),
  4663  		Amount:    sentVal,
  4664  		Fees:      value - sentVal,
  4665  		Recipient: &address,
  4666  	}, msgTx.CachedTxHash(), true)
  4667  
  4668  	return newOutput(msgTx.CachedTxHash(), 0, sentVal, wire.TxTreeRegular), nil
  4669  }
  4670  
  4671  // Send sends the exact value to the specified address. This is different from
  4672  // Withdraw, which subtracts the tx fees from the amount sent. feeRate is in
  4673  // units of atoms/byte.
  4674  func (dcr *ExchangeWallet) Send(address string, value, feeRate uint64) (asset.Coin, error) {
  4675  	addr, err := stdaddr.DecodeAddress(address, dcr.chainParams)
  4676  	if err != nil {
  4677  		return nil, fmt.Errorf("invalid address: %s", address)
  4678  	}
  4679  	msgTx, sentVal, fee, err := dcr.sendToAddress(addr, value, dcr.feeRateWithFallback(feeRate))
  4680  	if err != nil {
  4681  		return nil, err
  4682  	}
  4683  
  4684  	selfSend, err := dcr.OwnsDepositAddress(address)
  4685  	if err != nil {
  4686  		dcr.log.Errorf("error checking if address %q is owned: %v", address, err)
  4687  	}
  4688  	txType := asset.Send
  4689  	if selfSend {
  4690  		txType = asset.SelfSend
  4691  	}
  4692  
  4693  	dcr.addTxToHistory(&asset.WalletTransaction{
  4694  		Type:      txType,
  4695  		ID:        msgTx.CachedTxHash().String(),
  4696  		Amount:    sentVal,
  4697  		Fees:      fee,
  4698  		Recipient: &address,
  4699  	}, msgTx.CachedTxHash(), true)
  4700  
  4701  	return newOutput(msgTx.CachedTxHash(), 0, sentVal, wire.TxTreeRegular), nil
  4702  }
  4703  
  4704  // ValidateSecret checks that the secret satisfies the contract.
  4705  func (dcr *ExchangeWallet) ValidateSecret(secret, secretHash []byte) bool {
  4706  	h := sha256.Sum256(secret)
  4707  	return bytes.Equal(h[:], secretHash)
  4708  }
  4709  
  4710  // SwapConfirmations gets the number of confirmations and the spend status for
  4711  // the specified swap. The contract and matchTime are provided so that wallets
  4712  // may search for the coin using light filters.
  4713  //
  4714  // For a non-SPV wallet, if the swap appears spent but it cannot be located in a
  4715  // block with a cfilters scan, this will return asset.CoinNotFoundError. For SPV
  4716  // wallets, it is not an error if the transaction cannot be located SPV wallets
  4717  // cannot see non-wallet transactions until they are mined.
  4718  //
  4719  // If the coin is located, but recognized as spent, no error is returned.
  4720  func (dcr *ExchangeWallet) SwapConfirmations(ctx context.Context, coinID, contract dex.Bytes, matchTime time.Time) (confs uint32, spent bool, err error) {
  4721  	txHash, vout, err := decodeCoinID(coinID)
  4722  	if err != nil {
  4723  		return 0, false, err
  4724  	}
  4725  
  4726  	ctx, cancel := context.WithTimeout(ctx, confCheckTimeout)
  4727  	defer cancel()
  4728  
  4729  	// Check if we can find the contract onchain without using cfilters.
  4730  	var spendFlag int8
  4731  	_, confs, spendFlag, err = dcr.lookupTxOutput(ctx, txHash, vout)
  4732  	if err == nil {
  4733  		if spendFlag != -1 {
  4734  			return confs, spendFlag > 0, nil
  4735  		} // else go on to block filters scan
  4736  	} else if !errors.Is(err, asset.CoinNotFoundError) {
  4737  		return 0, false, err
  4738  	}
  4739  
  4740  	// Prepare the pkScript to find the contract output using block filters.
  4741  	scriptAddr, err := stdaddr.NewAddressScriptHashV0(contract, dcr.chainParams)
  4742  	if err != nil {
  4743  		return 0, false, fmt.Errorf("error encoding script address: %w", err)
  4744  	}
  4745  	_, p2shScript := scriptAddr.PaymentScript()
  4746  
  4747  	// Find the contract and its spend status using block filters.
  4748  	confs, spent, err = dcr.lookupTxOutWithBlockFilters(ctx, newOutPoint(txHash, vout), p2shScript, matchTime)
  4749  	// Don't trouble the caller if we're using an SPV wallet and the transaction
  4750  	// cannot be located.
  4751  	if errors.Is(err, asset.CoinNotFoundError) && dcr.wallet.SpvMode() {
  4752  		dcr.log.Debugf("SwapConfirmations - cfilters scan did not find %v:%d. "+
  4753  			"Assuming in mempool.", txHash, vout)
  4754  		err = nil
  4755  	}
  4756  	return confs, spent, err
  4757  }
  4758  
  4759  // RegFeeConfirmations gets the number of confirmations for the specified
  4760  // output.
  4761  func (dcr *ExchangeWallet) RegFeeConfirmations(ctx context.Context, coinID dex.Bytes) (confs uint32, err error) {
  4762  	txHash, _, err := decodeCoinID(coinID)
  4763  	if err != nil {
  4764  		return 0, err
  4765  	}
  4766  	tx, err := dcr.wallet.GetTransaction(ctx, txHash)
  4767  	if err != nil {
  4768  		return 0, err
  4769  	}
  4770  	return uint32(tx.Confirmations), nil
  4771  }
  4772  
  4773  // addInputCoins adds inputs to the MsgTx to spend the specified outputs.
  4774  func (dcr *ExchangeWallet) addInputCoins(msgTx *wire.MsgTx, coins asset.Coins) (uint64, error) {
  4775  	var totalIn uint64
  4776  	for _, coin := range coins {
  4777  		op, err := dcr.convertCoin(coin)
  4778  		if err != nil {
  4779  			return 0, err
  4780  		}
  4781  		if op.value == 0 {
  4782  			return 0, fmt.Errorf("zero-valued output detected for %s:%d", op.txHash(), op.vout())
  4783  		}
  4784  		if op.tree == wire.TxTreeUnknown { // Set the correct prevout tree if unknown.
  4785  			unspentPrevOut, err := dcr.wallet.UnspentOutput(dcr.ctx, op.txHash(), op.vout(), op.tree)
  4786  			if err != nil {
  4787  				return 0, fmt.Errorf("unable to determine tree for prevout %s: %v", op.pt, err)
  4788  			}
  4789  			op.tree = unspentPrevOut.Tree
  4790  		}
  4791  		totalIn += op.value
  4792  		prevOut := op.wireOutPoint()
  4793  		txIn := wire.NewTxIn(prevOut, int64(op.value), []byte{})
  4794  		msgTx.AddTxIn(txIn)
  4795  	}
  4796  	return totalIn, nil
  4797  }
  4798  
  4799  func (dcr *ExchangeWallet) shutdown() {
  4800  	dcr.bondReserves.Store(0)
  4801  	// or should it remember reserves in case we reconnect? There's a
  4802  	// reReserveFunds Core method for this... unclear
  4803  
  4804  	// Close all open channels for contract redemption searches
  4805  	// to prevent leakages and ensure goroutines that are started
  4806  	// to wait on these channels end gracefully.
  4807  	dcr.findRedemptionMtx.Lock()
  4808  	for contractOutpoint, req := range dcr.findRedemptionQueue {
  4809  		close(req.resultChan)
  4810  		delete(dcr.findRedemptionQueue, contractOutpoint)
  4811  	}
  4812  	dcr.findRedemptionMtx.Unlock()
  4813  
  4814  	// Disconnect the wallet. For rpc wallets, this shuts down
  4815  	// the rpcclient.Client.
  4816  	if dcr.wallet != nil {
  4817  		dcr.wallet.Disconnect()
  4818  	}
  4819  }
  4820  
  4821  // SyncStatus is information about the blockchain sync status.
  4822  func (dcr *ExchangeWallet) SyncStatus() (ss *asset.SyncStatus, err error) {
  4823  	defer func() {
  4824  		var synced bool
  4825  		if ss != nil {
  4826  			synced = ss.Synced
  4827  		}
  4828  
  4829  		if wasSynced := dcr.previouslySynced.Swap(synced); synced && !wasSynced {
  4830  			tip := dcr.cachedBestBlock()
  4831  			go dcr.syncTxHistory(dcr.ctx, uint64(tip.height))
  4832  		}
  4833  	}()
  4834  
  4835  	// If we have a rescan running, do different math.
  4836  	dcr.rescan.RLock()
  4837  	rescanProgress := dcr.rescan.progress
  4838  	dcr.rescan.RUnlock()
  4839  
  4840  	if rescanProgress != nil {
  4841  		height := dcr.cachedBestBlock().height
  4842  		if height < rescanProgress.scannedThrough {
  4843  			height = rescanProgress.scannedThrough
  4844  		}
  4845  		txHeight := uint64(rescanProgress.scannedThrough)
  4846  		return &asset.SyncStatus{
  4847  			Synced:         false,
  4848  			TargetHeight:   uint64(height),
  4849  			StartingBlocks: dcr.startingBlocks.Load(),
  4850  			Blocks:         uint64(height),
  4851  			Transactions:   &txHeight,
  4852  		}, nil
  4853  	}
  4854  
  4855  	// No rescan in progress. Ask wallet.
  4856  	ss, err = dcr.wallet.SyncStatus(dcr.ctx)
  4857  	if err != nil {
  4858  		return nil, err
  4859  	}
  4860  	ss.StartingBlocks = dcr.startingBlocks.Load()
  4861  	return ss, nil
  4862  }
  4863  
  4864  // Combines the RPC type with the spending input information.
  4865  type compositeUTXO struct {
  4866  	rpc   *walletjson.ListUnspentResult
  4867  	input *dexdcr.SpendInfo
  4868  	confs int64
  4869  	// TODO: consider including isDexChange bool for consumer
  4870  }
  4871  
  4872  // parseUTXOs constructs and returns a list of compositeUTXOs from the provided
  4873  // set of RPC utxos, including basic information required to spend each rpc utxo.
  4874  // The returned list is sorted by ascending value.
  4875  func (dcr *ExchangeWallet) parseUTXOs(unspents []*walletjson.ListUnspentResult) ([]*compositeUTXO, error) {
  4876  	utxos := make([]*compositeUTXO, 0, len(unspents))
  4877  	for _, utxo := range unspents {
  4878  		if !utxo.Spendable {
  4879  			continue
  4880  		}
  4881  		scriptPK, err := hex.DecodeString(utxo.ScriptPubKey)
  4882  		if err != nil {
  4883  			return nil, fmt.Errorf("error decoding pubkey script for %s, script = %s: %w", utxo.TxID, utxo.ScriptPubKey, err)
  4884  		}
  4885  		redeemScript, err := hex.DecodeString(utxo.RedeemScript)
  4886  		if err != nil {
  4887  			return nil, fmt.Errorf("error decoding redeem script for %s, script = %s: %w", utxo.TxID, utxo.RedeemScript, err)
  4888  		}
  4889  
  4890  		// NOTE: listunspent does not indicate script version, so for the
  4891  		// purposes of our funding coins, we are going to assume 0.
  4892  		nfo, err := dexdcr.InputInfo(0, scriptPK, redeemScript, dcr.chainParams)
  4893  		if err != nil {
  4894  			if errors.Is(err, dex.UnsupportedScriptError) {
  4895  				continue
  4896  			}
  4897  			return nil, fmt.Errorf("error reading asset info: %w", err)
  4898  		}
  4899  		if nfo.ScriptType == dexdcr.ScriptUnsupported || nfo.NonStandardScript {
  4900  			// InputInfo sets NonStandardScript for P2SH with non-standard
  4901  			// redeem scripts. Don't return these since they cannot fund
  4902  			// arbitrary txns.
  4903  			continue
  4904  		}
  4905  		utxos = append(utxos, &compositeUTXO{
  4906  			rpc:   utxo,
  4907  			input: nfo,
  4908  			confs: utxo.Confirmations,
  4909  		})
  4910  	}
  4911  	// Sort in ascending order by amount (smallest first).
  4912  	sort.Slice(utxos, func(i, j int) bool { return utxos[i].rpc.Amount < utxos[j].rpc.Amount })
  4913  	return utxos, nil
  4914  }
  4915  
  4916  // lockedAtoms is the total value of locked outputs, as locked with LockUnspent.
  4917  func (dcr *ExchangeWallet) lockedAtoms(acct string) (uint64, error) {
  4918  	lockedOutpoints, err := dcr.wallet.LockedOutputs(dcr.ctx, acct)
  4919  	if err != nil {
  4920  		return 0, err
  4921  	}
  4922  	var sum uint64
  4923  	for _, op := range lockedOutpoints {
  4924  		sum += toAtoms(op.Amount)
  4925  	}
  4926  	return sum, nil
  4927  }
  4928  
  4929  // convertCoin converts the asset.Coin to an output whose tree may be unknown.
  4930  // Use wallet.UnspentOutput to determine the output tree where necessary.
  4931  func (dcr *ExchangeWallet) convertCoin(coin asset.Coin) (*output, error) {
  4932  	op, _ := coin.(*output)
  4933  	if op != nil {
  4934  		return op, nil
  4935  	}
  4936  	txHash, vout, err := decodeCoinID(coin.ID())
  4937  	if err != nil {
  4938  		return nil, err
  4939  	}
  4940  	return newOutput(txHash, vout, coin.Value(), wire.TxTreeUnknown), nil
  4941  }
  4942  
  4943  // withdraw sends the amount to the address. Fees are subtracted from the
  4944  // sent value.
  4945  func (dcr *ExchangeWallet) withdraw(addr stdaddr.Address, val, feeRate uint64) (*wire.MsgTx, uint64, error) {
  4946  	if val == 0 {
  4947  		return nil, 0, fmt.Errorf("cannot withdraw value = 0")
  4948  	}
  4949  	baseSize := uint32(dexdcr.MsgTxOverhead + dexdcr.P2PKHOutputSize*2)
  4950  	reportChange := dcr.wallet.Accounts().UnmixedAccount == "" // otherwise change goes to unmixed account
  4951  	enough := sendEnough(val, feeRate, true, baseSize, reportChange)
  4952  	reserves := dcr.bondReserves.Load()
  4953  	coins, _, _, _, err := dcr.fund(reserves, enough)
  4954  	if err != nil {
  4955  		return nil, 0, fmt.Errorf("unable to withdraw %s DCR to address %s with feeRate %d atoms/byte: %w",
  4956  			amount(val), addr, feeRate, err)
  4957  	}
  4958  
  4959  	msgTx, sentVal, err := dcr.sendCoins(coins, addr, nil, val, 0, feeRate, true)
  4960  	if err != nil {
  4961  		if _, retErr := dcr.returnCoins(coins); retErr != nil {
  4962  			dcr.log.Errorf("Failed to unlock coins: %v", retErr)
  4963  		}
  4964  		return nil, 0, err
  4965  	}
  4966  	return msgTx, sentVal, nil
  4967  }
  4968  
  4969  // sendToAddress sends an exact amount to an address. Transaction fees will be
  4970  // in addition to the sent amount, and the output will be the zeroth output.
  4971  // TODO: Just use the sendtoaddress rpc since dcrwallet respects locked utxos!
  4972  func (dcr *ExchangeWallet) sendToAddress(addr stdaddr.Address, amt, feeRate uint64) (*wire.MsgTx, uint64, uint64, error) {
  4973  	baseSize := uint32(dexdcr.MsgTxOverhead + dexdcr.P2PKHOutputSize*2) // may be extra if change gets omitted (see signTxAndAddChange)
  4974  	reportChange := dcr.wallet.Accounts().UnmixedAccount == ""          // otherwise change goes to unmixed account
  4975  	enough := sendEnough(amt, feeRate, false, baseSize, reportChange)
  4976  	reserves := dcr.bondReserves.Load()
  4977  	coins, _, _, _, err := dcr.fund(reserves, enough)
  4978  	if err != nil {
  4979  		return nil, 0, 0, fmt.Errorf("Unable to send %s DCR with fee rate of %d atoms/byte: %w",
  4980  			amount(amt), feeRate, err)
  4981  	}
  4982  
  4983  	msgTx, sentVal, err := dcr.sendCoins(coins, addr, nil, amt, 0, feeRate, false)
  4984  	if err != nil {
  4985  		if _, retErr := dcr.returnCoins(coins); retErr != nil {
  4986  			dcr.log.Errorf("Failed to unlock coins: %v", retErr)
  4987  		}
  4988  		return nil, 0, 0, err
  4989  	}
  4990  
  4991  	var totalOut uint64
  4992  	for _, txOut := range msgTx.TxOut {
  4993  		totalOut += uint64(txOut.Value)
  4994  	}
  4995  	var totalIn uint64
  4996  	for _, coin := range coins {
  4997  		totalIn += coin.Value()
  4998  	}
  4999  
  5000  	return msgTx, sentVal, totalIn - totalOut, nil
  5001  }
  5002  
  5003  // sendCoins sends the amount to the address as the zeroth output, spending the
  5004  // specified coins. If subtract is true, the transaction fees will be taken from
  5005  // the sent value, otherwise it will taken from the change output. If there is
  5006  // change, it will be at index 1.
  5007  //
  5008  // An optional second output may be generated with the second address and amount
  5009  // arguments, if addr2 is non-nil. Note that to omit the extra output, the
  5010  // *interface* must be nil, not just the concrete type, so be cautious with
  5011  // concrete address types because a nil pointer wrap into a non-nil std.Address!
  5012  func (dcr *ExchangeWallet) sendCoins(coins asset.Coins, addr, addr2 stdaddr.Address, val, val2, feeRate uint64,
  5013  	subtract bool) (*wire.MsgTx, uint64, error) {
  5014  	baseTx := wire.NewMsgTx()
  5015  	_, err := dcr.addInputCoins(baseTx, coins)
  5016  	if err != nil {
  5017  		return nil, 0, err
  5018  	}
  5019  	payScriptVer, payScript := addr.PaymentScript()
  5020  	txOut := newTxOut(int64(val), payScriptVer, payScript)
  5021  	baseTx.AddTxOut(txOut)
  5022  	if addr2 != nil {
  5023  		payScriptVer, payScript := addr2.PaymentScript()
  5024  		txOut := newTxOut(int64(val2), payScriptVer, payScript)
  5025  		baseTx.AddTxOut(txOut)
  5026  	}
  5027  
  5028  	var feeSource int32 // subtract from vout 0
  5029  	if !subtract {
  5030  		feeSource = -1 // subtract from change
  5031  	}
  5032  
  5033  	tx, err := dcr.sendWithReturn(baseTx, feeRate, feeSource)
  5034  	if err != nil {
  5035  		return nil, 0, err
  5036  	}
  5037  	return tx, uint64(tx.TxOut[0].Value), err
  5038  }
  5039  
  5040  // sendAll sends the maximum sendable amount (total input amount minus fees) to
  5041  // the provided account as a single output, spending the specified coins.
  5042  func (dcr *ExchangeWallet) sendAll(coins asset.Coins, destAcct string) (*wire.MsgTx, uint64, error) {
  5043  	addr, err := dcr.wallet.InternalAddress(dcr.ctx, destAcct)
  5044  	if err != nil {
  5045  		return nil, 0, err
  5046  	}
  5047  
  5048  	baseTx := wire.NewMsgTx()
  5049  	totalIn, err := dcr.addInputCoins(baseTx, coins)
  5050  	if err != nil {
  5051  		return nil, 0, err
  5052  	}
  5053  	payScriptVer, payScript := addr.PaymentScript()
  5054  	txOut := newTxOut(int64(totalIn), payScriptVer, payScript)
  5055  	baseTx.AddTxOut(txOut)
  5056  
  5057  	feeRate := dcr.targetFeeRateWithFallback(2, 0)
  5058  	tx, err := dcr.sendWithReturn(baseTx, feeRate, 0) // subtract from vout 0
  5059  	return tx, uint64(txOut.Value), err
  5060  }
  5061  
  5062  // newTxOut returns a new transaction output with the given parameters.
  5063  func newTxOut(amount int64, pkScriptVer uint16, pkScript []byte) *wire.TxOut {
  5064  	return &wire.TxOut{
  5065  		Value:    amount,
  5066  		Version:  pkScriptVer,
  5067  		PkScript: pkScript,
  5068  	}
  5069  }
  5070  
  5071  // msgTxFromHex creates a wire.MsgTx by deserializing the hex transaction.
  5072  func msgTxFromHex(txHex string) (*wire.MsgTx, error) {
  5073  	msgTx := wire.NewMsgTx()
  5074  	if err := msgTx.Deserialize(hex.NewDecoder(strings.NewReader(txHex))); err != nil {
  5075  		return nil, err
  5076  	}
  5077  	return msgTx, nil
  5078  }
  5079  
  5080  // msgTxFromBytes creates a wire.MsgTx by deserializing the transaction bytes.
  5081  func msgTxFromBytes(txB []byte) (*wire.MsgTx, error) {
  5082  	msgTx := wire.NewMsgTx()
  5083  	if err := msgTx.Deserialize(bytes.NewReader(txB)); err != nil {
  5084  		return nil, err
  5085  	}
  5086  	return msgTx, nil
  5087  }
  5088  
  5089  func msgTxToHex(msgTx *wire.MsgTx) (string, error) {
  5090  	b, err := msgTx.Bytes()
  5091  	if err != nil {
  5092  		return "", err
  5093  	}
  5094  	return hex.EncodeToString(b), nil
  5095  }
  5096  
  5097  func (dcr *ExchangeWallet) makeExternalOut(acct string, val uint64) (*wire.TxOut, stdaddr.Address, error) {
  5098  	addr, err := dcr.wallet.ExternalAddress(dcr.ctx, acct)
  5099  	if err != nil {
  5100  		return nil, nil, fmt.Errorf("error creating change address: %w", err)
  5101  	}
  5102  	changeScriptVersion, changeScript := addr.PaymentScript()
  5103  	return newTxOut(int64(val), changeScriptVersion, changeScript), addr, nil
  5104  }
  5105  
  5106  func (dcr *ExchangeWallet) makeChangeOut(changeAcct string, val uint64) (*wire.TxOut, stdaddr.Address, error) {
  5107  	changeAddr, err := dcr.wallet.InternalAddress(dcr.ctx, changeAcct)
  5108  	if err != nil {
  5109  		return nil, nil, fmt.Errorf("error creating change address: %w", err)
  5110  	}
  5111  	changeScriptVersion, changeScript := changeAddr.PaymentScript()
  5112  	return newTxOut(int64(val), changeScriptVersion, changeScript), changeAddr, nil
  5113  }
  5114  
  5115  // sendWithReturn sends the unsigned transaction, adding a change output unless
  5116  // the amount is dust. subtractFrom indicates the output from which fees should
  5117  // be subtracted, where -1 indicates fees should come out of a change output.
  5118  func (dcr *ExchangeWallet) sendWithReturn(baseTx *wire.MsgTx, feeRate uint64, subtractFrom int32) (*wire.MsgTx, error) {
  5119  	signedTx, _, _, _, err := dcr.signTxAndAddChange(baseTx, feeRate, subtractFrom, dcr.depositAccount())
  5120  	if err != nil {
  5121  		return nil, err
  5122  	}
  5123  
  5124  	_, err = dcr.broadcastTx(signedTx)
  5125  	return signedTx, err
  5126  }
  5127  
  5128  // signTxAndAddChange signs the passed msgTx, adding a change output that pays
  5129  // an address from the specified changeAcct, unless the change amount is dust.
  5130  // subtractFrom indicates the output from which fees should be subtracted, where
  5131  // -1 indicates fees should come out of a change output. baseTx may be modified
  5132  // with an added change output or a reduced value of the subtractFrom output.
  5133  func (dcr *ExchangeWallet) signTxAndAddChange(baseTx *wire.MsgTx, feeRate uint64,
  5134  	subtractFrom int32, changeAcct string) (*wire.MsgTx, *output, string, uint64, error) {
  5135  	// Sign the transaction to get an initial size estimate and calculate
  5136  	// whether a change output would be dust.
  5137  	sigCycles := 1
  5138  	msgTx, err := dcr.wallet.SignRawTransaction(dcr.ctx, baseTx)
  5139  	if err != nil {
  5140  		return nil, nil, "", 0, err
  5141  	}
  5142  
  5143  	totalIn, totalOut, remaining, _, size := reduceMsgTx(msgTx)
  5144  	if totalIn < totalOut {
  5145  		return nil, nil, "", 0, fmt.Errorf("unbalanced transaction")
  5146  	}
  5147  
  5148  	minFee := feeRate * size
  5149  	if subtractFrom == -1 && minFee > remaining {
  5150  		return nil, nil, "", 0, fmt.Errorf("not enough funds to cover minimum fee rate of %v atoms/B: %s > %s remaining",
  5151  			feeRate, amount(minFee), amount(remaining))
  5152  	}
  5153  	if int(subtractFrom) >= len(baseTx.TxOut) {
  5154  		return nil, nil, "", 0, fmt.Errorf("invalid subtractFrom output %d for tx with %d outputs",
  5155  			subtractFrom, len(baseTx.TxOut))
  5156  	}
  5157  
  5158  	// Add a change output if there is enough remaining.
  5159  	var changeAdded bool
  5160  	var changeAddress stdaddr.Address
  5161  	var changeOutput *wire.TxOut
  5162  	minFeeWithChange := (size + dexdcr.P2PKHOutputSize) * feeRate
  5163  	if remaining > minFeeWithChange {
  5164  		changeValue := remaining - minFeeWithChange
  5165  		if subtractFrom >= 0 {
  5166  			// Subtract the additional fee needed for the added change output
  5167  			// from the specified existing output.
  5168  			changeValue = remaining
  5169  		}
  5170  		if !dexdcr.IsDustVal(dexdcr.P2PKHOutputSize, changeValue, feeRate) {
  5171  			if subtractFrom >= 0 { // only subtract after dust check
  5172  				baseTx.TxOut[subtractFrom].Value -= int64(minFeeWithChange)
  5173  				remaining += minFeeWithChange
  5174  			}
  5175  			changeOutput, changeAddress, err = dcr.makeChangeOut(changeAcct, changeValue)
  5176  			if err != nil {
  5177  				return nil, nil, "", 0, err
  5178  			}
  5179  			dcr.log.Debugf("Change output size = %d, addr = %s", changeOutput.SerializeSize(), changeAddress.String())
  5180  			changeAdded = true
  5181  			baseTx.AddTxOut(changeOutput) // unsigned txn
  5182  			remaining -= changeValue
  5183  		}
  5184  	}
  5185  
  5186  	lastFee := remaining
  5187  
  5188  	// If change added or subtracting from an existing output, iterate on fees.
  5189  	if changeAdded || subtractFrom >= 0 {
  5190  		subtractee := changeOutput
  5191  		if subtractFrom >= 0 {
  5192  			subtractee = baseTx.TxOut[subtractFrom]
  5193  		}
  5194  		// The amount available for fees is the sum of what is presently
  5195  		// allocated to fees (lastFee) and the value of the subtractee output,
  5196  		// which add to fees or absorb excess fees from lastFee.
  5197  		reservoir := lastFee + uint64(subtractee.Value)
  5198  
  5199  		// Find the best fee rate by closing in on it in a loop.
  5200  		tried := map[uint64]bool{}
  5201  		for {
  5202  			// Each cycle, sign the transaction and see if there is a need to
  5203  			// raise or lower the fees.
  5204  			sigCycles++
  5205  			msgTx, err = dcr.wallet.SignRawTransaction(dcr.ctx, baseTx)
  5206  			if err != nil {
  5207  				return nil, nil, "", 0, err
  5208  			}
  5209  			size = uint64(msgTx.SerializeSize())
  5210  			reqFee := feeRate * size
  5211  			if reqFee > reservoir {
  5212  				// IsDustVal check must be bugged.
  5213  				dcr.log.Errorf("reached the impossible place. in = %.8f, out = %.8f, reqFee = %.8f, lastFee = %.8f, raw tx = %x",
  5214  					toDCR(totalIn), toDCR(totalOut), toDCR(reqFee), toDCR(lastFee), dcr.wireBytes(msgTx))
  5215  				return nil, nil, "", 0, fmt.Errorf("change error")
  5216  			}
  5217  
  5218  			// If 1) lastFee == reqFee, nothing changed since the last cycle.
  5219  			// And there is likely no room for improvement. If 2) The reqFee
  5220  			// required for a transaction of this size is less than the
  5221  			// currently signed transaction fees, but we've already tried it,
  5222  			// then it must have a larger serialize size, so the current fee is
  5223  			// as good as it gets.
  5224  			if lastFee == reqFee || (lastFee > reqFee && tried[reqFee]) {
  5225  				break
  5226  			}
  5227  
  5228  			// The minimum fee for a transaction of this size is either higher or
  5229  			// lower than the fee in the currently signed transaction, and it hasn't
  5230  			// been tried yet, so try it now.
  5231  			tried[lastFee] = true
  5232  			subtractee.Value = int64(reservoir - reqFee) // next
  5233  			lastFee = reqFee
  5234  			if dexdcr.IsDust(subtractee, feeRate) {
  5235  				// Another condition that should be impossible, but check anyway in case
  5236  				// the maximum fee was underestimated causing the first check to be
  5237  				// missed.
  5238  				dcr.log.Errorf("reached the impossible place. in = %.8f, out = %.8f, reqFee = %.8f, lastFee = %.8f, raw tx = %x",
  5239  					toDCR(totalIn), toDCR(totalOut), toDCR(reqFee), toDCR(lastFee), dcr.wireBytes(msgTx))
  5240  				return nil, nil, "", 0, fmt.Errorf("dust error")
  5241  			}
  5242  			continue
  5243  		}
  5244  	}
  5245  
  5246  	// Double check the resulting txns fee and fee rate.
  5247  	_, _, checkFee, checkRate, size := reduceMsgTx(msgTx)
  5248  	if checkFee != lastFee {
  5249  		return nil, nil, "", 0, fmt.Errorf("fee mismatch! %.8f != %.8f, raw tx: %x", toDCR(checkFee), toDCR(lastFee), dcr.wireBytes(msgTx))
  5250  	}
  5251  	// Ensure the effective fee rate is at least the required fee rate.
  5252  	if checkRate < feeRate {
  5253  		return nil, nil, "", 0, fmt.Errorf("final fee rate for %s, %d, is lower than expected, %d. raw tx: %x",
  5254  			msgTx.CachedTxHash(), checkRate, feeRate, dcr.wireBytes(msgTx))
  5255  	}
  5256  	// This is a last ditch effort to catch ridiculously high fees. Right now,
  5257  	// it's just erroring for fees more than triple the expected rate, which is
  5258  	// admittedly un-scientific. This should account for any signature length
  5259  	// related variation as well as a potential dust change output with no
  5260  	// subtractee specified, in which case the dust goes to the miner.
  5261  	if changeAdded && checkRate > feeRate*3 {
  5262  		return nil, nil, "", 0, fmt.Errorf("final fee rate for %s, %d, is seemingly outrageous, target = %d, raw tx = %x",
  5263  			msgTx.CachedTxHash(), checkRate, feeRate, dcr.wireBytes(msgTx))
  5264  	}
  5265  
  5266  	txHash := msgTx.TxHash()
  5267  	dcr.log.Debugf("%d signature cycles to converge on fees for tx %s: "+
  5268  		"min rate = %d, actual fee rate = %d (%v for %v bytes), change = %v",
  5269  		sigCycles, txHash, feeRate, checkRate, checkFee, size, changeAdded)
  5270  
  5271  	var change *output
  5272  	var changeAddr string
  5273  	if changeAdded {
  5274  		change = newOutput(&txHash, uint32(len(msgTx.TxOut)-1), uint64(changeOutput.Value), wire.TxTreeRegular)
  5275  		changeAddr = changeAddress.String()
  5276  	}
  5277  
  5278  	return msgTx, change, changeAddr, lastFee, nil
  5279  }
  5280  
  5281  // ValidateAddress checks that the provided address is valid.
  5282  func (dcr *ExchangeWallet) ValidateAddress(address string) bool {
  5283  	_, err := stdaddr.DecodeAddress(address, dcr.chainParams)
  5284  	return err == nil
  5285  }
  5286  
  5287  // dummyP2PKHScript only has to be a valid 25-byte pay-to-pubkey-hash pkScript
  5288  // for EstimateSendTxFee when an empty or invalid address is provided.
  5289  var dummyP2PKHScript = []byte{0x76, 0xa9, 0x14, 0xe4, 0x28, 0x61, 0xa,
  5290  	0xfc, 0xd0, 0x4e, 0x21, 0x94, 0xf7, 0xe2, 0xcc, 0xf8,
  5291  	0x58, 0x7a, 0xc9, 0xe7, 0x2c, 0x79, 0x7b, 0x88, 0xac,
  5292  }
  5293  
  5294  // EstimateSendTxFee returns a tx fee estimate for sending or withdrawing the
  5295  // provided amount using the provided feeRate.
  5296  func (dcr *ExchangeWallet) EstimateSendTxFee(address string, sendAmount, feeRate uint64, subtract, _ bool) (fee uint64, isValidAddress bool, err error) {
  5297  	if sendAmount == 0 {
  5298  		return 0, false, fmt.Errorf("cannot check fee: send amount = 0")
  5299  	}
  5300  
  5301  	feeRate = dcr.feeRateWithFallback(feeRate)
  5302  
  5303  	var pkScript []byte
  5304  	var payScriptVer uint16
  5305  	if addr, err := stdaddr.DecodeAddress(address, dcr.chainParams); err == nil {
  5306  		payScriptVer, pkScript = addr.PaymentScript()
  5307  		isValidAddress = true
  5308  	} else {
  5309  		// use a dummy 25-byte p2pkh script
  5310  		pkScript = dummyP2PKHScript
  5311  	}
  5312  
  5313  	tx := wire.NewMsgTx()
  5314  
  5315  	tx.AddTxOut(newTxOut(int64(sendAmount), payScriptVer, pkScript)) // payScriptVer is default zero
  5316  
  5317  	utxos, err := dcr.spendableUTXOs()
  5318  	if err != nil {
  5319  		return 0, false, err
  5320  	}
  5321  
  5322  	minTxSize := uint32(tx.SerializeSize())
  5323  	reportChange := dcr.wallet.Accounts().UnmixedAccount == ""
  5324  	enough := sendEnough(sendAmount, feeRate, subtract, minTxSize, reportChange)
  5325  	sum, extra, inputsSize, _, _, _, err := tryFund(utxos, enough)
  5326  	if err != nil {
  5327  		return 0, false, err
  5328  	}
  5329  
  5330  	reserves := dcr.bondReserves.Load()
  5331  	avail := sumUTXOs(utxos)
  5332  	if avail-sum+extra /* avail-sendAmount-fees */ < reserves {
  5333  		return 0, false, errors.New("violates reserves")
  5334  	}
  5335  
  5336  	txSize := uint64(minTxSize + inputsSize)
  5337  	estFee := txSize * feeRate
  5338  	remaining := sum - sendAmount
  5339  
  5340  	// Check if there will be a change output if there is enough remaining.
  5341  	estFeeWithChange := (txSize + dexdcr.P2PKHOutputSize) * feeRate
  5342  	var changeValue uint64
  5343  	if remaining > estFeeWithChange {
  5344  		changeValue = remaining - estFeeWithChange
  5345  	}
  5346  
  5347  	if subtract {
  5348  		// fees are already included in sendAmount, anything else is change.
  5349  		changeValue = remaining
  5350  	}
  5351  
  5352  	var finalFee uint64
  5353  	if dexdcr.IsDustVal(dexdcr.P2PKHOutputSize, changeValue, feeRate) {
  5354  		// remaining cannot cover a non-dust change and the fee for the change.
  5355  		finalFee = estFee + remaining
  5356  	} else {
  5357  		// additional fee will be paid for non-dust change
  5358  		finalFee = estFeeWithChange
  5359  	}
  5360  	return finalFee, isValidAddress, nil
  5361  }
  5362  
  5363  // StandardSendFees returns the fees for a simple send tx with one input and two
  5364  // outputs.
  5365  func (dcr *ExchangeWallet) StandardSendFee(feeRate uint64) uint64 {
  5366  	var baseSize uint64 = dexdcr.MsgTxOverhead + dexdcr.P2PKHOutputSize*2 + dexdcr.P2PKHInputSize
  5367  	return feeRate * baseSize
  5368  }
  5369  
  5370  func (dcr *ExchangeWallet) isNative() bool {
  5371  	return dcr.walletType == walletTypeSPV
  5372  }
  5373  
  5374  // currentAgendas gets the most recent agendas from the chain params. The caller
  5375  // must populate the CurrentChoice field of the agendas.
  5376  func currentAgendas(chainParams *chaincfg.Params) (agendas []*asset.TBAgenda) {
  5377  	var bestID uint32
  5378  	for deploymentID := range chainParams.Deployments {
  5379  		if bestID == 0 || deploymentID > bestID {
  5380  			bestID = deploymentID
  5381  		}
  5382  	}
  5383  	for _, deployment := range chainParams.Deployments[bestID] {
  5384  		v := deployment.Vote
  5385  		agenda := &asset.TBAgenda{
  5386  			ID:          v.Id,
  5387  			Description: v.Description,
  5388  		}
  5389  		for _, choice := range v.Choices {
  5390  			agenda.Choices = append(agenda.Choices, &asset.TBChoice{
  5391  				ID:          choice.Id,
  5392  				Description: choice.Description,
  5393  			})
  5394  		}
  5395  		agendas = append(agendas, agenda)
  5396  	}
  5397  	return
  5398  }
  5399  
  5400  func (dcr *ExchangeWallet) StakeStatus() (*asset.TicketStakingStatus, error) {
  5401  	if !dcr.connected.Load() {
  5402  		return nil, errors.New("not connected, login first")
  5403  	}
  5404  	// Try to get tickets first, because this will error for older RPC + SPV
  5405  	// wallets.
  5406  	tickets, err := dcr.tickets(dcr.ctx)
  5407  	if err != nil {
  5408  		if errors.Is(err, oldSPVWalletErr) {
  5409  			return nil, nil
  5410  		}
  5411  		return nil, fmt.Errorf("error retrieving tickets: %w", err)
  5412  	}
  5413  	sinfo, err := dcr.wallet.StakeInfo(dcr.ctx)
  5414  	if err != nil {
  5415  		return nil, err
  5416  	}
  5417  
  5418  	isRPC := !dcr.isNative()
  5419  	var vspURL string
  5420  	if !isRPC {
  5421  		if v := dcr.vspV.Load(); v != nil {
  5422  			vspURL = v.(*vsp).URL
  5423  		}
  5424  	} else {
  5425  		rpcW, ok := dcr.wallet.(*rpcWallet)
  5426  		if !ok {
  5427  			return nil, errors.New("wallet not an *rpcWallet")
  5428  		}
  5429  		walletInfo, err := rpcW.walletInfo(dcr.ctx)
  5430  		if err != nil {
  5431  			return nil, fmt.Errorf("error retrieving wallet info: %w", err)
  5432  		}
  5433  		vspURL = walletInfo.VSP
  5434  	}
  5435  	voteChoices, tSpends, treasuryPolicy, err := dcr.wallet.VotingPreferences(dcr.ctx)
  5436  	if err != nil {
  5437  		return nil, fmt.Errorf("error retrieving stances: %w", err)
  5438  	}
  5439  	agendas := currentAgendas(dcr.chainParams)
  5440  	for _, agenda := range agendas {
  5441  		for _, c := range voteChoices {
  5442  			if c.AgendaID == agenda.ID {
  5443  				agenda.CurrentChoice = c.ChoiceID
  5444  				break
  5445  			}
  5446  		}
  5447  	}
  5448  
  5449  	return &asset.TicketStakingStatus{
  5450  		TicketPrice:   uint64(sinfo.Sdiff),
  5451  		VotingSubsidy: dcr.voteSubsidy(dcr.cachedBestBlock().height),
  5452  		VSP:           vspURL,
  5453  		IsRPC:         isRPC,
  5454  		Tickets:       tickets,
  5455  		Stances: asset.Stances{
  5456  			Agendas:        agendas,
  5457  			TreasurySpends: tSpends,
  5458  			TreasuryKeys:   treasuryPolicy,
  5459  		},
  5460  		Stats: dcr.ticketStatsFromStakeInfo(sinfo),
  5461  	}, nil
  5462  }
  5463  
  5464  func (dcr *ExchangeWallet) ticketStatsFromStakeInfo(sinfo *wallet.StakeInfoData) asset.TicketStats {
  5465  	return asset.TicketStats{
  5466  		TotalRewards: uint64(sinfo.TotalSubsidy),
  5467  		TicketCount:  sinfo.OwnMempoolTix + sinfo.Unspent + sinfo.Immature + sinfo.Voted + sinfo.Revoked,
  5468  		Votes:        sinfo.Voted,
  5469  		Revokes:      sinfo.Revoked,
  5470  		Mempool:      sinfo.OwnMempoolTix,
  5471  		Queued:       uint32(dcr.ticketBuyer.remaining.Load()),
  5472  	}
  5473  }
  5474  
  5475  func (dcr *ExchangeWallet) voteSubsidy(tipHeight int64) uint64 {
  5476  	// Chance of a given ticket voting in a block is
  5477  	// p = chainParams.TicketsPerBlock / (chainParams.TicketPoolSize * chainParams.TicketsPerBlock)
  5478  	//   = 1 / chainParams.TicketPoolSize
  5479  	// Expected number of blocks to vote is
  5480  	// 1 / p = chainParams.TicketPoolSize
  5481  	expectedBlocksToVote := int64(dcr.chainParams.TicketPoolSize)
  5482  	voteHeightExpectationValue := tipHeight + expectedBlocksToVote
  5483  	return uint64(dcr.subsidyCache.CalcStakeVoteSubsidyV3(voteHeightExpectationValue, blockchain.SSVDCP0012))
  5484  }
  5485  
  5486  // tickets gets tickets from the wallet and changes the status of "unspent"
  5487  // tickets that haven't reached expiration "live".
  5488  // DRAFT NOTE: From dcrwallet:
  5489  //
  5490  //	TicketStatusUnspent is a matured ticket that has not been spent.  It
  5491  //	is only used under SPV mode where it is unknown if a ticket is live,
  5492  //	was missed, or expired.
  5493  //
  5494  // But if the ticket has not reached a certain number of confirmations, we
  5495  // can say for sure it's not expired. With auto-revocations, "missed" or
  5496  // "expired" tickets are actually "revoked", I think.
  5497  // The only thing I can't figure out is how SPV wallets set the spender in the
  5498  // case of an auto-revocation. It might be happening here
  5499  // https://github.com/decred/dcrwallet/blob/a87fa843495ec57c1d3b478c2ceb3876c3749af5/wallet/chainntfns.go#L770-L775
  5500  // If we're seeing auto-revocations, we're fine to make the changes in this
  5501  // method.
  5502  func (dcr *ExchangeWallet) tickets(ctx context.Context) ([]*asset.Ticket, error) {
  5503  	tickets, err := dcr.wallet.Tickets(ctx)
  5504  	if err != nil {
  5505  		return nil, fmt.Errorf("error retrieving tickets: %w", err)
  5506  	}
  5507  	// Adjust status for SPV tickets that aren't expired.
  5508  	oldestTicketsBlock := dcr.cachedBestBlock().height - int64(dcr.chainParams.TicketExpiry) - int64(dcr.chainParams.TicketMaturity)
  5509  	for _, t := range tickets {
  5510  		if t.Status != asset.TicketStatusUnspent {
  5511  			continue
  5512  		}
  5513  		if t.Tx.BlockHeight == -1 || t.Tx.BlockHeight > oldestTicketsBlock {
  5514  			t.Status = asset.TicketStatusLive
  5515  		}
  5516  	}
  5517  	return tickets, nil
  5518  }
  5519  
  5520  func vspInfo(ctx context.Context, uri string) (*vspdjson.VspInfoResponse, error) {
  5521  	suffix := "/api/v3/vspinfo"
  5522  	path, err := neturl.JoinPath(uri, suffix)
  5523  	if err != nil {
  5524  		return nil, err
  5525  	}
  5526  	var info vspdjson.VspInfoResponse
  5527  	return &info, dexnet.Get(ctx, path, &info)
  5528  }
  5529  
  5530  // SetVSP sets the VSP provider. Ability to set can be checked with StakeStatus
  5531  // first. Only non-RPC (internal) wallets can be set. Part of the
  5532  // asset.TicketBuyer interface.
  5533  func (dcr *ExchangeWallet) SetVSP(url string) error {
  5534  	if !dcr.isNative() {
  5535  		return errors.New("cannot set vsp for external wallet")
  5536  	}
  5537  	info, err := vspInfo(dcr.ctx, url)
  5538  	if err != nil {
  5539  		return err
  5540  	}
  5541  	v := vsp{
  5542  		URL:           url,
  5543  		PubKey:        base64.StdEncoding.EncodeToString(info.PubKey),
  5544  		FeePercentage: info.FeePercentage,
  5545  	}
  5546  	b, err := json.Marshal(&v)
  5547  	if err != nil {
  5548  		return err
  5549  	}
  5550  	if err := os.WriteFile(dcr.vspFilepath, b, 0666); err != nil {
  5551  		return err
  5552  	}
  5553  	dcr.vspV.Store(&v)
  5554  	return nil
  5555  }
  5556  
  5557  // PurchaseTickets purchases n number of tickets. Part of the asset.TicketBuyer
  5558  // interface.
  5559  func (dcr *ExchangeWallet) PurchaseTickets(n int, feeSuggestion uint64) error {
  5560  	if n < 1 {
  5561  		return nil
  5562  	}
  5563  	if !dcr.connected.Load() {
  5564  		return errors.New("not connected, login first")
  5565  	}
  5566  	bal, err := dcr.Balance()
  5567  	if err != nil {
  5568  		return fmt.Errorf("error getting balance: %v", err)
  5569  	}
  5570  	isRPC := !dcr.isNative()
  5571  	if isRPC {
  5572  		rpcW, ok := dcr.wallet.(*rpcWallet)
  5573  		if !ok {
  5574  			return errors.New("wallet not an *rpcWallet")
  5575  		}
  5576  		walletInfo, err := rpcW.walletInfo(dcr.ctx)
  5577  		if err != nil {
  5578  			return fmt.Errorf("error retrieving wallet info: %w", err)
  5579  		}
  5580  		if walletInfo.SPV && walletInfo.VSP == "" {
  5581  			return errors.New("a vsp must best set to purchase tickets with an spv wallet")
  5582  		}
  5583  	}
  5584  	sinfo, err := dcr.wallet.StakeInfo(dcr.ctx)
  5585  	if err != nil {
  5586  		return fmt.Errorf("stakeinfo error: %v", err)
  5587  	}
  5588  	// I think we need to set this, otherwise we probably end up with default
  5589  	// of DefaultRelayFeePerKb = 1e4 => 10 atoms/byte.
  5590  	feePerKB := dcrutil.Amount(dcr.feeRateWithFallback(feeSuggestion) * 1000)
  5591  	if err := dcr.wallet.SetTxFee(dcr.ctx, feePerKB); err != nil {
  5592  		return fmt.Errorf("error setting wallet tx fee: %w", err)
  5593  	}
  5594  
  5595  	// Get a minimum size assuming a single-input split tx.
  5596  	fees := feePerKB * minVSPTicketPurchaseSize / 1000
  5597  	ticketPrice := sinfo.Sdiff + fees
  5598  	total := uint64(n) * uint64(ticketPrice)
  5599  	if bal.Available < total {
  5600  		return fmt.Errorf("available balance %s is lower than projected cost %s for %d tickets",
  5601  			dcrutil.Amount(bal.Available), dcrutil.Amount(total), n)
  5602  	}
  5603  	remain := dcr.ticketBuyer.remaining.Add(int32(n))
  5604  	dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Remaining: uint32(remain)})
  5605  	go dcr.runTicketBuyer()
  5606  
  5607  	return nil
  5608  }
  5609  
  5610  const ticketDataRoute = "ticketPurchaseUpdate"
  5611  
  5612  // TicketPurchaseUpdate is an update from the asynchronous ticket purchasing
  5613  // loop.
  5614  type TicketPurchaseUpdate struct {
  5615  	Err       string             `json:"err,omitempty"`
  5616  	Remaining uint32             `json:"remaining"`
  5617  	Tickets   []*asset.Ticket    `json:"tickets"`
  5618  	Stats     *asset.TicketStats `json:"stats,omitempty"`
  5619  }
  5620  
  5621  // runTicketBuyer attempts to buy requested tickets. Because of a dcrwallet bug,
  5622  // its possible that (Wallet).PurchaseTickets will purchase fewer tickets than
  5623  // requested, without error. To work around this bug, we add requested tickets
  5624  // to ExchangeWallet.ticketBuyer.remaining, and re-run runTicketBuyer every
  5625  // block.
  5626  func (dcr *ExchangeWallet) runTicketBuyer() {
  5627  	tb := &dcr.ticketBuyer
  5628  	if !tb.running.CompareAndSwap(false, true) {
  5629  		// already running
  5630  		return
  5631  	}
  5632  	defer tb.running.Store(false)
  5633  	var ok bool
  5634  	defer func() {
  5635  		if !ok {
  5636  			tb.remaining.Store(0)
  5637  		}
  5638  	}()
  5639  
  5640  	if tb.unconfirmedTickets == nil {
  5641  		tb.unconfirmedTickets = make(map[chainhash.Hash]struct{})
  5642  	}
  5643  
  5644  	remain := tb.remaining.Load()
  5645  	if remain < 1 {
  5646  		return
  5647  	}
  5648  
  5649  	for txHash := range tb.unconfirmedTickets {
  5650  		tx, err := dcr.wallet.GetTransaction(dcr.ctx, &txHash)
  5651  		if err != nil {
  5652  			dcr.log.Errorf("GetTransaction error ticket tx %s: %v", txHash, err)
  5653  			dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: err.Error()})
  5654  			return
  5655  		}
  5656  		if tx.Confirmations > 0 {
  5657  			delete(tb.unconfirmedTickets, txHash)
  5658  		}
  5659  	}
  5660  	if len(tb.unconfirmedTickets) > 0 {
  5661  		ok = true
  5662  		dcr.log.Tracef("Skipping ticket purchase attempt because there are still %d unconfirmed tickets", len(tb.unconfirmedTickets))
  5663  		return
  5664  	}
  5665  
  5666  	dcr.log.Tracef("Attempting to purchase %d tickets", remain)
  5667  
  5668  	bal, err := dcr.Balance()
  5669  	if err != nil {
  5670  		dcr.log.Errorf("GetBalance error: %v", err)
  5671  		dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: err.Error()})
  5672  		return
  5673  	}
  5674  	sinfo, err := dcr.wallet.StakeInfo(dcr.ctx)
  5675  	if err != nil {
  5676  		dcr.log.Errorf("StakeInfo error: %v", err)
  5677  		dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: err.Error()})
  5678  		return
  5679  	}
  5680  	if dcrutil.Amount(bal.Available) < sinfo.Sdiff*dcrutil.Amount(remain) {
  5681  		dcr.log.Errorf("Insufficient balance %s to purchase %d ticket at price %s: %v", dcrutil.Amount(bal.Available), remain, sinfo.Sdiff, err)
  5682  		dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: "insufficient balance"})
  5683  		return
  5684  	}
  5685  
  5686  	var tickets []*asset.Ticket
  5687  	if !dcr.isNative() {
  5688  		tickets, err = dcr.wallet.PurchaseTickets(dcr.ctx, int(remain), "", "", false)
  5689  	} else {
  5690  		v := dcr.vspV.Load()
  5691  		if v == nil {
  5692  			err = errors.New("no vsp set")
  5693  		} else {
  5694  			vInfo := v.(*vsp)
  5695  			tickets, err = dcr.wallet.PurchaseTickets(dcr.ctx, int(remain), vInfo.URL, vInfo.PubKey, dcr.mixing.Load())
  5696  		}
  5697  	}
  5698  	if err != nil {
  5699  		dcr.log.Errorf("PurchaseTickets error: %v", err)
  5700  		dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: err.Error()})
  5701  		return
  5702  	}
  5703  	purchased := int32(len(tickets))
  5704  	remain = tb.remaining.Add(-purchased)
  5705  	// sanity check
  5706  	if remain < 0 {
  5707  		remain = 0
  5708  		tb.remaining.Store(remain)
  5709  	}
  5710  	stats := dcr.ticketStatsFromStakeInfo(sinfo)
  5711  	stats.Mempool += uint32(len(tickets))
  5712  	stats.Queued = uint32(remain)
  5713  	dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{
  5714  		Tickets:   tickets,
  5715  		Remaining: uint32(remain),
  5716  		Stats:     &stats,
  5717  	})
  5718  	for _, ticket := range tickets {
  5719  		txHash, err := chainhash.NewHashFromStr(ticket.Tx.Hash)
  5720  		if err != nil {
  5721  			dcr.log.Errorf("NewHashFromStr error for ticket hash %s: %v", ticket.Tx.Hash, err)
  5722  			dcr.emit.Data(ticketDataRoute, &TicketPurchaseUpdate{Err: err.Error()})
  5723  			return
  5724  		}
  5725  		tb.unconfirmedTickets[*txHash] = struct{}{}
  5726  		dcr.addTxToHistory(&asset.WalletTransaction{
  5727  			Type:   asset.TicketPurchase,
  5728  			ID:     txHash.String(),
  5729  			Amount: ticket.Tx.TicketPrice,
  5730  			Fees:   ticket.Tx.Fees,
  5731  		}, txHash, true)
  5732  	}
  5733  	ok = true
  5734  }
  5735  
  5736  // SetVotingPreferences sets the vote choices for all active tickets and future
  5737  // tickets. Nil maps can be provided for no change. Part of the
  5738  // asset.TicketBuyer interface.
  5739  func (dcr *ExchangeWallet) SetVotingPreferences(choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string) error {
  5740  	if !dcr.connected.Load() {
  5741  		return errors.New("not connected, login first")
  5742  	}
  5743  	return dcr.wallet.SetVotingPreferences(dcr.ctx, choices, tspendPolicy, treasuryPolicy)
  5744  }
  5745  
  5746  // ListVSPs lists known available voting service providers.
  5747  func (dcr *ExchangeWallet) ListVSPs() ([]*asset.VotingServiceProvider, error) {
  5748  	if dcr.network == dex.Simnet {
  5749  		const simnetVSPUrl = "http://127.0.0.1:19591"
  5750  		vspi, err := vspInfo(dcr.ctx, simnetVSPUrl)
  5751  		if err != nil {
  5752  			dcr.log.Warnf("Error getting simnet VSP info: %v", err)
  5753  			return []*asset.VotingServiceProvider{}, nil
  5754  		}
  5755  		return []*asset.VotingServiceProvider{{
  5756  			URL:           simnetVSPUrl,
  5757  			Network:       dex.Simnet,
  5758  			Launched:      uint64(time.Now().Add(-time.Hour * 24 * 180).UnixMilli()),
  5759  			LastUpdated:   uint64(time.Now().Add(-time.Minute * 15).UnixMilli()),
  5760  			APIVersions:   vspi.APIVersions,
  5761  			FeePercentage: vspi.FeePercentage,
  5762  			Closed:        vspi.VspClosed,
  5763  			Voting:        vspi.Voting,
  5764  			Voted:         vspi.Voted,
  5765  			Revoked:       vspi.Revoked,
  5766  			VSPDVersion:   vspi.VspdVersion,
  5767  			BlockHeight:   vspi.BlockHeight,
  5768  			NetShare:      vspi.NetworkProportion,
  5769  		}}, nil
  5770  	}
  5771  
  5772  	// This struct is not quite compatible with vspdjson.VspInfoResponse.
  5773  	var res map[string]*struct {
  5774  		Network       string  `json:"network"`
  5775  		Launched      uint64  `json:"launched"`    // seconds
  5776  		LastUpdated   uint64  `json:"lastupdated"` // seconds
  5777  		APIVersions   []int64 `json:"apiversions"`
  5778  		FeePercentage float64 `json:"feepercentage"`
  5779  		Closed        bool    `json:"closed"`
  5780  		Voting        int64   `json:"voting"`
  5781  		Voted         int64   `json:"voted"`
  5782  		Revoked       int64   `json:"revoked"`
  5783  		VSPDVersion   string  `json:"vspdversion"`
  5784  		BlockHeight   uint32  `json:"blockheight"`
  5785  		NetShare      float32 `json:"estimatednetworkproportion"`
  5786  	}
  5787  	if err := dexnet.Get(dcr.ctx, "https://api.decred.org/?c=vsp", &res); err != nil {
  5788  		return nil, err
  5789  	}
  5790  	vspds := make([]*asset.VotingServiceProvider, 0)
  5791  	for host, v := range res {
  5792  		net, err := dex.NetFromString(v.Network)
  5793  		if err != nil {
  5794  			dcr.log.Warnf("error parsing VSP network from %q", v.Network)
  5795  		}
  5796  		if net != dcr.network {
  5797  			continue
  5798  		}
  5799  		vspds = append(vspds, &asset.VotingServiceProvider{
  5800  			URL:           "https://" + host,
  5801  			Network:       net,
  5802  			Launched:      v.Launched * 1000,    // to milliseconds
  5803  			LastUpdated:   v.LastUpdated * 1000, // to milliseconds
  5804  			APIVersions:   v.APIVersions,
  5805  			FeePercentage: v.FeePercentage,
  5806  			Closed:        v.Closed,
  5807  			Voting:        v.Voting,
  5808  			Voted:         v.Voted,
  5809  			Revoked:       v.Revoked,
  5810  			VSPDVersion:   v.VSPDVersion,
  5811  			BlockHeight:   v.BlockHeight,
  5812  			NetShare:      v.NetShare,
  5813  		})
  5814  	}
  5815  	return vspds, nil
  5816  }
  5817  
  5818  // TicketPage fetches a page of tickets within a range of block numbers with a
  5819  // target page size and optional offset. scanStart is the block in which to
  5820  // start the scan. The scan progresses in reverse block number order, starting
  5821  // at scanStart and going to progressively lower blocks. scanStart can be set to
  5822  // -1 to indicate the current chain tip.
  5823  func (dcr *ExchangeWallet) TicketPage(scanStart int32, n, skipN int) ([]*asset.Ticket, error) {
  5824  	if !dcr.connected.Load() {
  5825  		return nil, errors.New("not connected, login first")
  5826  	}
  5827  	pager, is := dcr.wallet.(ticketPager)
  5828  	if !is {
  5829  		return nil, errors.New("ticket pagination not supported for this wallet")
  5830  	}
  5831  	return pager.TicketPage(dcr.ctx, scanStart, n, skipN)
  5832  }
  5833  
  5834  func (dcr *ExchangeWallet) broadcastTx(signedTx *wire.MsgTx) (*chainhash.Hash, error) {
  5835  	txHash, err := dcr.wallet.SendRawTransaction(dcr.ctx, signedTx, false)
  5836  	if err != nil {
  5837  		return nil, fmt.Errorf("sendrawtx error: %w, raw tx: %x", err, dcr.wireBytes(signedTx))
  5838  	}
  5839  	checkHash := signedTx.TxHash()
  5840  	if *txHash != checkHash {
  5841  		return nil, fmt.Errorf("transaction sent, but received unexpected transaction ID back from RPC server. "+
  5842  			"expected %s, got %s, raw tx: %x", *txHash, checkHash, dcr.wireBytes(signedTx))
  5843  	}
  5844  	return txHash, nil
  5845  }
  5846  
  5847  // createSig creates and returns the serialized raw signature and compressed
  5848  // pubkey for a transaction input signature.
  5849  func (dcr *ExchangeWallet) createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr stdaddr.Address) (sig, pubkey []byte, err error) {
  5850  	sigType, err := dexdcr.AddressSigType(addr)
  5851  	if err != nil {
  5852  		return nil, nil, err
  5853  	}
  5854  
  5855  	priv, err := dcr.wallet.AddressPrivKey(dcr.ctx, addr)
  5856  	if err != nil {
  5857  		return nil, nil, err
  5858  	}
  5859  	defer priv.Zero()
  5860  
  5861  	sig, err = sign.RawTxInSignature(tx, idx, pkScript, txscript.SigHashAll, priv.Serialize(), sigType)
  5862  	if err != nil {
  5863  		return nil, nil, err
  5864  	}
  5865  
  5866  	return sig, priv.PubKey().SerializeCompressed(), nil
  5867  }
  5868  
  5869  func (dcr *ExchangeWallet) txDB() *btc.BadgerTxDB {
  5870  	db := dcr.txHistoryDB.Load()
  5871  	if db == nil {
  5872  		return nil
  5873  	}
  5874  	return db.(*btc.BadgerTxDB)
  5875  }
  5876  
  5877  func rpcTxFee(tx *ListTransactionsResult) uint64 {
  5878  	if tx.Fee != nil {
  5879  		if *tx.Fee < 0 {
  5880  			return toAtoms(-*tx.Fee)
  5881  		}
  5882  		return toAtoms(*tx.Fee)
  5883  	}
  5884  	return 0
  5885  }
  5886  
  5887  func isRegularMix(tx *wire.MsgTx) (isMix bool, mixDenom int64) {
  5888  	if len(tx.TxOut) < 3 || len(tx.TxIn) < 3 {
  5889  		return false, 0
  5890  	}
  5891  
  5892  	mixedOuts := make(map[int64]uint32)
  5893  	for _, o := range tx.TxOut {
  5894  		val := o.Value
  5895  		if _, ok := splitPointMap[val]; ok {
  5896  			mixedOuts[val]++
  5897  			continue
  5898  		}
  5899  	}
  5900  
  5901  	var mixCount uint32
  5902  	for val, count := range mixedOuts {
  5903  		if count < 3 {
  5904  			continue
  5905  		}
  5906  		if val > mixDenom {
  5907  			mixDenom = val
  5908  			mixCount = count
  5909  		}
  5910  	}
  5911  
  5912  	// TODO: revisit the input count requirements
  5913  	isMix = mixCount >= uint32(len(tx.TxOut)/2)
  5914  	return
  5915  }
  5916  
  5917  // isMixedSplitTx tests if a transaction is a CSPP-mixed ticket split
  5918  // transaction (the transaction that creates appropriately-sized outputs to be
  5919  // spent by a ticket purchase). This dumbly checks for at least three outputs
  5920  // of the same size and three of other sizes. It could be smarter by checking
  5921  // for ticket price + ticket fee outputs, but it's impossible to know the fee
  5922  // after the fact although it`s probably the default fee.
  5923  func isMixedSplitTx(tx *wire.MsgTx) (isMix bool, tikPrice int64) {
  5924  	if len(tx.TxOut) < 6 || len(tx.TxIn) < 3 {
  5925  		return false, 0
  5926  	}
  5927  	values := make(map[int64]int)
  5928  	for _, o := range tx.TxOut {
  5929  		values[o.Value]++
  5930  	}
  5931  
  5932  	var numPossibleTickets int
  5933  	for k, v := range values {
  5934  		if v > numPossibleTickets {
  5935  			numPossibleTickets = v
  5936  			tikPrice = k
  5937  		}
  5938  	}
  5939  	numOtherOut := len(tx.TxOut) - numPossibleTickets
  5940  
  5941  	// NOTE: The numOtherOut requirement may be too strict,
  5942  	if numPossibleTickets < 3 || numOtherOut < 3 {
  5943  		return false, 0
  5944  	}
  5945  
  5946  	return true, tikPrice
  5947  }
  5948  
  5949  func isMixTx(tx *wire.MsgTx) (isMix bool, mixDenom int64) {
  5950  	if isMix, mixDenom = isRegularMix(tx); isMix {
  5951  		return
  5952  	}
  5953  
  5954  	return isMixedSplitTx(tx)
  5955  }
  5956  
  5957  // idUnknownTx identifies the type and details of a transaction either made
  5958  // or received by the wallet.
  5959  func (dcr *ExchangeWallet) idUnknownTx(ctx context.Context, ltxr *ListTransactionsResult) (*asset.WalletTransaction, error) {
  5960  	txHash, err := chainhash.NewHashFromStr(ltxr.TxID)
  5961  	if err != nil {
  5962  		return nil, fmt.Errorf("error decoding tx hash %s: %v", ltxr.TxID, err)
  5963  	}
  5964  	tx, err := dcr.wallet.GetTransaction(ctx, txHash)
  5965  	if err != nil {
  5966  		return nil, fmt.Errorf("GetTransaction error: %v", err)
  5967  	}
  5968  
  5969  	var totalIn uint64
  5970  	for _, txIn := range tx.MsgTx.TxIn {
  5971  		if txIn.ValueIn > 0 {
  5972  			totalIn += uint64(txIn.ValueIn)
  5973  		}
  5974  	}
  5975  
  5976  	var totalOut uint64
  5977  	for _, txOut := range tx.MsgTx.TxOut {
  5978  		totalOut += uint64(txOut.Value)
  5979  	}
  5980  
  5981  	fee := rpcTxFee(ltxr)
  5982  	if fee == 0 && totalIn > totalOut {
  5983  		fee = totalIn - totalOut
  5984  	}
  5985  
  5986  	switch *ltxr.TxType {
  5987  	case walletjson.LTTTVote:
  5988  		return &asset.WalletTransaction{
  5989  			Type:   asset.TicketVote,
  5990  			ID:     ltxr.TxID,
  5991  			Amount: totalOut,
  5992  			Fees:   fee,
  5993  		}, nil
  5994  	case walletjson.LTTTRevocation:
  5995  		return &asset.WalletTransaction{
  5996  			Type:   asset.TicketRevocation,
  5997  			ID:     ltxr.TxID,
  5998  			Amount: totalOut,
  5999  			Fees:   fee,
  6000  		}, nil
  6001  	case walletjson.LTTTTicket:
  6002  		return &asset.WalletTransaction{
  6003  			Type:   asset.TicketPurchase,
  6004  			ID:     ltxr.TxID,
  6005  			Amount: totalOut,
  6006  			Fees:   fee,
  6007  		}, nil
  6008  	}
  6009  
  6010  	txIsBond := func(msgTx *wire.MsgTx) (bool, *asset.BondTxInfo) {
  6011  		if len(msgTx.TxOut) < 2 {
  6012  			return false, nil
  6013  		}
  6014  		const scriptVer = 0
  6015  		acctID, lockTime, pkHash, err := dexdcr.ExtractBondCommitDataV0(scriptVer, msgTx.TxOut[1].PkScript)
  6016  		if err != nil {
  6017  			return false, nil
  6018  		}
  6019  		return true, &asset.BondTxInfo{
  6020  			AccountID: acctID[:],
  6021  			LockTime:  uint64(lockTime),
  6022  			BondID:    pkHash[:],
  6023  		}
  6024  	}
  6025  	if isBond, bondInfo := txIsBond(tx.MsgTx); isBond {
  6026  		return &asset.WalletTransaction{
  6027  			Type:     asset.CreateBond,
  6028  			ID:       ltxr.TxID,
  6029  			Amount:   uint64(tx.MsgTx.TxOut[0].Value),
  6030  			Fees:     fee,
  6031  			BondInfo: bondInfo,
  6032  		}, nil
  6033  	}
  6034  
  6035  	// Any other P2SH may be a swap or a send. We cannot determine unless we
  6036  	// look up the transaction that spends this UTXO.
  6037  	txPaysToScriptHash := func(msgTx *wire.MsgTx) (v uint64) {
  6038  		for _, txOut := range msgTx.TxOut {
  6039  			if txscript.IsPayToScriptHash(txOut.PkScript) {
  6040  				v += uint64(txOut.Value)
  6041  			}
  6042  		}
  6043  		return
  6044  	}
  6045  	if v := txPaysToScriptHash(tx.MsgTx); v > 0 {
  6046  		return &asset.WalletTransaction{
  6047  			Type:   asset.SwapOrSend,
  6048  			ID:     ltxr.TxID,
  6049  			Amount: v,
  6050  			Fees:   fee,
  6051  		}, nil
  6052  	}
  6053  
  6054  	// Helper function will help us identify inputs that spend P2SH contracts.
  6055  	containsContractAtPushIndex := func(msgTx *wire.MsgTx, idx int, isContract func(contract []byte) bool) bool {
  6056  	txinloop:
  6057  		for _, txIn := range msgTx.TxIn {
  6058  			// not segwit
  6059  			const scriptVer = 0
  6060  			tokenizer := txscript.MakeScriptTokenizer(scriptVer, txIn.SignatureScript)
  6061  			for i := 0; i <= idx; i++ { // contract is 5th item item in redemption and 4th in refund
  6062  				if !tokenizer.Next() {
  6063  					continue txinloop
  6064  				}
  6065  			}
  6066  			if isContract(tokenizer.Data()) {
  6067  				return true
  6068  			}
  6069  		}
  6070  		return false
  6071  	}
  6072  
  6073  	// Swap redemptions and refunds
  6074  	contractIsSwap := func(contract []byte) bool {
  6075  		_, _, _, _, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams)
  6076  		return err == nil
  6077  	}
  6078  	redeemsSwap := func(msgTx *wire.MsgTx) bool {
  6079  		return containsContractAtPushIndex(msgTx, 4, contractIsSwap)
  6080  	}
  6081  	if redeemsSwap(tx.MsgTx) {
  6082  		return &asset.WalletTransaction{
  6083  			Type:   asset.Redeem,
  6084  			ID:     ltxr.TxID,
  6085  			Amount: totalOut + fee,
  6086  			Fees:   fee,
  6087  		}, nil
  6088  	}
  6089  	refundsSwap := func(msgTx *wire.MsgTx) bool {
  6090  		return containsContractAtPushIndex(msgTx, 3, contractIsSwap)
  6091  	}
  6092  	if refundsSwap(tx.MsgTx) {
  6093  		return &asset.WalletTransaction{
  6094  			Type:   asset.Refund,
  6095  			ID:     ltxr.TxID,
  6096  			Amount: totalOut + fee,
  6097  			Fees:   fee,
  6098  		}, nil
  6099  	}
  6100  
  6101  	// Bond refunds
  6102  	redeemsBond := func(msgTx *wire.MsgTx) (bool, *asset.BondTxInfo) {
  6103  		var bondInfo *asset.BondTxInfo
  6104  		isBond := func(contract []byte) bool {
  6105  			const scriptVer = 0
  6106  			lockTime, pkHash, err := dexdcr.ExtractBondDetailsV0(scriptVer, contract)
  6107  			if err != nil {
  6108  				return false
  6109  			}
  6110  			bondInfo = &asset.BondTxInfo{
  6111  				AccountID: []byte{}, // Could look for the bond tx to get this, I guess.
  6112  				LockTime:  uint64(lockTime),
  6113  				BondID:    pkHash[:],
  6114  			}
  6115  			return true
  6116  		}
  6117  		return containsContractAtPushIndex(msgTx, 2, isBond), bondInfo
  6118  	}
  6119  	if isBondRedemption, bondInfo := redeemsBond(tx.MsgTx); isBondRedemption {
  6120  		return &asset.WalletTransaction{
  6121  			Type:     asset.RedeemBond,
  6122  			ID:       ltxr.TxID,
  6123  			Amount:   totalOut,
  6124  			Fees:     fee,
  6125  			BondInfo: bondInfo,
  6126  		}, nil
  6127  	}
  6128  
  6129  	const scriptVersion = 0
  6130  
  6131  	allOutputsPayUs := func(msgTx *wire.MsgTx) bool {
  6132  		for _, txOut := range msgTx.TxOut {
  6133  			_, addrs := stdscript.ExtractAddrs(scriptVersion, txOut.PkScript, dcr.chainParams)
  6134  			if len(addrs) != 1 { // sanity check
  6135  				return false
  6136  			}
  6137  
  6138  			addr := addrs[0]
  6139  			owns, err := dcr.wallet.WalletOwnsAddress(ctx, addr)
  6140  			if err != nil {
  6141  				dcr.log.Errorf("walletOwnsAddress error: %w", err)
  6142  				return false
  6143  			}
  6144  			if !owns {
  6145  				return false
  6146  			}
  6147  		}
  6148  
  6149  		return true
  6150  	}
  6151  
  6152  	if ltxr.Send && allOutputsPayUs(tx.MsgTx) && len(tx.MsgTx.TxIn) == 1 {
  6153  		return &asset.WalletTransaction{
  6154  			Type: asset.Split,
  6155  			ID:   ltxr.TxID,
  6156  			Fees: fee,
  6157  		}, nil
  6158  	}
  6159  
  6160  	if isMix, mixDenom := isMixTx(tx.MsgTx); isMix {
  6161  		var mixedAmount uint64
  6162  		for _, txOut := range tx.MsgTx.TxOut {
  6163  			if txOut.Value == mixDenom {
  6164  				_, addrs := stdscript.ExtractAddrs(scriptVersion, txOut.PkScript, dcr.chainParams)
  6165  				if err != nil {
  6166  					dcr.log.Errorf("ExtractAddrs error: %w", err)
  6167  					continue
  6168  				}
  6169  				if len(addrs) != 1 { // sanity check
  6170  					continue
  6171  				}
  6172  
  6173  				addr := addrs[0]
  6174  				owns, err := dcr.wallet.WalletOwnsAddress(ctx, addr)
  6175  				if err != nil {
  6176  					dcr.log.Errorf("walletOwnsAddress error: %w", err)
  6177  					continue
  6178  				}
  6179  
  6180  				if owns {
  6181  					mixedAmount += uint64(txOut.Value)
  6182  				}
  6183  			}
  6184  		}
  6185  
  6186  		return &asset.WalletTransaction{
  6187  			Type:   asset.Mix,
  6188  			ID:     ltxr.TxID,
  6189  			Amount: mixedAmount,
  6190  			Fees:   fee,
  6191  		}, nil
  6192  	}
  6193  
  6194  	getRecipient := func(msgTx *wire.MsgTx, receive bool) *string {
  6195  		for _, txOut := range msgTx.TxOut {
  6196  			_, addrs := stdscript.ExtractAddrs(scriptVersion, txOut.PkScript, dcr.chainParams)
  6197  			if err != nil {
  6198  				dcr.log.Errorf("ExtractAddrs error: %w", err)
  6199  				continue
  6200  			}
  6201  			if len(addrs) != 1 { // sanity check
  6202  				continue
  6203  			}
  6204  
  6205  			addr := addrs[0]
  6206  			owns, err := dcr.wallet.WalletOwnsAddress(ctx, addr)
  6207  			if err != nil {
  6208  				dcr.log.Errorf("walletOwnsAddress error: %w", err)
  6209  				continue
  6210  			}
  6211  
  6212  			if receive == owns {
  6213  				str := addr.String()
  6214  				return &str
  6215  			}
  6216  		}
  6217  		return nil
  6218  	}
  6219  
  6220  	txOutDirection := func(msgTx *wire.MsgTx) (in, out uint64) {
  6221  		for _, txOut := range msgTx.TxOut {
  6222  			_, addrs := stdscript.ExtractAddrs(scriptVersion, txOut.PkScript, dcr.chainParams)
  6223  			if err != nil {
  6224  				dcr.log.Errorf("ExtractAddrs error: %w", err)
  6225  				continue
  6226  			}
  6227  			if len(addrs) != 1 { // sanity check
  6228  				continue
  6229  			}
  6230  
  6231  			addr := addrs[0]
  6232  			owns, err := dcr.wallet.WalletOwnsAddress(ctx, addr)
  6233  			if err != nil {
  6234  				dcr.log.Errorf("walletOwnsAddress error: %w", err)
  6235  				continue
  6236  			}
  6237  			if owns {
  6238  				in += uint64(txOut.Value)
  6239  			} else {
  6240  				out += uint64(txOut.Value)
  6241  			}
  6242  		}
  6243  		return
  6244  	}
  6245  
  6246  	in, out := txOutDirection(tx.MsgTx)
  6247  
  6248  	if ltxr.Send {
  6249  		txType := asset.Send
  6250  		amt := out
  6251  		if allOutputsPayUs(tx.MsgTx) {
  6252  			txType = asset.SelfSend
  6253  			amt = in
  6254  		}
  6255  		return &asset.WalletTransaction{
  6256  			Type:      txType,
  6257  			ID:        ltxr.TxID,
  6258  			Amount:    amt,
  6259  			Fees:      fee,
  6260  			Recipient: getRecipient(tx.MsgTx, false),
  6261  		}, nil
  6262  	}
  6263  
  6264  	return &asset.WalletTransaction{
  6265  		Type:      asset.Receive,
  6266  		ID:        ltxr.TxID,
  6267  		Amount:    in,
  6268  		Fees:      fee,
  6269  		Recipient: getRecipient(tx.MsgTx, true),
  6270  	}, nil
  6271  }
  6272  
  6273  // addUnknownTransactionsToHistory checks for any transactions the wallet has
  6274  // made or received that are not part of the transaction history. It scans
  6275  // from the last point to which it had previously scanned to the current tip.
  6276  func (dcr *ExchangeWallet) addUnknownTransactionsToHistory(tip uint64) {
  6277  	txHistoryDB := dcr.txDB()
  6278  
  6279  	const blockQueryBuffer = 3
  6280  	var blockToQuery uint64
  6281  	lastQuery := dcr.receiveTxLastQuery.Load()
  6282  	if lastQuery == 0 {
  6283  		// TODO: use wallet birthday instead of block 0.
  6284  		// blockToQuery = 0
  6285  	} else if lastQuery < tip-blockQueryBuffer {
  6286  		blockToQuery = lastQuery - blockQueryBuffer
  6287  	} else {
  6288  		blockToQuery = tip - blockQueryBuffer
  6289  	}
  6290  
  6291  	txs, err := dcr.wallet.ListSinceBlock(dcr.ctx, int32(blockToQuery))
  6292  	if err != nil {
  6293  		dcr.log.Errorf("Error listing transactions since block %d: %v", blockToQuery, err)
  6294  		return
  6295  	}
  6296  
  6297  	for _, tx := range txs {
  6298  		if dcr.ctx.Err() != nil {
  6299  			return
  6300  		}
  6301  		txHash, err := chainhash.NewHashFromStr(tx.TxID)
  6302  		if err != nil {
  6303  			dcr.log.Errorf("Error decoding tx hash %s: %v", tx.TxID, err)
  6304  			continue
  6305  		}
  6306  		_, err = txHistoryDB.GetTx(txHash.String())
  6307  		if err == nil {
  6308  			continue
  6309  		}
  6310  		if !errors.Is(err, asset.CoinNotFoundError) {
  6311  			dcr.log.Errorf("Error getting tx %s: %v", txHash.String(), err)
  6312  			continue
  6313  		}
  6314  		wt, err := dcr.idUnknownTx(dcr.ctx, &tx)
  6315  		if err != nil {
  6316  			dcr.log.Errorf("error identifying transaction: %v", err)
  6317  			continue
  6318  		}
  6319  
  6320  		if tx.BlockIndex != nil && *tx.BlockIndex > 0 && *tx.BlockIndex < int64(tip-blockQueryBuffer) {
  6321  			wt.BlockNumber = uint64(*tx.BlockIndex)
  6322  			wt.Timestamp = uint64(tx.BlockTime)
  6323  		}
  6324  
  6325  		// Don't send notifications for the initial sync to avoid spamming the
  6326  		// front end. A notification is sent at the end of the initial sync.
  6327  		dcr.addTxToHistory(wt, txHash, true, blockToQuery == 0)
  6328  	}
  6329  
  6330  	dcr.receiveTxLastQuery.Store(tip)
  6331  	err = txHistoryDB.SetLastReceiveTxQuery(tip)
  6332  	if err != nil {
  6333  		dcr.log.Errorf("Error setting last query to %d: %v", tip, err)
  6334  	}
  6335  
  6336  	if blockToQuery == 0 {
  6337  		dcr.emit.TransactionHistorySyncedNote()
  6338  	}
  6339  }
  6340  
  6341  // syncTxHistory checks to see if there are any transactions which the wallet
  6342  // has made or received that are not part of the transaction history, then
  6343  // identifies and adds them. It also checks all the pending transactions to see
  6344  // if they have been mined into a block, and if so, updates the transaction
  6345  // history to reflect the block height.
  6346  func (dcr *ExchangeWallet) syncTxHistory(ctx context.Context, tip uint64) {
  6347  	if !dcr.syncingTxHistory.CompareAndSwap(false, true) {
  6348  		return
  6349  	}
  6350  	defer dcr.syncingTxHistory.Store(false)
  6351  
  6352  	txHistoryDB := dcr.txDB()
  6353  	if txHistoryDB == nil {
  6354  		return
  6355  	}
  6356  
  6357  	ss, err := dcr.SyncStatus()
  6358  	if err != nil {
  6359  		dcr.log.Errorf("Error getting sync status: %v", err)
  6360  		return
  6361  	}
  6362  	if !ss.Synced {
  6363  		return
  6364  	}
  6365  
  6366  	dcr.addUnknownTransactionsToHistory(tip)
  6367  
  6368  	pendingTxsCopy := make(map[chainhash.Hash]btc.ExtendedWalletTx, len(dcr.pendingTxs))
  6369  	dcr.pendingTxsMtx.RLock()
  6370  	for hash, tx := range dcr.pendingTxs {
  6371  		pendingTxsCopy[hash] = *tx
  6372  	}
  6373  	dcr.pendingTxsMtx.RUnlock()
  6374  
  6375  	handlePendingTx := func(txHash chainhash.Hash, tx *btc.ExtendedWalletTx) {
  6376  		if !tx.Submitted {
  6377  			return
  6378  		}
  6379  
  6380  		gtr, err := dcr.wallet.GetTransaction(ctx, &txHash)
  6381  		if errors.Is(err, asset.CoinNotFoundError) {
  6382  			err = txHistoryDB.RemoveTx(txHash.String())
  6383  			if err == nil {
  6384  				dcr.pendingTxsMtx.Lock()
  6385  				delete(dcr.pendingTxs, txHash)
  6386  				dcr.pendingTxsMtx.Unlock()
  6387  			} else {
  6388  				// Leave it in the pendingPendingTxs and attempt to remove it
  6389  				// again next time.
  6390  				dcr.log.Errorf("Error removing tx %s from the history store: %v", txHash.String(), err)
  6391  			}
  6392  			return
  6393  		}
  6394  		if err != nil {
  6395  			dcr.log.Errorf("Error getting transaction %s: %v", txHash, err)
  6396  			return
  6397  		}
  6398  
  6399  		var updated bool
  6400  		if gtr.BlockHash != "" {
  6401  			blockHash, err := chainhash.NewHashFromStr(gtr.BlockHash)
  6402  			if err != nil {
  6403  				dcr.log.Errorf("Error decoding block hash %s: %v", gtr.BlockHash, err)
  6404  				return
  6405  			}
  6406  			block, err := dcr.wallet.GetBlockHeader(ctx, blockHash)
  6407  			if err != nil {
  6408  				dcr.log.Errorf("Error getting block height for %s: %v", blockHash, err)
  6409  				return
  6410  			}
  6411  			blockHeight := block.Height
  6412  			if tx.BlockNumber != uint64(blockHeight) || tx.Timestamp != uint64(block.Timestamp.Unix()) {
  6413  				tx.BlockNumber = uint64(blockHeight)
  6414  				tx.Timestamp = uint64(block.Timestamp.Unix())
  6415  				updated = true
  6416  			}
  6417  		} else if gtr.BlockHash == "" && tx.BlockNumber != 0 {
  6418  			tx.BlockNumber = 0
  6419  			tx.Timestamp = 0
  6420  			updated = true
  6421  		}
  6422  
  6423  		var confs uint64
  6424  		if tx.BlockNumber > 0 && tip >= tx.BlockNumber {
  6425  			confs = tip - tx.BlockNumber + 1
  6426  		}
  6427  		if confs >= defaultRedeemConfTarget {
  6428  			tx.Confirmed = true
  6429  			updated = true
  6430  		}
  6431  
  6432  		if updated {
  6433  			err = txHistoryDB.StoreTx(tx)
  6434  			if err != nil {
  6435  				dcr.log.Errorf("Error updating tx %s: %v", txHash, err)
  6436  				return
  6437  			}
  6438  
  6439  			dcr.pendingTxsMtx.Lock()
  6440  			if tx.Confirmed {
  6441  				delete(dcr.pendingTxs, txHash)
  6442  			} else {
  6443  				dcr.pendingTxs[txHash] = tx
  6444  			}
  6445  			dcr.pendingTxsMtx.Unlock()
  6446  
  6447  			dcr.emit.TransactionNote(tx.WalletTransaction, false)
  6448  		}
  6449  	}
  6450  
  6451  	for hash, tx := range pendingTxsCopy {
  6452  		if dcr.ctx.Err() != nil {
  6453  			return
  6454  		}
  6455  		handlePendingTx(hash, &tx)
  6456  	}
  6457  }
  6458  
  6459  func (dcr *ExchangeWallet) markTxAsSubmitted(txHash *chainhash.Hash) {
  6460  	txHistoryDB := dcr.txDB()
  6461  	if txHistoryDB == nil {
  6462  		return
  6463  	}
  6464  
  6465  	err := txHistoryDB.MarkTxAsSubmitted(txHash.String())
  6466  	if err != nil {
  6467  		dcr.log.Errorf("failed to mark tx as submitted in tx history db: %v", err)
  6468  	}
  6469  
  6470  	dcr.pendingTxsMtx.Lock()
  6471  	wt, found := dcr.pendingTxs[*txHash]
  6472  	dcr.pendingTxsMtx.Unlock()
  6473  
  6474  	if !found {
  6475  		dcr.log.Errorf("Transaction %s not found in pending txs", txHash)
  6476  		return
  6477  	}
  6478  
  6479  	wt.Submitted = true
  6480  
  6481  	dcr.emit.TransactionNote(wt.WalletTransaction, true)
  6482  }
  6483  
  6484  func (dcr *ExchangeWallet) removeTxFromHistory(txHash *chainhash.Hash) {
  6485  	txHistoryDB := dcr.txDB()
  6486  	if txHistoryDB == nil {
  6487  		return
  6488  	}
  6489  
  6490  	dcr.pendingTxsMtx.Lock()
  6491  	delete(dcr.pendingTxs, *txHash)
  6492  	dcr.pendingTxsMtx.Unlock()
  6493  
  6494  	err := txHistoryDB.RemoveTx(txHash.String())
  6495  	if err != nil {
  6496  		dcr.log.Errorf("failed to remove tx from tx history db: %v", err)
  6497  	}
  6498  }
  6499  
  6500  func (dcr *ExchangeWallet) addTxToHistory(wt *asset.WalletTransaction, txHash *chainhash.Hash, submitted bool, skipNotes ...bool) {
  6501  	txHistoryDB := dcr.txDB()
  6502  	if txHistoryDB == nil {
  6503  		return
  6504  	}
  6505  
  6506  	ewt := &btc.ExtendedWalletTx{
  6507  		WalletTransaction: wt,
  6508  		Submitted:         submitted,
  6509  	}
  6510  
  6511  	if wt.BlockNumber == 0 {
  6512  		dcr.pendingTxsMtx.Lock()
  6513  		dcr.pendingTxs[*txHash] = ewt
  6514  		dcr.pendingTxsMtx.Unlock()
  6515  	}
  6516  
  6517  	err := txHistoryDB.StoreTx(ewt)
  6518  	if err != nil {
  6519  		dcr.log.Errorf("failed to store tx in tx history db: %v", err)
  6520  	}
  6521  
  6522  	skipNote := len(skipNotes) > 0 && skipNotes[0]
  6523  	if submitted && !skipNote {
  6524  		dcr.emit.TransactionNote(wt, true)
  6525  	}
  6526  }
  6527  
  6528  func (dcr *ExchangeWallet) checkPeers(ctx context.Context) {
  6529  	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
  6530  	defer cancel()
  6531  	numPeers, err := dcr.wallet.PeerCount(ctx)
  6532  	if err != nil { // e.g. dcrd passthrough fail in non-SPV mode
  6533  		prevPeer := atomic.SwapUint32(&dcr.lastPeerCount, 0)
  6534  		if prevPeer != 0 {
  6535  			dcr.log.Errorf("Failed to get peer count: %v", err)
  6536  			dcr.peersChange(0, err)
  6537  		}
  6538  		return
  6539  	}
  6540  	prevPeer := atomic.SwapUint32(&dcr.lastPeerCount, numPeers)
  6541  	if prevPeer != numPeers {
  6542  		dcr.peersChange(numPeers, nil)
  6543  	}
  6544  }
  6545  
  6546  func (dcr *ExchangeWallet) monitorPeers(ctx context.Context) {
  6547  	ticker := time.NewTicker(peerCountTicker)
  6548  	defer ticker.Stop()
  6549  	for {
  6550  		dcr.checkPeers(ctx)
  6551  
  6552  		select {
  6553  		case <-ticker.C:
  6554  		case <-ctx.Done():
  6555  			return
  6556  		}
  6557  	}
  6558  }
  6559  
  6560  func (dcr *ExchangeWallet) emitTipChange(height int64) {
  6561  	var data any
  6562  	sinfo, err := dcr.wallet.StakeInfo(dcr.ctx)
  6563  	if err != nil {
  6564  		dcr.log.Errorf("Error getting stake info for tip change notification data: %v", err)
  6565  	} else {
  6566  		data = &struct {
  6567  			TicketPrice   uint64            `json:"ticketPrice"`
  6568  			VotingSubsidy uint64            `json:"votingSubsidy"`
  6569  			Stats         asset.TicketStats `json:"stats"`
  6570  		}{
  6571  			TicketPrice:   uint64(sinfo.Sdiff),
  6572  			Stats:         dcr.ticketStatsFromStakeInfo(sinfo),
  6573  			VotingSubsidy: dcr.voteSubsidy(height),
  6574  		}
  6575  	}
  6576  
  6577  	dcr.emit.TipChange(uint64(height), data)
  6578  	go dcr.runTicketBuyer()
  6579  }
  6580  
  6581  func (dcr *ExchangeWallet) emitBalance() {
  6582  	if bal, err := dcr.Balance(); err != nil {
  6583  		dcr.log.Errorf("Error getting balance after mempool tx notification: %v", err)
  6584  	} else {
  6585  		dcr.emit.BalanceChange(bal)
  6586  	}
  6587  }
  6588  
  6589  // monitorBlocks pings for new blocks and runs the tipChange callback function
  6590  // when the block changes. New blocks are also scanned for potential contract
  6591  // redeems.
  6592  func (dcr *ExchangeWallet) monitorBlocks(ctx context.Context) {
  6593  	ticker := time.NewTicker(blockTicker)
  6594  	defer ticker.Stop()
  6595  
  6596  	var walletBlock <-chan *block
  6597  	if notifier, isNotifier := dcr.wallet.(tipNotifier); isNotifier {
  6598  		walletBlock = notifier.tipFeed()
  6599  	}
  6600  
  6601  	// A polledBlock is a block found during polling, but whose broadcast has
  6602  	// been queued in anticipation of a wallet notification.
  6603  	type polledBlock struct {
  6604  		*block
  6605  		queue *time.Timer
  6606  	}
  6607  
  6608  	// queuedBlock is the currently queued, polling-discovered block that will
  6609  	// be broadcast after a timeout if the wallet doesn't send the matching
  6610  	// notification.
  6611  	var queuedBlock *polledBlock
  6612  
  6613  	// checkTip captures queuedBlock and walletBlock.
  6614  	checkTip := func() {
  6615  		ctxInternal, cancel0 := context.WithTimeout(ctx, 4*time.Second)
  6616  		defer cancel0()
  6617  
  6618  		newTip, err := dcr.getBestBlock(ctxInternal)
  6619  		if err != nil {
  6620  			dcr.log.Errorf("failed to get best block: %v", err)
  6621  			return
  6622  		}
  6623  
  6624  		if dcr.cachedBestBlock().hash.IsEqual(newTip.hash) {
  6625  			return
  6626  		}
  6627  
  6628  		if walletBlock == nil {
  6629  			dcr.handleTipChange(ctx, newTip.hash, newTip.height)
  6630  			return
  6631  		}
  6632  
  6633  		// Queue it for reporting, but don't send it right away. Give the wallet
  6634  		// a chance to provide their block update. SPV wallet may need more time
  6635  		// after storing the block header to fetch and scan filters and issue
  6636  		// the FilteredBlockConnected report.
  6637  		if queuedBlock != nil {
  6638  			queuedBlock.queue.Stop()
  6639  		}
  6640  		blockAllowance := walletBlockAllowance
  6641  		ctxInternal, cancel1 := context.WithTimeout(ctx, 4*time.Second)
  6642  		defer cancel1()
  6643  		ss, err := dcr.wallet.SyncStatus(ctxInternal)
  6644  		if err != nil {
  6645  			dcr.log.Errorf("Error retrieving sync status before queuing polled block: %v", err)
  6646  		} else if !ss.Synced {
  6647  			blockAllowance *= 10
  6648  		}
  6649  		queuedBlock = &polledBlock{
  6650  			block: newTip,
  6651  			queue: time.AfterFunc(blockAllowance, func() {
  6652  				if ss, _ := dcr.SyncStatus(); ss != nil && ss.Synced {
  6653  					dcr.log.Warnf("Reporting a block found in polling that the wallet apparently "+
  6654  						"never reported: %s (%d). If you see this message repeatedly, it may indicate "+
  6655  						"an issue with the wallet.", newTip.hash, newTip.height)
  6656  				}
  6657  				dcr.handleTipChange(ctx, newTip.hash, newTip.height)
  6658  			}),
  6659  		}
  6660  	}
  6661  
  6662  	for {
  6663  		select {
  6664  		case <-ticker.C:
  6665  			checkTip()
  6666  
  6667  		case walletTip := <-walletBlock:
  6668  			if walletTip == nil {
  6669  				// Mempool tx seen.
  6670  				dcr.emitBalance()
  6671  
  6672  				tip := dcr.cachedBestBlock()
  6673  				dcr.syncTxHistory(ctx, uint64(tip.height))
  6674  				continue
  6675  			}
  6676  			if queuedBlock != nil && walletTip.height >= queuedBlock.height {
  6677  				if !queuedBlock.queue.Stop() && walletTip.hash == queuedBlock.hash {
  6678  					continue
  6679  				}
  6680  				queuedBlock = nil
  6681  			}
  6682  			dcr.handleTipChange(ctx, walletTip.hash, walletTip.height)
  6683  		case <-ctx.Done():
  6684  			return
  6685  		}
  6686  
  6687  		// Ensure context cancellation takes priority before the next iteration.
  6688  		if ctx.Err() != nil {
  6689  			return
  6690  		}
  6691  	}
  6692  }
  6693  
  6694  func (dcr *ExchangeWallet) handleTipChange(ctx context.Context, newTipHash *chainhash.Hash, newTipHeight int64) {
  6695  	if dcr.ctx.Err() != nil {
  6696  		return
  6697  	}
  6698  
  6699  	// Lock to avoid concurrent handleTipChange execution for simplicity.
  6700  	dcr.handleTipMtx.Lock()
  6701  	defer dcr.handleTipMtx.Unlock()
  6702  
  6703  	prevTip := dcr.currentTip.Swap(&block{newTipHeight, newTipHash}).(*block)
  6704  
  6705  	dcr.log.Tracef("tip change: %d (%s) => %d (%s)", prevTip.height, prevTip.hash, newTipHeight, newTipHash)
  6706  
  6707  	dcr.emitTipChange(newTipHeight)
  6708  
  6709  	if dcr.cycleMixer != nil {
  6710  		dcr.cycleMixer()
  6711  	}
  6712  
  6713  	dcr.wg.Add(1)
  6714  	go func() {
  6715  		dcr.syncTxHistory(ctx, uint64(newTipHeight))
  6716  		dcr.wg.Done()
  6717  	}()
  6718  
  6719  	// Search for contract redemption in new blocks if there
  6720  	// are contracts pending redemption.
  6721  	dcr.findRedemptionMtx.RLock()
  6722  	pendingContractsCount := len(dcr.findRedemptionQueue)
  6723  	contractOutpoints := make([]outPoint, 0, pendingContractsCount)
  6724  	for contractOutpoint := range dcr.findRedemptionQueue {
  6725  		contractOutpoints = append(contractOutpoints, contractOutpoint)
  6726  	}
  6727  	dcr.findRedemptionMtx.RUnlock()
  6728  	if pendingContractsCount == 0 {
  6729  		return
  6730  	}
  6731  
  6732  	startHeight := prevTip.height + 1
  6733  
  6734  	// Redemption search would be compromised if the starting point cannot
  6735  	// be determined, as searching just the new tip might result in blocks
  6736  	// being omitted from the search operation. If that happens, cancel all
  6737  	// find redemption requests in queue.
  6738  	notifyFatalFindRedemptionError := func(s string, a ...any) {
  6739  		dcr.fatalFindRedemptionsError(fmt.Errorf("tipChange handler - "+s, a...), contractOutpoints)
  6740  	}
  6741  
  6742  	// Check if the previous tip is still part of the mainchain (prevTip confs >= 0).
  6743  	// Redemption search would typically resume from prevTip.height + 1 unless the
  6744  	// previous tip was re-orged out of the mainchain, in which case redemption
  6745  	// search will resume from the mainchain ancestor of the previous tip.
  6746  	prevTipHeader, isMainchain, _, err := dcr.blockHeader(ctx, prevTip.hash)
  6747  	if err != nil {
  6748  		// Redemption search cannot continue reliably without knowing if there
  6749  		// was a reorg, cancel all find redemption requests in queue.
  6750  		notifyFatalFindRedemptionError("blockHeader error for prev tip hash %s: %w",
  6751  			prevTip.hash, err)
  6752  		return
  6753  	}
  6754  	if !isMainchain {
  6755  		// The previous tip is no longer part of the mainchain. Crawl blocks
  6756  		// backwards until finding a mainchain block. Start with the block
  6757  		// that is the immediate ancestor to the previous tip.
  6758  		ancestorBlockHash, ancestorHeight, err := dcr.mainchainAncestor(ctx, &prevTipHeader.PrevBlock)
  6759  		if err != nil {
  6760  			notifyFatalFindRedemptionError("find mainchain ancestor for prev block: %s: %w", prevTipHeader.PrevBlock, err)
  6761  			return
  6762  		}
  6763  
  6764  		dcr.log.Debugf("reorg detected during tip change from height %d (%s) to %d (%s)",
  6765  			ancestorHeight, ancestorBlockHash, newTipHeight, newTipHash)
  6766  
  6767  		startHeight = ancestorHeight // have to recheck orphaned blocks again
  6768  	}
  6769  
  6770  	// Run the redemption search from the startHeight determined above up
  6771  	// till the current tip height.
  6772  	dcr.findRedemptionsInBlockRange(startHeight, newTipHeight, contractOutpoints)
  6773  }
  6774  
  6775  func (dcr *ExchangeWallet) getBestBlock(ctx context.Context) (*block, error) {
  6776  	hash, height, err := dcr.wallet.GetBestBlock(ctx)
  6777  	if err != nil {
  6778  		return nil, err
  6779  	}
  6780  	return &block{hash: hash, height: height}, nil
  6781  }
  6782  
  6783  // mainchainAncestor crawls blocks backwards starting at the provided hash
  6784  // until finding a mainchain block. Returns the first mainchain block found.
  6785  func (dcr *ExchangeWallet) mainchainAncestor(ctx context.Context, blockHash *chainhash.Hash) (*chainhash.Hash, int64, error) {
  6786  	checkHash := blockHash
  6787  	for {
  6788  		checkBlock, isMainchain, _, err := dcr.blockHeader(ctx, checkHash)
  6789  		if err != nil {
  6790  			return nil, 0, fmt.Errorf("getblockheader error for block %s: %w", checkHash, err)
  6791  		}
  6792  		if isMainchain {
  6793  			// This is a mainchain block, return the hash and height.
  6794  			return checkHash, int64(checkBlock.Height), nil
  6795  		}
  6796  		if checkBlock.Height == 0 {
  6797  			// Crawled back to genesis block without finding a mainchain ancestor
  6798  			// for the previous tip. Should never happen!
  6799  			return nil, 0, fmt.Errorf("no mainchain ancestor found for block %s", blockHash)
  6800  		}
  6801  		checkHash = &checkBlock.PrevBlock
  6802  	}
  6803  }
  6804  
  6805  // blockHeader returns the *BlockHeader for the specified block hash, and bools
  6806  // indicating if the block is mainchain, and approved by stakeholders.
  6807  // validMainchain will always be false if mainchain is false; mainchain can be
  6808  // true for an invalidated block.
  6809  func (dcr *ExchangeWallet) blockHeader(ctx context.Context, blockHash *chainhash.Hash) (blockHeader *BlockHeader, mainchain, validMainchain bool, err error) {
  6810  	blockHeader, err = dcr.wallet.GetBlockHeader(ctx, blockHash)
  6811  	if err != nil {
  6812  		return nil, false, false, fmt.Errorf("GetBlockHeader error for block %s: %w", blockHash, err)
  6813  	}
  6814  	if blockHeader.Confirmations < 0 { // not mainchain, really just == -1, but catch all unexpected
  6815  		dcr.log.Warnf("Block %v is a SIDE CHAIN block at height %d!", blockHash, blockHeader.Height)
  6816  		return blockHeader, false, false, nil
  6817  	}
  6818  
  6819  	// It's mainchain. Now check if there is a validating block.
  6820  	if blockHeader.NextHash == nil { // we're at the tip
  6821  		return blockHeader, true, true, nil
  6822  	}
  6823  
  6824  	nextHeader, err := dcr.wallet.GetBlockHeader(ctx, blockHeader.NextHash)
  6825  	if err != nil {
  6826  		return nil, false, false, fmt.Errorf("error fetching validating block: %w", err)
  6827  	}
  6828  
  6829  	validMainchain = nextHeader.VoteBits&1 != 0
  6830  	if !validMainchain {
  6831  		dcr.log.Warnf("Block %v found in mainchain, but stakeholder DISAPPROVED!", blockHash)
  6832  	}
  6833  	return blockHeader, true, validMainchain, nil
  6834  }
  6835  
  6836  func (dcr *ExchangeWallet) cachedBestBlock() *block {
  6837  	return dcr.currentTip.Load().(*block)
  6838  }
  6839  
  6840  // wireBytes dumps the serialized transaction bytes.
  6841  func (dcr *ExchangeWallet) wireBytes(tx *wire.MsgTx) []byte {
  6842  	s, err := tx.Bytes()
  6843  	// wireBytes is just used for logging, and a serialization error is
  6844  	// extremely unlikely, so just log the error and return the nil bytes.
  6845  	if err != nil {
  6846  		dcr.log.Errorf("error serializing transaction: %v", err)
  6847  	}
  6848  	return s
  6849  }
  6850  
  6851  // Convert the DCR value to atoms.
  6852  func toAtoms(v float64) uint64 {
  6853  	return uint64(math.Round(v * conventionalConversionFactor))
  6854  }
  6855  
  6856  // toCoinID converts the tx hash and vout to a coin ID, as a []byte.
  6857  func toCoinID(txHash *chainhash.Hash, vout uint32) []byte {
  6858  	coinID := make([]byte, chainhash.HashSize+4)
  6859  	copy(coinID[:chainhash.HashSize], txHash[:])
  6860  	binary.BigEndian.PutUint32(coinID[chainhash.HashSize:], vout)
  6861  	return coinID
  6862  }
  6863  
  6864  // decodeCoinID decodes the coin ID into a tx hash and a vout.
  6865  func decodeCoinID(coinID dex.Bytes) (*chainhash.Hash, uint32, error) {
  6866  	if len(coinID) != 36 {
  6867  		return nil, 0, fmt.Errorf("coin ID wrong length. expected 36, got %d", len(coinID))
  6868  	}
  6869  	var txHash chainhash.Hash
  6870  	copy(txHash[:], coinID[:32])
  6871  	return &txHash, binary.BigEndian.Uint32(coinID[32:]), nil
  6872  }
  6873  
  6874  // reduceMsgTx computes the total input and output amounts, the resulting
  6875  // absolute fee and fee rate, and the serialized transaction size.
  6876  func reduceMsgTx(tx *wire.MsgTx) (in, out, fees, rate, size uint64) {
  6877  	for _, txIn := range tx.TxIn {
  6878  		in += uint64(txIn.ValueIn)
  6879  	}
  6880  	for _, txOut := range tx.TxOut {
  6881  		out += uint64(txOut.Value)
  6882  	}
  6883  	fees = in - out
  6884  	size = uint64(tx.SerializeSize())
  6885  	rate = fees / size
  6886  	return
  6887  }
  6888  
  6889  // toDCR returns a float representation in conventional units for the given
  6890  // atoms.
  6891  func toDCR[V uint64 | int64](v V) float64 {
  6892  	return dcrutil.Amount(v).ToCoin()
  6893  }
  6894  
  6895  // calcBumpedRate calculated a bump on the baseRate. If bump is nil, the
  6896  // baseRate is returned directly. If *bump is out of range, an error is
  6897  // returned.
  6898  func calcBumpedRate(baseRate uint64, bump *float64) (uint64, error) {
  6899  	if bump == nil {
  6900  		return baseRate, nil
  6901  	}
  6902  	userBump := *bump
  6903  	if userBump > 2.0 {
  6904  		return baseRate, fmt.Errorf("fee bump %f is higher than the 2.0 limit", userBump)
  6905  	}
  6906  	if userBump < 1.0 {
  6907  		return baseRate, fmt.Errorf("fee bump %f is lower than 1", userBump)
  6908  	}
  6909  	return uint64(math.Round(float64(baseRate) * userBump)), nil
  6910  }
  6911  
  6912  func float64PtrStr(v *float64) string {
  6913  	if v == nil {
  6914  		return "nil"
  6915  	}
  6916  	return strconv.FormatFloat(*v, 'f', 8, 64)
  6917  }
  6918  
  6919  // WalletTransaction returns a transaction that either the wallet has made or
  6920  // one in which the wallet has received funds. The txID should be either a
  6921  // coin ID or a transaction hash in hexadecimal form.
  6922  func (dcr *ExchangeWallet) WalletTransaction(ctx context.Context, txID string) (*asset.WalletTransaction, error) {
  6923  	coinID, err := hex.DecodeString(txID)
  6924  	if err == nil {
  6925  		txHash, _, err := decodeCoinID(coinID)
  6926  		if err == nil {
  6927  			txID = txHash.String()
  6928  		}
  6929  	}
  6930  
  6931  	txHistoryDB := dcr.txDB()
  6932  	if txHistoryDB == nil {
  6933  		return nil, fmt.Errorf("tx database not initialized")
  6934  	}
  6935  
  6936  	tx, err := txHistoryDB.GetTx(txID)
  6937  	if err != nil && !errors.Is(err, asset.CoinNotFoundError) {
  6938  		return nil, err
  6939  	}
  6940  	if tx != nil && tx.Confirmed {
  6941  		return tx, nil
  6942  	}
  6943  
  6944  	txHash, err := chainhash.NewHashFromStr(txID)
  6945  	if err != nil {
  6946  		return nil, fmt.Errorf("error decoding txid %s: %w", txID, err)
  6947  	}
  6948  	gtr, err := dcr.wallet.GetTransaction(ctx, txHash)
  6949  	if err != nil {
  6950  		return nil, fmt.Errorf("error getting transaction %s: %w", txID, err)
  6951  	}
  6952  	if len(gtr.Details) == 0 {
  6953  		return nil, fmt.Errorf("no details found for transaction %s", txID)
  6954  	}
  6955  
  6956  	var blockHeight, blockTime uint64
  6957  	if gtr.BlockHash != "" {
  6958  		blockHash, err := chainhash.NewHashFromStr(gtr.BlockHash)
  6959  		if err != nil {
  6960  			return nil, fmt.Errorf("error decoding block hash %s: %w", gtr.BlockHash, err)
  6961  		}
  6962  		blockHeader, err := dcr.wallet.GetBlockHeader(ctx, blockHash)
  6963  		if err != nil {
  6964  			return nil, fmt.Errorf("error getting block header for block %s: %w", blockHash, err)
  6965  		}
  6966  		blockHeight = uint64(blockHeader.Height)
  6967  		blockTime = uint64(blockHeader.Timestamp.Unix())
  6968  	}
  6969  
  6970  	updated := tx == nil
  6971  	if tx == nil {
  6972  		blockIndex := int64(blockHeight)
  6973  		regularTx := walletjson.LTTTRegular
  6974  		tx, err = dcr.idUnknownTx(ctx, &ListTransactionsResult{
  6975  			TxID:       txID,
  6976  			BlockIndex: &blockIndex,
  6977  			BlockTime:  int64(blockTime),
  6978  			Send:       gtr.Details[0].Category == "send",
  6979  			TxType:     &regularTx,
  6980  		})
  6981  		if err != nil {
  6982  			return nil, fmt.Errorf("xerror identifying transaction: %v", err)
  6983  		}
  6984  	}
  6985  
  6986  	if tx.BlockNumber != blockHeight || tx.Timestamp != blockTime {
  6987  		tx.BlockNumber = blockHeight
  6988  		tx.Timestamp = blockTime
  6989  		updated = true
  6990  	}
  6991  
  6992  	if updated {
  6993  		dcr.addTxToHistory(tx, txHash, true)
  6994  	}
  6995  
  6996  	// If the wallet knows about the transaction, it will be part of the
  6997  	// available balance, so we always return Confirmed = true.
  6998  	tx.Confirmed = true
  6999  	return tx, nil
  7000  }
  7001  
  7002  // TxHistory returns all the transactions the wallet has made. If refID is nil,
  7003  // then transactions starting from the most recent are returned (past is ignored).
  7004  // If past is true, the transactions prior to the refID are returned, otherwise
  7005  // the transactions after the refID are returned. n is the number of
  7006  // transactions to return. If n is <= 0, all the transactions will be returned.
  7007  func (dcr *ExchangeWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) {
  7008  	txHistoryDB := dcr.txDB()
  7009  	if txHistoryDB == nil {
  7010  		return nil, fmt.Errorf("tx database not initialized")
  7011  	}
  7012  
  7013  	return txHistoryDB.GetTxs(n, refID, past)
  7014  }
  7015  
  7016  // ConfirmRedemption returns how many confirmations a redemption has. Normally
  7017  // this is very straightforward. However there are two situations that have come
  7018  // up that this also handles. One is when the wallet can not find the redemption
  7019  // transaction. This is most likely because the fee was set too low and the tx
  7020  // was removed from the mempool. In the case where it is not found, this will
  7021  // send a new tx using the provided fee suggestion. The second situation
  7022  // this watches for is a transaction that we can find but has been sitting in
  7023  // the mempool for a long time. This has been observed with the wallet in SPV
  7024  // mode and the transaction inputs having been spent by another transaction. The
  7025  // wallet will not pick up on this so we could tell it to abandon the original
  7026  // transaction and, again, send a new one using the provided feeSuggestion, but
  7027  // only warning for now. This method should not be run for the same redemption
  7028  // concurrently as it need to watch a new redeem transaction before finishing.
  7029  func (dcr *ExchangeWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, feeSuggestion uint64) (*asset.ConfirmRedemptionStatus, error) {
  7030  	txHash, _, err := decodeCoinID(coinID)
  7031  	if err != nil {
  7032  		return nil, err
  7033  	}
  7034  
  7035  	var secretHash [32]byte
  7036  	copy(secretHash[:], redemption.Spends.SecretHash)
  7037  	dcr.mempoolRedeemsMtx.RLock()
  7038  	mRedeem, have := dcr.mempoolRedeems[secretHash]
  7039  	dcr.mempoolRedeemsMtx.RUnlock()
  7040  
  7041  	var deleteMempoolRedeem bool
  7042  	defer func() {
  7043  		if deleteMempoolRedeem {
  7044  			dcr.mempoolRedeemsMtx.Lock()
  7045  			delete(dcr.mempoolRedeems, secretHash)
  7046  			dcr.mempoolRedeemsMtx.Unlock()
  7047  		}
  7048  	}()
  7049  
  7050  	tx, err := dcr.wallet.GetTransaction(dcr.ctx, txHash)
  7051  	if err != nil && !errors.Is(err, asset.CoinNotFoundError) {
  7052  		return nil, fmt.Errorf("problem searching for redemption transaction %s: %w", txHash, err)
  7053  	}
  7054  	if err == nil {
  7055  		if have && mRedeem.txHash == *txHash {
  7056  			if tx.Confirmations == 0 && time.Now().After(mRedeem.firstSeen.Add(maxRedeemMempoolAge)) {
  7057  				// Transaction has been sitting in the mempool
  7058  				// for a long time now.
  7059  				//
  7060  				// TODO: Consider abandoning.
  7061  				redeemAge := time.Since(mRedeem.firstSeen)
  7062  				dcr.log.Warnf("Redemption transaction %v has been in the mempool for %v which is too long.", txHash, redeemAge)
  7063  			}
  7064  		} else {
  7065  			if have {
  7066  				// This should not happen. Core has told us to
  7067  				// watch a new redeem with a different transaction
  7068  				// hash for a trade we were already watching.
  7069  				return nil, fmt.Errorf("tx were were watching %s for redeem with secret hash %x being "+
  7070  					"replaced by tx %s. core should not be replacing the transaction. maybe ConfirmRedemption "+
  7071  					"is being run concurrently for the same redeem", mRedeem.txHash, secretHash, *txHash)
  7072  			}
  7073  			// Will hit this if bisonw was restarted with an actively
  7074  			// redeeming swap.
  7075  			dcr.mempoolRedeemsMtx.Lock()
  7076  			dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: *txHash, firstSeen: time.Now()}
  7077  			dcr.mempoolRedeemsMtx.Unlock()
  7078  		}
  7079  		if tx.Confirmations >= requiredRedeemConfirms {
  7080  			deleteMempoolRedeem = true
  7081  		}
  7082  		return &asset.ConfirmRedemptionStatus{
  7083  			Confs:  uint64(tx.Confirmations),
  7084  			Req:    requiredRedeemConfirms,
  7085  			CoinID: coinID,
  7086  		}, nil
  7087  	}
  7088  
  7089  	// Redemption transaction is missing from the point of view of our wallet!
  7090  	// Unlikely, but possible it was redeemed by another transaction. We
  7091  	// assume a contract past its locktime cannot make it here, so it must
  7092  	// not be refunded. Check if the contract is still an unspent output.
  7093  
  7094  	swapHash, vout, err := decodeCoinID(redemption.Spends.Coin.ID())
  7095  	if err != nil {
  7096  		return nil, err
  7097  	}
  7098  
  7099  	_, _, spentStatus, err := dcr.lookupTxOutput(dcr.ctx, swapHash, vout)
  7100  	if err != nil {
  7101  		return nil, fmt.Errorf("error finding unspent contract: %w", err)
  7102  	}
  7103  
  7104  	switch spentStatus {
  7105  	case -1, 1:
  7106  		// First find the block containing the output itself.
  7107  		scriptAddr, err := stdaddr.NewAddressScriptHashV0(redemption.Spends.Contract, dcr.chainParams)
  7108  		if err != nil {
  7109  			return nil, fmt.Errorf("error encoding contract address: %w", err)
  7110  		}
  7111  		_, pkScript := scriptAddr.PaymentScript()
  7112  		outFound, block, err := dcr.externalTxOutput(dcr.ctx, newOutPoint(swapHash, vout),
  7113  			pkScript, time.Now().Add(-60*24*time.Hour)) // search up to 60 days ago
  7114  		if err != nil {
  7115  			return nil, err // possibly the contract is still in mempool
  7116  		}
  7117  		spent, err := dcr.isOutputSpent(dcr.ctx, outFound)
  7118  		if err != nil {
  7119  			return nil, fmt.Errorf("error checking if contract %v:%d is spent: %w", *swapHash, vout, err)
  7120  		}
  7121  		if !spent {
  7122  			break
  7123  		}
  7124  		vin := -1
  7125  		spendTx := outFound.spenderTx
  7126  		for i := range spendTx.TxIn {
  7127  			sigScript := spendTx.TxIn[i].SignatureScript
  7128  			sigScriptLen := len(sigScript)
  7129  			if sigScriptLen < dexdcr.SwapContractSize {
  7130  				continue
  7131  			}
  7132  			// The spent contract is at the end of the signature
  7133  			// script. Lop off the front half.
  7134  			script := sigScript[sigScriptLen-dexdcr.SwapContractSize:]
  7135  			_, _, _, sh, err := dexdcr.ExtractSwapDetails(script, dcr.chainParams)
  7136  			if err != nil {
  7137  				// This is not our script, but not necessarily
  7138  				// a problem.
  7139  				dcr.log.Tracef("Error encountered searching for the input that spends %v, "+
  7140  					"extracting swap details from vin %d of %d. Probably not a problem: %v.",
  7141  					spendTx.TxHash(), i, len(spendTx.TxIn), err)
  7142  				continue
  7143  			}
  7144  			if bytes.Equal(sh[:], secretHash[:]) {
  7145  				vin = i
  7146  				break
  7147  			}
  7148  		}
  7149  		if vin >= 0 {
  7150  			_, height, err := dcr.wallet.GetBestBlock(dcr.ctx)
  7151  			if err != nil {
  7152  				return nil, err
  7153  			}
  7154  			confs := uint64(height - block.height)
  7155  			hash := spendTx.TxHash()
  7156  			if confs < requiredRedeemConfirms {
  7157  				dcr.mempoolRedeemsMtx.Lock()
  7158  				dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: hash, firstSeen: time.Now()}
  7159  				dcr.mempoolRedeemsMtx.Unlock()
  7160  			}
  7161  			return &asset.ConfirmRedemptionStatus{
  7162  				Confs:  confs,
  7163  				Req:    requiredRedeemConfirms,
  7164  				CoinID: toCoinID(&hash, uint32(vin)),
  7165  			}, nil
  7166  		}
  7167  		dcr.log.Warnf("Contract coin %v spent by someone but not sure who.", redemption.Spends.Coin.ID())
  7168  		// Incorrect, but we will be in a loop of erroring if we don't
  7169  		// return something. We were unable to find the spender for some
  7170  		// reason.
  7171  
  7172  		// May be still in the map if abandonTx failed.
  7173  		deleteMempoolRedeem = true
  7174  
  7175  		return &asset.ConfirmRedemptionStatus{
  7176  			Confs:  requiredRedeemConfirms,
  7177  			Req:    requiredRedeemConfirms,
  7178  			CoinID: coinID,
  7179  		}, nil
  7180  	}
  7181  
  7182  	// The contract has not yet been redeemed, but it seems the redeeming
  7183  	// tx has disappeared. Assume the fee was too low at the time and it
  7184  	// was eventually purged from the mempool. Attempt to redeem again with
  7185  	// a currently reasonable fee.
  7186  
  7187  	form := &asset.RedeemForm{
  7188  		Redemptions:   []*asset.Redemption{redemption},
  7189  		FeeSuggestion: feeSuggestion,
  7190  	}
  7191  	_, coin, _, err := dcr.Redeem(form)
  7192  	if err != nil {
  7193  		return nil, fmt.Errorf("unable to re-redeem %s: %w", redemption.Spends.Coin.ID(), err)
  7194  	}
  7195  
  7196  	coinID = coin.ID()
  7197  	newRedeemHash, _, err := decodeCoinID(coinID)
  7198  	if err != nil {
  7199  		return nil, err
  7200  	}
  7201  
  7202  	dcr.mempoolRedeemsMtx.Lock()
  7203  	dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: *newRedeemHash, firstSeen: time.Now()}
  7204  	dcr.mempoolRedeemsMtx.Unlock()
  7205  
  7206  	return &asset.ConfirmRedemptionStatus{
  7207  		Confs:  0,
  7208  		Req:    requiredRedeemConfirms,
  7209  		CoinID: coinID,
  7210  	}, nil
  7211  }
  7212  
  7213  var _ asset.GeocodeRedeemer = (*ExchangeWallet)(nil)
  7214  
  7215  // RedeemGeocode redeems funds from a geocode game tx to this wallet.
  7216  func (dcr *ExchangeWallet) RedeemGeocode(code []byte, msg string) (dex.Bytes, uint64, error) {
  7217  	msgLen := len([]byte(msg))
  7218  	if msgLen > stdscript.MaxDataCarrierSizeV0 {
  7219  		return nil, 0, fmt.Errorf("message is too long. must be %d > %d", msgLen, stdscript.MaxDataCarrierSizeV0)
  7220  	}
  7221  
  7222  	k, err := hdkeychain.NewMaster(code, dcr.chainParams)
  7223  	if err != nil {
  7224  		return nil, 0, fmt.Errorf("error generating key from bond: %w", err)
  7225  	}
  7226  	gameKey, err := k.SerializedPrivKey()
  7227  	if err != nil {
  7228  		return nil, 0, fmt.Errorf("error serializing private key: %w", err)
  7229  	}
  7230  
  7231  	gamePub := k.SerializedPubKey()
  7232  	gameAddr, err := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(dcrutil.Hash160(gamePub), dcr.chainParams)
  7233  	if err != nil {
  7234  		return nil, 0, fmt.Errorf("error generating address: %w", err)
  7235  	}
  7236  
  7237  	gameTxs, err := getDcrdataTxs(dcr.ctx, gameAddr.String(), dcr.network)
  7238  	if err != nil {
  7239  		return nil, 0, fmt.Errorf("error getting tx from dcrdata: %w", err)
  7240  	}
  7241  
  7242  	_, gameScript := gameAddr.PaymentScript()
  7243  
  7244  	feeRate, err := dcr.feeRate(2)
  7245  	if err != nil {
  7246  		return nil, 0, fmt.Errorf("error getting tx fee rate: %w", err)
  7247  	}
  7248  
  7249  	redeemTx := wire.NewMsgTx()
  7250  	var redeemable int64
  7251  	for _, gameTx := range gameTxs {
  7252  		txHash := gameTx.TxHash()
  7253  		for vout, txOut := range gameTx.TxOut {
  7254  			if bytes.Equal(txOut.PkScript, gameScript) {
  7255  				redeemable += txOut.Value
  7256  				prevOut := wire.NewOutPoint(&txHash, uint32(vout), wire.TxTreeRegular)
  7257  				redeemTx.AddTxIn(wire.NewTxIn(prevOut, txOut.Value, gameScript))
  7258  			}
  7259  		}
  7260  	}
  7261  
  7262  	if len(redeemTx.TxIn) == 0 {
  7263  		return nil, 0, fmt.Errorf("no spendable game outputs found in %d txs for address %s", len(gameTxs), gameAddr)
  7264  	}
  7265  
  7266  	var txSize uint64 = dexdcr.MsgTxOverhead + uint64(len(redeemTx.TxIn))*dexdcr.P2PKHInputSize + dexdcr.P2PKHOutputSize
  7267  	if msgLen > 0 {
  7268  		txSize += dexdcr.TxOutOverhead + 1 /* opreturn */ + uint64(wire.VarIntSerializeSize(uint64(msgLen))) + uint64(msgLen)
  7269  	}
  7270  	fees := feeRate * txSize
  7271  
  7272  	if uint64(redeemable) < fees {
  7273  		return nil, 0, fmt.Errorf("estimated fees %d are less than the redeemable value %d", fees, redeemable)
  7274  	}
  7275  	win := uint64(redeemable) - fees
  7276  	if dexdcr.IsDustVal(dexdcr.P2PKHOutputSize, win, feeRate) {
  7277  		return nil, 0, fmt.Errorf("received value is dust after fees: %d - %d = %d", redeemable, fees, win)
  7278  	}
  7279  
  7280  	redeemAddr, err := dcr.wallet.ExternalAddress(dcr.ctx, dcr.depositAccount())
  7281  	if err != nil {
  7282  		return nil, 0, fmt.Errorf("error getting redeem address: %w", err)
  7283  	}
  7284  	_, redeemScript := redeemAddr.PaymentScript()
  7285  
  7286  	redeemTx.AddTxOut(wire.NewTxOut(int64(win), redeemScript))
  7287  	if msgLen > 0 {
  7288  		msgScript, err := txscript.NewScriptBuilder().AddOp(txscript.OP_RETURN).AddData([]byte(msg)).Script()
  7289  		if err != nil {
  7290  			return nil, 0, fmt.Errorf("error building message script: %w", err)
  7291  		}
  7292  		redeemTx.AddTxOut(wire.NewTxOut(0, msgScript))
  7293  	}
  7294  
  7295  	for vin, txIn := range redeemTx.TxIn {
  7296  		redeemInSig, err := sign.RawTxInSignature(redeemTx, vin, gameScript, txscript.SigHashAll,
  7297  			gameKey, dcrec.STEcdsaSecp256k1)
  7298  		if err != nil {
  7299  			return nil, 0, fmt.Errorf("error creating signature for input script: %w", err)
  7300  		}
  7301  		txIn.SignatureScript, err = txscript.NewScriptBuilder().AddData(redeemInSig).AddData(gamePub).Script()
  7302  		if err != nil {
  7303  			return nil, 0, fmt.Errorf("error building p2pkh sig script: %w", err)
  7304  		}
  7305  	}
  7306  
  7307  	redeemHash, err := dcr.broadcastTx(redeemTx)
  7308  	if err != nil {
  7309  		return nil, 0, fmt.Errorf("error broadcasting tx: %w", err)
  7310  	}
  7311  
  7312  	return toCoinID(redeemHash, 0), win, nil
  7313  }
  7314  
  7315  func getDcrdataTxs(ctx context.Context, addr string, net dex.Network) (txs []*wire.MsgTx, _ error) {
  7316  	apiRoot := "https://dcrdata.decred.org/api/"
  7317  	switch net {
  7318  	case dex.Testnet:
  7319  		apiRoot = "https://testnet.dcrdata.org/api/"
  7320  	case dex.Simnet:
  7321  		apiRoot = "http://127.0.0.1:17779/api/"
  7322  	}
  7323  
  7324  	var resp struct {
  7325  		Txs []struct {
  7326  			TxID string `json:"txid"`
  7327  		} `json:"address_transactions"`
  7328  	}
  7329  	if err := dexnet.Get(ctx, apiRoot+"address/"+addr, &resp); err != nil {
  7330  		return nil, fmt.Errorf("error getting address info for address %q: %w", addr, err)
  7331  	}
  7332  	for _, tx := range resp.Txs {
  7333  		txID := tx.TxID
  7334  
  7335  		// tx/hex response is a hex string but is not JSON encoded.
  7336  		r, err := http.DefaultClient.Get(apiRoot + "tx/hex/" + txID)
  7337  		if err != nil {
  7338  			return nil, fmt.Errorf("error getting transaction %q: %w", txID, err)
  7339  		}
  7340  		defer r.Body.Close()
  7341  		b, err := io.ReadAll(r.Body)
  7342  		if err != nil {
  7343  			return nil, fmt.Errorf("error reading response body: %w", err)
  7344  		}
  7345  		r.Body.Close()
  7346  		hexTx := string(b)
  7347  		txB, err := hex.DecodeString(hexTx)
  7348  		if err != nil {
  7349  			return nil, fmt.Errorf("error decoding hex for tx %q: %w", txID, err)
  7350  		}
  7351  
  7352  		tx, err := msgTxFromBytes(txB)
  7353  		if err != nil {
  7354  			return nil, fmt.Errorf("error deserializing tx %x: %w", txID, err)
  7355  		}
  7356  		txs = append(txs, tx)
  7357  	}
  7358  
  7359  	return
  7360  }