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