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

     1  //go:build !harness && !vspd
     2  
     3  package dcr
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"crypto/sha256"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"math"
    14  	"math/rand"
    15  	"os"
    16  	"reflect"
    17  	"sort"
    18  	"strings"
    19  	"sync"
    20  	"sync/atomic"
    21  	"testing"
    22  	"time"
    23  
    24  	"decred.org/dcrdex/client/asset"
    25  	"decred.org/dcrdex/dex"
    26  	"decred.org/dcrdex/dex/calc"
    27  	"decred.org/dcrdex/dex/config"
    28  	"decred.org/dcrdex/dex/encode"
    29  	dexdcr "decred.org/dcrdex/dex/networks/dcr"
    30  	"decred.org/dcrwallet/v4/rpc/client/dcrwallet"
    31  	walletjson "decred.org/dcrwallet/v4/rpc/jsonrpc/types"
    32  	"github.com/decred/dcrd/chaincfg/chainhash"
    33  	"github.com/decred/dcrd/chaincfg/v3"
    34  	"github.com/decred/dcrd/dcrec"
    35  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    36  	"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
    37  	"github.com/decred/dcrd/dcrjson/v4"
    38  	"github.com/decred/dcrd/dcrutil/v4"
    39  	"github.com/decred/dcrd/gcs/v4"
    40  	"github.com/decred/dcrd/gcs/v4/blockcf2"
    41  	chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
    42  	"github.com/decred/dcrd/txscript/v4"
    43  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    44  	"github.com/decred/dcrd/wire"
    45  )
    46  
    47  var (
    48  	tLogger   dex.Logger
    49  	tCtx      context.Context
    50  	tLotSize  uint64 = 1e7
    51  	tRateStep uint64 = 100
    52  	tDCR             = &dex.Asset{
    53  		ID:         42,
    54  		Symbol:     "dcr",
    55  		Version:    version,
    56  		MaxFeeRate: 24, // FundOrder and swap/redeem fallback when estimation fails
    57  		SwapConf:   1,
    58  	}
    59  	optimalFeeRate uint64 = 22
    60  	tErr                  = fmt.Errorf("test error")
    61  	tTxID                 = "308e9a3675fc3ea3862b7863eeead08c621dcc37ff59de597dd3cdab41450ad9"
    62  	tTxHash        *chainhash.Hash
    63  	tPKHAddr       stdaddr.Address
    64  	tP2PKHScript   []byte
    65  	tChainParams          = chaincfg.MainNetParams()
    66  	feeSuggestion  uint64 = 10
    67  	tAcctName             = "tAcctName"
    68  )
    69  
    70  func randBytes(l int) []byte {
    71  	b := make([]byte, l)
    72  	rand.Read(b)
    73  	return b
    74  }
    75  
    76  func makeGetTxOutRes(confs, lots int64, pkScript []byte) *chainjson.GetTxOutResult {
    77  	val := dcrutil.Amount(lots * int64(tLotSize)).ToCoin()
    78  	return &chainjson.GetTxOutResult{
    79  		Confirmations: confs,
    80  		Value:         val,
    81  		ScriptPubKey: chainjson.ScriptPubKeyResult{
    82  			Hex: hex.EncodeToString(pkScript),
    83  		},
    84  	}
    85  }
    86  
    87  func makeRawTx(inputs []*wire.TxIn, outputScripts []dex.Bytes) *wire.MsgTx {
    88  	tx := wire.NewMsgTx()
    89  	for _, pkScript := range outputScripts {
    90  		tx.TxOut = append(tx.TxOut, &wire.TxOut{
    91  			PkScript: pkScript,
    92  		})
    93  	}
    94  	tx.TxIn = inputs
    95  	return tx
    96  }
    97  
    98  func makeTxHex(inputs []*wire.TxIn, pkScripts []dex.Bytes) (string, error) {
    99  	msgTx := wire.NewMsgTx()
   100  	msgTx.TxIn = inputs
   101  	for _, pkScript := range pkScripts {
   102  		txOut := wire.NewTxOut(100000000, pkScript)
   103  		msgTx.AddTxOut(txOut)
   104  	}
   105  	txBuf := bytes.NewBuffer(make([]byte, 0, msgTx.SerializeSize()))
   106  	err := msgTx.Serialize(txBuf)
   107  	if err != nil {
   108  		return "", err
   109  	}
   110  	return hex.EncodeToString(txBuf.Bytes()), nil
   111  }
   112  
   113  func makeRPCVin(txHash *chainhash.Hash, vout uint32, sigScript []byte) *wire.TxIn {
   114  	return &wire.TxIn{
   115  		PreviousOutPoint: *wire.NewOutPoint(txHash, vout, 0),
   116  		SignatureScript:  sigScript,
   117  	}
   118  }
   119  
   120  func newTxOutResult(script []byte, value uint64, confs int64) *chainjson.GetTxOutResult {
   121  	return &chainjson.GetTxOutResult{
   122  		Confirmations: confs,
   123  		Value:         float64(value) / 1e8,
   124  		ScriptPubKey: chainjson.ScriptPubKeyResult{
   125  			Hex: hex.EncodeToString(script),
   126  		},
   127  	}
   128  }
   129  
   130  func dummyInput() *wire.TxIn {
   131  	return wire.NewTxIn(wire.NewOutPoint(&chainhash.Hash{0x01}, 0, 0), 0, nil)
   132  }
   133  
   134  func dummyTx() *wire.MsgTx {
   135  	return makeRawTx([]*wire.TxIn{dummyInput()}, []dex.Bytes{})
   136  }
   137  
   138  // sometimes we want to not start monitor blocks to avoid a race condition in
   139  // case we replace the wallet.
   140  func tNewWalletMonitorBlocks(monitorBlocks bool) (*ExchangeWallet, *tRPCClient, func()) {
   141  	client := newTRPCClient()
   142  	log := tLogger.SubLogger("trpc")
   143  	walletCfg := &asset.WalletConfig{
   144  		Emit:        asset.NewWalletEmitter(client.emitC, BipID, log),
   145  		PeersChange: func(uint32, error) {},
   146  	}
   147  	walletCtx, shutdown := context.WithCancel(tCtx)
   148  
   149  	wallet, err := unconnectedWallet(walletCfg, &walletConfig{}, tChainParams, tLogger, dex.Simnet)
   150  	if err != nil {
   151  		shutdown()
   152  		panic(err.Error())
   153  	}
   154  	rpcw := &rpcWallet{
   155  		rpcClient: client,
   156  		log:       log,
   157  	}
   158  	rpcw.accountsV.Store(XCWalletAccounts{
   159  		PrimaryAccount: tAcctName,
   160  	})
   161  	wallet.wallet = rpcw
   162  	wallet.ctx = walletCtx
   163  
   164  	// Initialize the best block.
   165  	tip, _ := wallet.getBestBlock(walletCtx)
   166  	wallet.currentTip.Store(tip)
   167  
   168  	if monitorBlocks {
   169  		go wallet.monitorBlocks(walletCtx)
   170  	}
   171  
   172  	return wallet, client, shutdown
   173  
   174  }
   175  
   176  func tNewWallet() (*ExchangeWallet, *tRPCClient, func()) {
   177  	return tNewWalletMonitorBlocks(true)
   178  }
   179  
   180  func signFunc(msgTx *wire.MsgTx, scriptSize int) (*wire.MsgTx, bool, error) {
   181  	for i := range msgTx.TxIn {
   182  		msgTx.TxIn[i].SignatureScript = randBytes(scriptSize)
   183  	}
   184  	return msgTx, true, nil
   185  }
   186  
   187  type tRPCClient struct {
   188  	sendRawHash    *chainhash.Hash
   189  	sendRawErr     error
   190  	sentRawTx      *wire.MsgTx
   191  	txOutRes       map[outPoint]*chainjson.GetTxOutResult
   192  	txOutErr       error
   193  	bestBlockErr   error
   194  	mempoolErr     error
   195  	rawTxErr       error
   196  	unspent        []walletjson.ListUnspentResult
   197  	unspentErr     error
   198  	balanceResult  *walletjson.GetBalanceResult
   199  	balanceErr     error
   200  	lockUnspentErr error
   201  	changeAddr     stdaddr.Address
   202  	changeAddrErr  error
   203  	newAddr        stdaddr.Address
   204  	newAddrErr     error
   205  	signFunc       func(tx *wire.MsgTx) (*wire.MsgTx, bool, error)
   206  	privWIF        *dcrutil.WIF
   207  	privWIFErr     error
   208  	walletTxFn     func() (*walletjson.GetTransactionResult, error)
   209  	lockErr        error
   210  	passErr        error
   211  	disconnected   bool
   212  	rawRes         map[string]json.RawMessage
   213  	rawErr         map[string]error
   214  	blockchain     *tBlockchain
   215  	lluCoins       []walletjson.ListUnspentResult // Returned from ListLockUnspent
   216  	lockedCoins    []*wire.OutPoint               // Last submitted to LockUnspent
   217  	listLockedErr  error
   218  	estFeeErr      error
   219  	emitC          chan asset.WalletNotification
   220  	// tickets
   221  	purchasedTickets   [][]*chainhash.Hash
   222  	purchaseTicketsErr error
   223  	stakeInfo          walletjson.GetStakeInfoResult
   224  	validateAddress    map[string]*walletjson.ValidateAddressResult
   225  }
   226  
   227  type wireTxWithHeight struct {
   228  	tx     *wire.MsgTx
   229  	height int64
   230  }
   231  
   232  type tBlockchain struct {
   233  	mtx               sync.RWMutex
   234  	rawTxs            map[chainhash.Hash]*wireTxWithHeight
   235  	mainchain         map[int64]*chainhash.Hash
   236  	verboseBlocks     map[chainhash.Hash]*wire.MsgBlock
   237  	blockHeaders      map[chainhash.Hash]*wire.BlockHeader
   238  	v2CFilterBuilders map[chainhash.Hash]*tV2CFilterBuilder
   239  }
   240  
   241  func (blockchain *tBlockchain) addRawTx(blockHeight int64, tx *wire.MsgTx) (*chainhash.Hash, *wire.MsgBlock) {
   242  	blockchain.mtx.Lock()
   243  	defer blockchain.mtx.Unlock()
   244  
   245  	blockchain.rawTxs[tx.TxHash()] = &wireTxWithHeight{tx, blockHeight}
   246  
   247  	if blockHeight < 0 {
   248  		return nil, nil
   249  	}
   250  
   251  	prevBlockHash := blockchain.mainchain[blockHeight-1]
   252  
   253  	// Mined tx. Add to block.
   254  	block := blockchain.blockAt(blockHeight)
   255  	if block == nil {
   256  		block = &wire.MsgBlock{
   257  			Header: wire.BlockHeader{
   258  				PrevBlock: *prevBlockHash,
   259  				Height:    uint32(blockHeight),
   260  				VoteBits:  1,
   261  				Timestamp: time.Now(),
   262  			},
   263  		}
   264  	}
   265  	block.Transactions = append(block.Transactions, tx)
   266  	blockHash := block.BlockHash()
   267  	blockFilterBuilder := blockchain.v2CFilterBuilders[blockHash]
   268  	blockchain.mainchain[blockHeight] = &blockHash
   269  	blockchain.verboseBlocks[blockHash] = block
   270  
   271  	blockchain.blockHeaders[blockHash] = &block.Header
   272  
   273  	// Save prevout and output scripts in block cfilters.
   274  	if blockFilterBuilder == nil {
   275  		blockFilterBuilder = &tV2CFilterBuilder{}
   276  		copy(blockFilterBuilder.key[:], randBytes(16))
   277  
   278  	}
   279  	blockchain.v2CFilterBuilders[blockHash] = blockFilterBuilder
   280  	for _, txIn := range tx.TxIn {
   281  		prevOut := &txIn.PreviousOutPoint
   282  		prevTx, found := blockchain.rawTxs[prevOut.Hash]
   283  		if !found || len(prevTx.tx.TxOut) <= int(prevOut.Index) {
   284  			continue
   285  		}
   286  		blockFilterBuilder.data.AddRegularPkScript(prevTx.tx.TxOut[prevOut.Index].PkScript)
   287  	}
   288  	for _, txOut := range tx.TxOut {
   289  		blockFilterBuilder.data.AddRegularPkScript(txOut.PkScript)
   290  	}
   291  
   292  	return &blockHash, block
   293  }
   294  
   295  // blockchain.mtx lock should be held for writes.
   296  func (blockchain *tBlockchain) blockAt(height int64) *wire.MsgBlock {
   297  	blkHash, found := blockchain.mainchain[height]
   298  	if found {
   299  		return blockchain.verboseBlocks[*blkHash]
   300  	}
   301  
   302  	return nil
   303  }
   304  
   305  type tV2CFilterBuilder struct {
   306  	data blockcf2.Entries
   307  	key  [gcs.KeySize]byte
   308  }
   309  
   310  func (filterBuilder *tV2CFilterBuilder) build() (*gcs.FilterV2, error) {
   311  	return gcs.NewFilterV2(blockcf2.B, blockcf2.M, filterBuilder.key, filterBuilder.data)
   312  }
   313  
   314  func defaultSignFunc(tx *wire.MsgTx) (*wire.MsgTx, bool, error) { return tx, true, nil }
   315  
   316  func newTRPCClient() *tRPCClient {
   317  	// setup genesis block, required by bestblock polling goroutine
   318  	var newHash chainhash.Hash
   319  	copy(newHash[:], randBytes(32))
   320  	return &tRPCClient{
   321  		txOutRes: make(map[outPoint]*chainjson.GetTxOutResult),
   322  		blockchain: &tBlockchain{
   323  			rawTxs: map[chainhash.Hash]*wireTxWithHeight{},
   324  			mainchain: map[int64]*chainhash.Hash{
   325  				0: &newHash,
   326  			},
   327  			verboseBlocks: map[chainhash.Hash]*wire.MsgBlock{
   328  				newHash: {},
   329  			},
   330  			blockHeaders: map[chainhash.Hash]*wire.BlockHeader{
   331  				newHash: {},
   332  			},
   333  			v2CFilterBuilders: map[chainhash.Hash]*tV2CFilterBuilder{},
   334  		},
   335  		signFunc: defaultSignFunc,
   336  		rawRes:   make(map[string]json.RawMessage),
   337  		rawErr:   make(map[string]error),
   338  		emitC:    make(chan asset.WalletNotification, 128),
   339  	}
   340  }
   341  
   342  func (c *tRPCClient) GetCurrentNet(context.Context) (wire.CurrencyNet, error) {
   343  	return tChainParams.Net, nil
   344  }
   345  
   346  func (c *tRPCClient) EstimateSmartFee(_ context.Context, confirmations int64, mode chainjson.EstimateSmartFeeMode) (*chainjson.EstimateSmartFeeResult, error) {
   347  	if c.estFeeErr != nil {
   348  		return nil, c.estFeeErr
   349  	}
   350  	optimalRate := float64(optimalFeeRate) * 1e-5 // optimalFeeRate: 22 atoms/byte = 0.00022 DCR/KB * 1e8 atoms/DCR * 1e-3 KB/Byte
   351  	// fmt.Println((float64(optimalFeeRate)*1e-5)-0.00022)
   352  	return &chainjson.EstimateSmartFeeResult{FeeRate: optimalRate}, nil
   353  }
   354  
   355  func (c *tRPCClient) SendRawTransaction(_ context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) {
   356  	c.sentRawTx = tx
   357  	if c.sendRawErr == nil && c.sendRawHash == nil {
   358  		h := tx.TxHash()
   359  		return &h, nil
   360  	}
   361  	return c.sendRawHash, c.sendRawErr
   362  }
   363  
   364  func (c *tRPCClient) GetTxOut(_ context.Context, txHash *chainhash.Hash, vout uint32, tree int8, mempool bool) (*chainjson.GetTxOutResult, error) {
   365  	return c.txOutRes[newOutPoint(txHash, vout)], c.txOutErr
   366  }
   367  
   368  func (c *tRPCClient) GetBestBlock(_ context.Context) (*chainhash.Hash, int64, error) {
   369  	if c.bestBlockErr != nil {
   370  		return nil, -1, c.bestBlockErr
   371  	}
   372  	bestHash, bestBlkHeight := c.getBestBlock()
   373  	return bestHash, bestBlkHeight, nil
   374  }
   375  
   376  func (c *tRPCClient) getBestBlock() (*chainhash.Hash, int64) {
   377  	c.blockchain.mtx.RLock()
   378  	defer c.blockchain.mtx.RUnlock()
   379  	var bestHash *chainhash.Hash
   380  	var bestBlkHeight int64
   381  	for height, hash := range c.blockchain.mainchain {
   382  		if height >= bestBlkHeight {
   383  			bestBlkHeight = height
   384  			bestHash = hash
   385  		}
   386  	}
   387  	return bestHash, bestBlkHeight
   388  }
   389  
   390  func (c *tRPCClient) GetBlockHash(_ context.Context, blockHeight int64) (*chainhash.Hash, error) {
   391  	c.blockchain.mtx.RLock()
   392  	defer c.blockchain.mtx.RUnlock()
   393  	h, found := c.blockchain.mainchain[blockHeight]
   394  	if !found {
   395  		return nil, fmt.Errorf("no test block at height %d", blockHeight)
   396  	}
   397  	return h, nil
   398  }
   399  
   400  func (c *tRPCClient) GetBlock(_ context.Context, blockHash *chainhash.Hash) (*wire.MsgBlock, error) {
   401  	c.blockchain.mtx.RLock()
   402  	defer c.blockchain.mtx.RUnlock()
   403  	blk, found := c.blockchain.verboseBlocks[*blockHash]
   404  	if !found {
   405  		return nil, fmt.Errorf("no test block found for %s", blockHash)
   406  	}
   407  	return blk, nil
   408  }
   409  
   410  func (c *tRPCClient) GetBlockHeaderVerbose(_ context.Context, blockHash *chainhash.Hash) (*chainjson.GetBlockHeaderVerboseResult, error) {
   411  	c.blockchain.mtx.RLock()
   412  	defer c.blockchain.mtx.RUnlock()
   413  	hdr, found := c.blockchain.blockHeaders[*blockHash]
   414  	if !found {
   415  		return nil, fmt.Errorf("no test block header found for %s", blockHash)
   416  	}
   417  	return &chainjson.GetBlockHeaderVerboseResult{
   418  		Height:        hdr.Height,
   419  		Hash:          blockHash.String(),
   420  		PreviousHash:  hdr.PrevBlock.String(),
   421  		Confirmations: 1,  // just not -1, which indicates side chain
   422  		NextHash:      "", // empty string signals that it is tip
   423  	}, nil
   424  }
   425  
   426  func (c *tRPCClient) GetBlockHeader(_ context.Context, blockHash *chainhash.Hash) (*wire.BlockHeader, error) {
   427  	c.blockchain.mtx.RLock()
   428  	defer c.blockchain.mtx.RUnlock()
   429  	hdr, found := c.blockchain.blockHeaders[*blockHash]
   430  	if !found {
   431  		return nil, fmt.Errorf("no test block header found for %s", blockHash)
   432  	}
   433  	return hdr, nil
   434  }
   435  
   436  func (c *tRPCClient) GetRawMempool(_ context.Context, txType chainjson.GetRawMempoolTxTypeCmd) ([]*chainhash.Hash, error) {
   437  	if c.mempoolErr != nil {
   438  		return nil, c.mempoolErr
   439  	}
   440  	c.blockchain.mtx.RLock()
   441  	defer c.blockchain.mtx.RUnlock()
   442  	txHashes := make([]*chainhash.Hash, 0)
   443  	for _, tx := range c.blockchain.rawTxs {
   444  		if tx.height < 0 {
   445  			txHash := tx.tx.TxHash()
   446  			txHashes = append(txHashes, &txHash)
   447  		}
   448  	}
   449  	return txHashes, nil
   450  }
   451  
   452  func (c *tRPCClient) GetBalanceMinConf(_ context.Context, account string, minConfirms int) (*walletjson.GetBalanceResult, error) {
   453  	return c.balanceResult, c.balanceErr
   454  }
   455  
   456  func (c *tRPCClient) LockUnspent(_ context.Context, unlock bool, ops []*wire.OutPoint) error {
   457  	if unlock == false {
   458  		if c.lockedCoins == nil {
   459  			c.lockedCoins = ops
   460  		} else {
   461  			c.lockedCoins = append(c.lockedCoins, ops...)
   462  		}
   463  	}
   464  	return c.lockUnspentErr
   465  }
   466  
   467  func (c *tRPCClient) GetRawChangeAddress(_ context.Context, account string, net stdaddr.AddressParams) (stdaddr.Address, error) {
   468  	return c.changeAddr, c.changeAddrErr
   469  }
   470  
   471  func (c *tRPCClient) GetNewAddressGapPolicy(_ context.Context, account string, gapPolicy dcrwallet.GapPolicy) (stdaddr.Address, error) {
   472  	return c.newAddr, c.newAddrErr
   473  }
   474  
   475  func (c *tRPCClient) DumpPrivKey(_ context.Context, address stdaddr.Address) (*dcrutil.WIF, error) {
   476  	return c.privWIF, c.privWIFErr
   477  }
   478  
   479  func (c *tRPCClient) GetTransaction(_ context.Context, txHash *chainhash.Hash) (*walletjson.GetTransactionResult, error) {
   480  	if c.walletTxFn != nil {
   481  		return c.walletTxFn()
   482  	}
   483  	c.blockchain.mtx.RLock()
   484  	defer c.blockchain.mtx.RUnlock()
   485  	if rawTx, has := c.blockchain.rawTxs[*txHash]; has {
   486  		b, err := rawTx.tx.Bytes()
   487  		if err != nil {
   488  			return nil, err
   489  		}
   490  		walletTx := &walletjson.GetTransactionResult{
   491  			Hex: hex.EncodeToString(b),
   492  		}
   493  		return walletTx, nil
   494  	}
   495  	return nil, dcrjson.NewRPCError(dcrjson.ErrRPCNoTxInfo, "no test transaction")
   496  }
   497  
   498  func (c *tRPCClient) AccountUnlocked(_ context.Context, acct string) (*walletjson.AccountUnlockedResult, error) {
   499  	return &walletjson.AccountUnlockedResult{}, nil // go the walletlock/walletpassphrase route
   500  }
   501  
   502  func (c *tRPCClient) LockAccount(_ context.Context, acct string) error       { return nil }
   503  func (c *tRPCClient) UnlockAccount(_ context.Context, acct, pw string) error { return nil }
   504  
   505  func (c *tRPCClient) WalletLock(_ context.Context) error {
   506  	return c.lockErr
   507  }
   508  
   509  func (c *tRPCClient) WalletPassphrase(_ context.Context, passphrase string, timeoutSecs int64) error {
   510  	return c.passErr
   511  }
   512  
   513  func (c *tRPCClient) WalletInfo(_ context.Context) (*walletjson.WalletInfoResult, error) {
   514  	return &walletjson.WalletInfoResult{
   515  		Unlocked: true,
   516  	}, nil
   517  }
   518  
   519  func (c *tRPCClient) ValidateAddress(_ context.Context, address stdaddr.Address) (*walletjson.ValidateAddressWalletResult, error) {
   520  	if c.validateAddress != nil {
   521  		if c.validateAddress[address.String()] != nil {
   522  			return c.validateAddress[address.String()], nil
   523  		}
   524  		return &walletjson.ValidateAddressWalletResult{}, nil
   525  	}
   526  
   527  	return &walletjson.ValidateAddressWalletResult{
   528  		IsMine:  true,
   529  		Account: tAcctName,
   530  	}, nil
   531  }
   532  
   533  func (c *tRPCClient) Disconnected() bool {
   534  	return c.disconnected
   535  }
   536  
   537  func (c *tRPCClient) GetStakeInfo(ctx context.Context) (*walletjson.GetStakeInfoResult, error) {
   538  	return &c.stakeInfo, nil
   539  }
   540  
   541  func (c *tRPCClient) PurchaseTicket(ctx context.Context, fromAccount string, spendLimit dcrutil.Amount, minConf *int,
   542  	ticketAddress stdaddr.Address, numTickets *int, poolAddress stdaddr.Address, poolFees *dcrutil.Amount,
   543  	expiry *int, ticketChange *bool, ticketFee *dcrutil.Amount) (tix []*chainhash.Hash, _ error) {
   544  
   545  	if c.purchaseTicketsErr != nil {
   546  		return nil, c.purchaseTicketsErr
   547  	}
   548  
   549  	if len(c.purchasedTickets) > 0 {
   550  		tix = c.purchasedTickets[0]
   551  		c.purchasedTickets = c.purchasedTickets[1:]
   552  	}
   553  
   554  	return tix, nil
   555  }
   556  
   557  func (c *tRPCClient) GetTickets(ctx context.Context, includeImmature bool) ([]*chainhash.Hash, error) {
   558  	return nil, nil
   559  }
   560  
   561  func (c *tRPCClient) GetVoteChoices(ctx context.Context) (*walletjson.GetVoteChoicesResult, error) {
   562  	return nil, nil
   563  }
   564  
   565  func (c *tRPCClient) SetVoteChoice(ctx context.Context, agendaID, choiceID string) error {
   566  	return nil
   567  }
   568  
   569  func (c *tRPCClient) RawRequest(_ context.Context, method string, params []json.RawMessage) (json.RawMessage, error) {
   570  	if rr, found := c.rawRes[method]; found {
   571  		return rr, c.rawErr[method] // err probably should be nil, but respect the config
   572  	}
   573  	if re, found := c.rawErr[method]; found {
   574  		return nil, re
   575  	}
   576  
   577  	switch method {
   578  	case methodGetPeerInfo:
   579  		return json.Marshal([]*walletjson.GetPeerInfoResult{
   580  			{
   581  				Addr: "127.0.0.1",
   582  			},
   583  		})
   584  	case methodGetCFilterV2:
   585  		if len(params) != 1 {
   586  			return nil, fmt.Errorf("getcfilterv2 requires 1 param, got %d", len(params))
   587  		}
   588  
   589  		var hashStr string
   590  		json.Unmarshal(params[0], &hashStr)
   591  		blkHash, _ := chainhash.NewHashFromStr(hashStr)
   592  
   593  		c.blockchain.mtx.RLock()
   594  		defer c.blockchain.mtx.RUnlock()
   595  		blockFilterBuilder := c.blockchain.v2CFilterBuilders[*blkHash]
   596  		if blockFilterBuilder == nil {
   597  			return nil, fmt.Errorf("cfilters builder not found for block %s", blkHash)
   598  		}
   599  		v2CFilter, err := blockFilterBuilder.build()
   600  		if err != nil {
   601  			return nil, err
   602  		}
   603  		res := &walletjson.GetCFilterV2Result{
   604  			BlockHash: blkHash.String(),
   605  			Filter:    hex.EncodeToString(v2CFilter.Bytes()),
   606  			Key:       hex.EncodeToString(blockFilterBuilder.key[:]),
   607  		}
   608  		return json.Marshal(res)
   609  
   610  	case methodListUnspent:
   611  		if c.unspentErr != nil {
   612  			return nil, c.unspentErr
   613  		}
   614  
   615  		var acct string
   616  		if len(params) > 3 {
   617  			// filter with provided acct param
   618  			_ = json.Unmarshal(params[3], &acct)
   619  		}
   620  		allAccts := acct == "" || acct == "*"
   621  
   622  		var unspents []walletjson.ListUnspentResult
   623  		for _, unspent := range c.unspent {
   624  			if allAccts || unspent.Account == acct {
   625  				unspents = append(unspents, unspent)
   626  			}
   627  		}
   628  
   629  		response, _ := json.Marshal(unspents)
   630  		return response, nil
   631  
   632  	case methodListLockUnspent:
   633  		if c.listLockedErr != nil {
   634  			return nil, c.listLockedErr
   635  		}
   636  
   637  		var acct string
   638  		if len(params) > 0 {
   639  			_ = json.Unmarshal(params[0], &acct)
   640  		}
   641  		allAccts := acct == "" || acct == "*"
   642  
   643  		var locked []chainjson.TransactionInput
   644  		for _, utxo := range c.lluCoins {
   645  			if allAccts || utxo.Account == acct {
   646  				locked = append(locked, chainjson.TransactionInput{
   647  					Txid:   utxo.TxID,
   648  					Amount: utxo.Amount,
   649  					Vout:   utxo.Vout,
   650  					Tree:   utxo.Tree,
   651  				})
   652  			}
   653  		}
   654  		response, _ := json.Marshal(locked)
   655  		return response, nil
   656  
   657  	case methodSignRawTransaction:
   658  		if len(params) != 1 {
   659  			return nil, fmt.Errorf("needed 1 param")
   660  		}
   661  
   662  		var msgTxHex string
   663  		err := json.Unmarshal(params[0], &msgTxHex)
   664  		if err != nil {
   665  			return nil, err
   666  		}
   667  
   668  		msgTx, err := msgTxFromHex(msgTxHex)
   669  		if err != nil {
   670  			res := walletjson.SignRawTransactionResult{
   671  				Hex: msgTxHex,
   672  				Errors: []walletjson.SignRawTransactionError{
   673  					{
   674  						TxID:  msgTx.CachedTxHash().String(),
   675  						Error: err.Error(),
   676  					},
   677  				},
   678  				// Complete stays false.
   679  			}
   680  			return json.Marshal(&res)
   681  		}
   682  
   683  		if c.signFunc == nil {
   684  			return nil, fmt.Errorf("no signFunc configured")
   685  		}
   686  
   687  		signedTx, complete, err := c.signFunc(msgTx)
   688  		if err != nil {
   689  			res := walletjson.SignRawTransactionResult{
   690  				Hex: msgTxHex,
   691  				Errors: []walletjson.SignRawTransactionError{
   692  					{
   693  						TxID:  msgTx.CachedTxHash().String(),
   694  						Error: err.Error(),
   695  					},
   696  				},
   697  				// Complete stays false.
   698  			}
   699  			return json.Marshal(&res)
   700  		}
   701  
   702  		txHex, err := msgTxToHex(signedTx)
   703  		if err != nil {
   704  			return nil, fmt.Errorf("failed to encode MsgTx: %w", err)
   705  		}
   706  
   707  		res := walletjson.SignRawTransactionResult{
   708  			Hex:      txHex,
   709  			Complete: complete,
   710  		}
   711  		return json.Marshal(&res)
   712  
   713  	case methodWalletInfo:
   714  		return json.Marshal(new(walletjson.WalletInfoResult))
   715  	}
   716  
   717  	return nil, fmt.Errorf("method %v not implemented by (*tRPCClient).RawRequest", method)
   718  }
   719  
   720  func (c *tRPCClient) SetTxFee(ctx context.Context, fee dcrutil.Amount) error {
   721  	return nil
   722  }
   723  
   724  func (c *tRPCClient) ListSinceBlock(ctx context.Context, hash *chainhash.Hash) (*walletjson.ListSinceBlockResult, error) {
   725  	return nil, nil
   726  }
   727  
   728  func TestMain(m *testing.M) {
   729  	tChainParams = chaincfg.MainNetParams()
   730  	tPKHAddr, _ = stdaddr.DecodeAddress("DsTya4cCFBgtofDLiRhkyPYEQjgs3HnarVP", tChainParams)
   731  	tLogger = dex.StdOutLogger("TEST", dex.LevelTrace)
   732  	var shutdown func()
   733  	tCtx, shutdown = context.WithCancel(context.Background())
   734  	tTxHash, _ = chainhash.NewHashFromStr(tTxID)
   735  	tP2PKHScript, _ = hex.DecodeString("76a9148fc02268f208a61767504fe0b48d228641ba81e388ac")
   736  	// tP2SH, _ = hex.DecodeString("76a91412a9abf5c32392f38bd8a1f57d81b1aeecc5699588ac")
   737  	doIt := func() int {
   738  		// Not counted as coverage, must test Archiver constructor explicitly.
   739  		defer shutdown()
   740  		return m.Run()
   741  	}
   742  	os.Exit(doIt())
   743  }
   744  
   745  func TestMaxFundingFees(t *testing.T) {
   746  	wallet, _, shutdown := tNewWallet()
   747  	defer shutdown()
   748  
   749  	maxFeeRate := uint64(100)
   750  
   751  	useSplitOptions := map[string]string{
   752  		multiSplitKey: "true",
   753  	}
   754  	noSplitOptions := map[string]string{
   755  		multiSplitKey: "false",
   756  	}
   757  
   758  	maxFundingFees := wallet.MaxFundingFees(3, maxFeeRate, useSplitOptions)
   759  	expectedFees := maxFeeRate * (dexdcr.P2PKHInputSize*12 + dexdcr.P2PKHOutputSize*4 + dexdcr.MsgTxOverhead)
   760  	if maxFundingFees != expectedFees {
   761  		t.Fatalf("unexpected max funding fees. expected %d, got %d", expectedFees, maxFundingFees)
   762  	}
   763  
   764  	maxFundingFees = wallet.MaxFundingFees(3, maxFeeRate, noSplitOptions)
   765  	if maxFundingFees != 0 {
   766  		t.Fatalf("unexpected max funding fees. expected 0, got %d", maxFundingFees)
   767  	}
   768  }
   769  
   770  func TestAvailableFund(t *testing.T) {
   771  	wallet, node, shutdown := tNewWallet()
   772  	defer shutdown()
   773  
   774  	// With an empty list returned, there should be no error, but the value zero
   775  	// should be returned.
   776  	unspents := make([]walletjson.ListUnspentResult, 0)
   777  	node.unspent = unspents
   778  	balanceResult := &walletjson.GetBalanceResult{
   779  		Balances: []walletjson.GetAccountBalanceResult{
   780  			{
   781  				AccountName: tAcctName,
   782  			},
   783  		},
   784  	}
   785  	node.balanceResult = balanceResult
   786  	bal, err := wallet.Balance()
   787  	if err != nil {
   788  		t.Fatalf("error for zero utxos: %v", err)
   789  	}
   790  	if bal.Available != 0 {
   791  		t.Fatalf("expected available = 0, got %d", bal.Available)
   792  	}
   793  	if bal.Immature != 0 {
   794  		t.Fatalf("expected unconf = 0, got %d", bal.Immature)
   795  	}
   796  
   797  	var vout uint32
   798  	addUtxo := func(atomAmt uint64, confs int64, lock bool) {
   799  		utxo := walletjson.ListUnspentResult{
   800  			TxID:          tTxID,
   801  			Vout:          vout,
   802  			Address:       tPKHAddr.String(),
   803  			Account:       tAcctName,
   804  			Amount:        float64(atomAmt) / 1e8,
   805  			Confirmations: confs,
   806  			ScriptPubKey:  hex.EncodeToString(tP2PKHScript),
   807  			Spendable:     true,
   808  		}
   809  		if lock {
   810  			node.lluCoins = append(node.lluCoins, utxo)
   811  		} else {
   812  			unspents = append(unspents, utxo)
   813  			node.unspent = unspents
   814  		}
   815  		// update balance
   816  		balanceResult.Balances[0].Spendable += utxo.Amount
   817  		vout++
   818  	}
   819  
   820  	// Add 1 unspent output and check balance
   821  	var littleLots uint64 = 6
   822  	littleOrder := tLotSize * littleLots
   823  	littleFunds := calc.RequiredOrderFunds(littleOrder, dexdcr.P2PKHInputSize, littleLots, dexdcr.InitTxSizeBase, dexdcr.InitTxSize, tDCR.MaxFeeRate)
   824  	addUtxo(littleFunds, 0, false)
   825  	bal, err = wallet.Balance()
   826  	if err != nil {
   827  		t.Fatalf("error for 1 utxo: %v", err)
   828  	}
   829  	if bal.Available != littleFunds {
   830  		t.Fatalf("expected available = %d for confirmed utxos, got %d", littleFunds, bal.Available)
   831  	}
   832  	if bal.Immature != 0 {
   833  		t.Fatalf("expected immature = %d, got %d", 0, bal.Immature)
   834  	}
   835  	if bal.Locked != 0 {
   836  		t.Fatalf("expected locked = %d, got %d", 0, bal.Locked)
   837  	}
   838  
   839  	// Add a second utxo, lock it and check balance.
   840  	lockedBit := tLotSize * 2
   841  	addUtxo(lockedBit, 1, true)
   842  	bal, err = wallet.Balance()
   843  	if err != nil {
   844  		t.Fatalf("error for 2 utxos: %v", err)
   845  	}
   846  	// Available balance should exclude locked utxo amount.
   847  	if bal.Available != littleFunds {
   848  		t.Fatalf("expected available = %d for confirmed utxos, got %d", littleFunds, bal.Available)
   849  	}
   850  	if bal.Immature != 0 {
   851  		t.Fatalf("expected immature = %d, got %d", 0, bal.Immature)
   852  	}
   853  	if bal.Locked != lockedBit {
   854  		t.Fatalf("expected locked = %d, got %d", lockedBit, bal.Locked)
   855  	}
   856  
   857  	// Add a third utxo.
   858  	var lottaLots uint64 = 100
   859  	lottaOrder := tLotSize * 100
   860  	// Add funding for an extra input to accommodate the later combined tests.
   861  	lottaFunds := calc.RequiredOrderFunds(lottaOrder, 2*dexdcr.P2PKHInputSize, lottaLots, dexdcr.InitTxSizeBase, dexdcr.InitTxSize, tDCR.MaxFeeRate)
   862  	addUtxo(lottaFunds, 1, false)
   863  	bal, err = wallet.Balance()
   864  	if err != nil {
   865  		t.Fatalf("error for 3 utxos: %v", err)
   866  	}
   867  	if bal.Available != littleFunds+lottaFunds {
   868  		t.Fatalf("expected available = %d for 2 outputs, got %d", littleFunds+lottaFunds, bal.Available)
   869  	}
   870  	if bal.Immature != 0 {
   871  		t.Fatalf("expected unconf = 0 for 2 outputs, got %d", bal.Immature)
   872  	}
   873  	// locked balance should remain same as utxo2 amount.
   874  	if bal.Locked != lockedBit {
   875  		t.Fatalf("expected locked = %d, got %d", lockedBit, bal.Locked)
   876  	}
   877  
   878  	ord := &asset.Order{
   879  		Version:       version,
   880  		Value:         0,
   881  		MaxSwapCount:  1,
   882  		MaxFeeRate:    tDCR.MaxFeeRate,
   883  		FeeSuggestion: feeSuggestion,
   884  	}
   885  
   886  	setOrderValue := func(v uint64) {
   887  		ord.Value = v
   888  		ord.MaxSwapCount = v / tLotSize
   889  	}
   890  
   891  	// Zero value
   892  	_, _, _, err = wallet.FundOrder(ord)
   893  	if err == nil {
   894  		t.Fatalf("no funding error for zero value")
   895  	}
   896  
   897  	// Nothing to spend
   898  	node.unspent = nil
   899  	setOrderValue(littleOrder)
   900  	_, _, _, err = wallet.FundOrder(ord)
   901  	if err == nil {
   902  		t.Fatalf("no error for zero utxos")
   903  	}
   904  	node.unspent = unspents
   905  
   906  	// RPC error
   907  	node.unspentErr = tErr
   908  	_, _, _, err = wallet.FundOrder(ord)
   909  	if err == nil {
   910  		t.Fatalf("no funding error for rpc error")
   911  	}
   912  	node.unspentErr = nil
   913  
   914  	// Negative response when locking outputs.
   915  	node.lockUnspentErr = tErr
   916  	_, _, _, err = wallet.FundOrder(ord)
   917  	if err == nil {
   918  		t.Fatalf("no error for lockunspent result = false: %v", err)
   919  	}
   920  	node.lockUnspentErr = nil
   921  
   922  	// Fund a little bit, but small output is unconfirmed.
   923  	spendables, _, _, err := wallet.FundOrder(ord)
   924  	if err != nil {
   925  		t.Fatalf("error funding small amount: %v", err)
   926  	}
   927  	if len(spendables) != 1 {
   928  		t.Fatalf("expected 1 spendable, got %d", len(spendables))
   929  	}
   930  	v := spendables[0].Value()
   931  	if v != lottaFunds {
   932  		t.Fatalf("expected spendable of value %d, got %d", lottaFunds, v)
   933  	}
   934  
   935  	// Now confirm the little bit and have it selected.
   936  	unspents[0].Confirmations++
   937  	spendables, _, fees, err := wallet.FundOrder(ord)
   938  	if err != nil {
   939  		t.Fatalf("error funding small amount: %v", err)
   940  	}
   941  	if len(spendables) != 1 {
   942  		t.Fatalf("expected 1 spendable, got %d", len(spendables))
   943  	}
   944  	if fees != 0 {
   945  		t.Fatalf("expected zero fees, got %d", fees)
   946  	}
   947  	v = spendables[0].Value()
   948  	if v != littleFunds {
   949  		t.Fatalf("expected spendable of value %d, got %d", littleFunds, v)
   950  	}
   951  
   952  	// Fund a lotta bit.
   953  	setOrderValue(lottaOrder)
   954  	spendables, _, _, err = wallet.FundOrder(ord)
   955  	if err != nil {
   956  		t.Fatalf("error funding large amount: %v", err)
   957  	}
   958  	if len(spendables) != 1 {
   959  		t.Fatalf("expected 1 spendable, got %d", len(spendables))
   960  	}
   961  	if fees != 0 {
   962  		t.Fatalf("expected zero fees, got %d", fees)
   963  	}
   964  	v = spendables[0].Value()
   965  	if v != lottaFunds {
   966  		t.Fatalf("expected spendable of value %d, got %d", lottaFunds, v)
   967  	}
   968  
   969  	extraLottaOrder := littleOrder + lottaOrder
   970  	extraLottaLots := littleLots + lottaLots
   971  	// Prepare for a split transaction.
   972  	baggageFees := tDCR.MaxFeeRate * splitTxBaggage
   973  	node.newAddr = tPKHAddr
   974  	node.changeAddr = tPKHAddr
   975  	wallet.config().useSplitTx = true
   976  	// No split performed due to economics is not an error.
   977  	setOrderValue(extraLottaOrder)
   978  	coins, _, _, err := wallet.FundOrder(ord)
   979  	if err != nil {
   980  		t.Fatalf("error for no-split split: %v", err)
   981  	}
   982  	if fees != 0 {
   983  		t.Fatalf("expected zero fees, got %d", fees)
   984  	}
   985  	// Should be both coins.
   986  	if len(coins) != 2 {
   987  		t.Fatalf("no-split split didn't return both coins")
   988  	}
   989  
   990  	// Not enough to cover transaction fees.
   991  	tweak := float64(littleFunds+lottaFunds-calc.RequiredOrderFunds(extraLottaOrder, 2*dexdcr.P2PKHInputSize, extraLottaLots, dexdcr.InitTxSizeBase, dexdcr.InitTxSize, tDCR.MaxFeeRate)+1) / 1e8
   992  	node.unspent[0].Amount -= tweak
   993  	setOrderValue(extraLottaOrder)
   994  	_, _, _, err = wallet.FundOrder(ord)
   995  	if err == nil {
   996  		t.Fatalf("no error when not enough to cover tx fees")
   997  	}
   998  	if fees != 0 {
   999  		t.Fatalf("expected zero fees, got %d", fees)
  1000  	}
  1001  
  1002  	node.unspent[0].Amount += tweak
  1003  
  1004  	// No split because not standing order.
  1005  	ord.Immediate = true
  1006  	coins, _, _, err = wallet.FundOrder(ord)
  1007  	if err != nil {
  1008  		t.Fatalf("error for no-split split: %v", err)
  1009  	}
  1010  	if fees != 0 {
  1011  		t.Fatalf("expected zero fees, got %d", fees)
  1012  	}
  1013  	ord.Immediate = false
  1014  	if len(coins) != 2 {
  1015  		t.Fatalf("no-split split didn't return both coins")
  1016  	}
  1017  
  1018  	// With a little more locked, the split should be performed.
  1019  	node.unspent[1].Amount += float64(baggageFees) / 1e8
  1020  	coins, _, fees, err = wallet.FundOrder(ord)
  1021  	if err != nil {
  1022  		t.Fatalf("error for split tx: %v", err)
  1023  	}
  1024  
  1025  	inputSize := dexdcr.P2PKHInputSize - dexdcr.P2PKHSigScriptSize // no sig script
  1026  	splitTxSize := dexdcr.MsgTxOverhead + (2 * inputSize) + 2*dexdcr.P2PKHOutputSize
  1027  	expectedFees := uint64(splitTxSize) * feeSuggestion
  1028  	if fees != expectedFees {
  1029  		t.Fatalf("expected fees of %d, got %d", expectedFees, fees)
  1030  	}
  1031  
  1032  	// Should be just one coin.
  1033  	if len(coins) != 1 {
  1034  		t.Fatalf("split failed - coin count != 1")
  1035  	}
  1036  	if node.sentRawTx == nil {
  1037  		t.Fatalf("split failed - no tx sent")
  1038  	}
  1039  
  1040  	// Hit some error paths.
  1041  
  1042  	// GetNewAddressGapPolicy error
  1043  	node.newAddrErr = tErr
  1044  	_, _, _, err = wallet.FundOrder(ord)
  1045  	if err == nil {
  1046  		t.Fatalf("no error for split tx change addr error")
  1047  	}
  1048  	node.newAddrErr = nil
  1049  
  1050  	// GetRawChangeAddress error
  1051  	node.changeAddrErr = tErr
  1052  	_, _, _, err = wallet.FundOrder(ord)
  1053  	if err == nil {
  1054  		t.Fatalf("no error for split tx change addr error")
  1055  	}
  1056  	node.changeAddrErr = nil
  1057  
  1058  	// SendRawTx error
  1059  	node.sendRawErr = tErr
  1060  	_, _, _, err = wallet.FundOrder(ord)
  1061  	if err == nil {
  1062  		t.Fatalf("no error for split tx send error")
  1063  	}
  1064  	node.sendRawErr = nil
  1065  
  1066  	// Success again.
  1067  	_, _, _, err = wallet.FundOrder(ord)
  1068  	if err != nil {
  1069  		t.Fatalf("error for split tx recovery run")
  1070  	}
  1071  
  1072  	// Not enough funds, because littleUnspent is a different account.
  1073  	unspents[0].Account = "wrong account"
  1074  	setOrderValue(extraLottaOrder)
  1075  	_, _, _, err = wallet.FundOrder(ord)
  1076  	if err == nil {
  1077  		t.Fatalf("no error for wrong account")
  1078  	}
  1079  	node.unspent[0].Account = tAcctName
  1080  
  1081  	// Place locked utxo in different account, check locked balance.
  1082  	node.lluCoins[0].Account = "wrong account"
  1083  	bal, err = wallet.Balance()
  1084  	if err != nil {
  1085  		t.Fatalf("error for 3 utxos, with locked utxo in wrong acct: %v", err)
  1086  	}
  1087  	if bal.Locked != 0 {
  1088  		t.Fatalf("expected locked = %d, got %d", 0, bal.Locked)
  1089  	}
  1090  }
  1091  
  1092  // Since ReturnCoins takes the wallet.Coin interface, make sure any interface
  1093  // is acceptable.
  1094  type tCoin struct{ id []byte }
  1095  
  1096  func (c *tCoin) ID() dex.Bytes {
  1097  	if len(c.id) > 0 {
  1098  		return c.id
  1099  	}
  1100  	return make([]byte, 36)
  1101  }
  1102  func (c *tCoin) String() string                                    { return hex.EncodeToString(c.id) }
  1103  func (c *tCoin) Value() uint64                                     { return 100 }
  1104  func (c *tCoin) Confirmations(ctx context.Context) (uint32, error) { return 2, nil }
  1105  func (c *tCoin) TxID() string                                      { return hex.EncodeToString(c.id) }
  1106  
  1107  func TestReturnCoins(t *testing.T) {
  1108  	wallet, node, shutdown := tNewWallet()
  1109  	defer shutdown()
  1110  
  1111  	// Test it with the local output type.
  1112  	coins := asset.Coins{
  1113  		newOutput(tTxHash, 0, 1, wire.TxTreeRegular),
  1114  	}
  1115  	err := wallet.ReturnCoins(coins)
  1116  	if err != nil {
  1117  		t.Fatalf("error with output type coins: %v", err)
  1118  	}
  1119  
  1120  	// Should error for no coins.
  1121  	err = wallet.ReturnCoins(asset.Coins{})
  1122  	if err == nil {
  1123  		t.Fatalf("no error for zero coins")
  1124  	}
  1125  
  1126  	// nil unlocks all
  1127  	wallet.fundingCoins[outPoint{*tTxHash, 0}] = &fundingCoin{}
  1128  	err = wallet.ReturnCoins(nil)
  1129  	if err != nil {
  1130  		t.Fatalf("error for nil coins: %v", err)
  1131  	}
  1132  	if len(wallet.fundingCoins) != 0 {
  1133  		t.Errorf("all funding coins not unlocked")
  1134  	}
  1135  
  1136  	// Have the RPC return negative response.
  1137  	node.lockUnspentErr = tErr
  1138  	err = wallet.ReturnCoins(coins)
  1139  	if err == nil {
  1140  		t.Fatalf("no error for RPC failure")
  1141  	}
  1142  	node.lockUnspentErr = nil
  1143  
  1144  	// ReturnCoins should accept any type that implements wallet.Coin.
  1145  
  1146  	// Test the convertCoin method while we're here.
  1147  	badID := []byte{0x01, 0x02}
  1148  	badCoins := asset.Coins{&tCoin{id: badID}, &tCoin{id: badID}}
  1149  
  1150  	err = wallet.ReturnCoins(badCoins)
  1151  	if err == nil {
  1152  		t.Fatalf("no error for bad coins")
  1153  	}
  1154  
  1155  	coinID := toCoinID(tTxHash, 0)
  1156  	coins = asset.Coins{&tCoin{id: coinID}, &tCoin{id: coinID}}
  1157  	err = wallet.ReturnCoins(coins)
  1158  	if err != nil {
  1159  		t.Fatalf("error with custom coin type: %v", err)
  1160  	}
  1161  }
  1162  
  1163  func TestFundingCoins(t *testing.T) {
  1164  	wallet, node, shutdown := tNewWallet()
  1165  	defer shutdown()
  1166  
  1167  	vout := uint32(123)
  1168  	coinID := toCoinID(tTxHash, vout)
  1169  	p2pkhUnspent := walletjson.ListUnspentResult{
  1170  		TxID:      tTxID,
  1171  		Vout:      vout,
  1172  		Address:   tPKHAddr.String(),
  1173  		Spendable: true,
  1174  		Account:   tAcctName,
  1175  	}
  1176  
  1177  	node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent}
  1178  	coinIDs := []dex.Bytes{coinID}
  1179  
  1180  	ensureGood := func() {
  1181  		t.Helper()
  1182  		coins, err := wallet.FundingCoins(coinIDs)
  1183  		if err != nil {
  1184  			t.Fatalf("FundingCoins error: %v", err)
  1185  		}
  1186  		if len(coins) != 1 {
  1187  			t.Fatalf("expected 1 coin, got %d", len(coins))
  1188  		}
  1189  	}
  1190  
  1191  	// Check initial success.
  1192  	ensureGood()
  1193  
  1194  	// Clear the RPC coins, but add a coin to the cache.
  1195  	node.unspent = nil
  1196  	opID := newOutPoint(tTxHash, vout)
  1197  	wallet.fundingCoins[opID] = &fundingCoin{
  1198  		op: newOutput(tTxHash, vout, 0, 0),
  1199  	}
  1200  	ensureGood()
  1201  
  1202  	ensureErr := func(tag string) {
  1203  		_, err := wallet.FundingCoins(coinIDs)
  1204  		if err == nil {
  1205  			t.Fatalf("%s: no error", tag)
  1206  		}
  1207  	}
  1208  
  1209  	// No coins
  1210  	delete(wallet.fundingCoins, opID)
  1211  	ensureErr("no coins")
  1212  	node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent}
  1213  
  1214  	// Bad coin ID
  1215  	ogIDs := coinIDs
  1216  	coinIDs = []dex.Bytes{randBytes(35)}
  1217  	ensureErr("bad coin ID")
  1218  	coinIDs = ogIDs
  1219  
  1220  	// listunspent error
  1221  	node.unspentErr = tErr
  1222  	ensureErr("listunpent")
  1223  	node.unspentErr = nil
  1224  
  1225  	ensureGood()
  1226  }
  1227  
  1228  func checkMaxOrder(t *testing.T, wallet *ExchangeWallet, lots, swapVal, maxFees, estWorstCase, estBestCase uint64) {
  1229  	t.Helper()
  1230  	_, maxOrder, err := wallet.maxOrder(tLotSize, feeSuggestion, tDCR.MaxFeeRate)
  1231  	if err != nil {
  1232  		t.Fatalf("MaxOrder error: %v", err)
  1233  	}
  1234  	checkSwapEstimate(t, maxOrder, lots, swapVal, maxFees, estWorstCase, estBestCase)
  1235  }
  1236  
  1237  func checkSwapEstimate(t *testing.T, est *asset.SwapEstimate, lots, swapVal, maxFees, estWorstCase, estBestCase uint64) {
  1238  	t.Helper()
  1239  	if est.Lots != lots {
  1240  		t.Fatalf("MaxOrder has wrong Lots. wanted %d, got %d", lots, est.Lots)
  1241  	}
  1242  	if est.Value != swapVal {
  1243  		t.Fatalf("est has wrong Value. wanted %d, got %d", swapVal, est.Value)
  1244  	}
  1245  	if est.MaxFees != maxFees {
  1246  		t.Fatalf("est has wrong MaxFees. wanted %d, got %d", maxFees, est.MaxFees)
  1247  	}
  1248  	if est.RealisticWorstCase != estWorstCase {
  1249  		t.Fatalf("MaxOrder has wrong RealisticWorstCase. wanted %d, got %d", estWorstCase, est.RealisticWorstCase)
  1250  	}
  1251  	if est.RealisticBestCase != estBestCase {
  1252  		t.Fatalf("MaxOrder has wrong RealisticBestCase. wanted %d, got %d", estBestCase, est.RealisticBestCase)
  1253  	}
  1254  }
  1255  
  1256  func TestFundMultiOrder(t *testing.T) {
  1257  	wallet, node, shutdown := tNewWallet()
  1258  	defer shutdown()
  1259  
  1260  	maxFeeRate := uint64(80)
  1261  	feeSuggestion := uint64(60)
  1262  
  1263  	txIDs := make([]string, 0, 5)
  1264  	txHashes := make([]chainhash.Hash, 0, 5)
  1265  
  1266  	addresses := []string{
  1267  		tPKHAddr.String(),
  1268  		tPKHAddr.String(),
  1269  		tPKHAddr.String(),
  1270  		tPKHAddr.String(),
  1271  	}
  1272  	scriptPubKeys := []string{
  1273  		hex.EncodeToString(tP2PKHScript),
  1274  		hex.EncodeToString(tP2PKHScript),
  1275  		hex.EncodeToString(tP2PKHScript),
  1276  		hex.EncodeToString(tP2PKHScript),
  1277  	}
  1278  	for i := 0; i < 5; i++ {
  1279  		txIDs = append(txIDs, hex.EncodeToString(encode.RandomBytes(32)))
  1280  		h, _ := chainhash.NewHashFromStr(txIDs[i])
  1281  		txHashes = append(txHashes, *h)
  1282  	}
  1283  
  1284  	expectedSplitFee := func(numInputs, numOutputs uint64) uint64 {
  1285  		inputSize := uint64(dexdcr.P2PKHInputSize)
  1286  		outputSize := uint64(dexdcr.P2PKHOutputSize)
  1287  		return (dexdcr.MsgTxOverhead + numInputs*inputSize + numOutputs*outputSize) * feeSuggestion
  1288  	}
  1289  
  1290  	requiredForOrder := func(value, maxSwapCount uint64) int64 {
  1291  		inputSize := uint64(dexdcr.P2PKHInputSize)
  1292  		return int64(calc.RequiredOrderFunds(value, inputSize, maxSwapCount,
  1293  			dexdcr.InitTxSizeBase, dexdcr.InitTxSize, maxFeeRate))
  1294  	}
  1295  
  1296  	type test struct {
  1297  		name         string
  1298  		multiOrder   *asset.MultiOrder
  1299  		allOrNothing bool
  1300  		maxLock      uint64
  1301  		utxos        []walletjson.ListUnspentResult
  1302  		bondReserves uint64
  1303  		balance      uint64
  1304  
  1305  		// if expectedCoins is nil, all the coins are from
  1306  		// the split output. If any of the coins are nil,
  1307  		// than that output is from the split output.
  1308  		expectedCoins         []asset.Coins
  1309  		expectedRedeemScripts [][]dex.Bytes
  1310  		expectSendRawTx       bool
  1311  		expectedSplitFee      uint64
  1312  		expectedInputs        []*wire.TxIn
  1313  		expectedOutputs       []*wire.TxOut
  1314  		expectedChange        uint64
  1315  		expectedLockedCoins   []*wire.OutPoint
  1316  		expectErr             bool
  1317  	}
  1318  
  1319  	tests := []*test{
  1320  		{ // "split not allowed, utxos like split previously done"
  1321  			name: "split not allowed, utxos like split previously done",
  1322  			multiOrder: &asset.MultiOrder{
  1323  				Values: []*asset.MultiOrderValue{
  1324  					{
  1325  						Value:        1e6,
  1326  						MaxSwapCount: 1,
  1327  					},
  1328  					{
  1329  						Value:        2e6,
  1330  						MaxSwapCount: 2,
  1331  					},
  1332  				},
  1333  				MaxFeeRate:    maxFeeRate,
  1334  				FeeSuggestion: feeSuggestion,
  1335  				Options: map[string]string{
  1336  					"swapsplit": "false",
  1337  				},
  1338  			},
  1339  			utxos: []walletjson.ListUnspentResult{
  1340  				{
  1341  					Confirmations: 1,
  1342  					Spendable:     true,
  1343  					TxID:          txIDs[0],
  1344  					Account:       tAcctName,
  1345  					ScriptPubKey:  scriptPubKeys[0],
  1346  					Address:       addresses[0],
  1347  					Amount:        19e5 / 1e8,
  1348  					Vout:          0,
  1349  				},
  1350  				{
  1351  					Confirmations: 1,
  1352  					Spendable:     true,
  1353  					TxID:          txIDs[1],
  1354  					Account:       tAcctName,
  1355  					ScriptPubKey:  scriptPubKeys[1],
  1356  					Address:       addresses[1],
  1357  					Amount:        35e5 / 1e8,
  1358  					Vout:          0,
  1359  				},
  1360  			},
  1361  			balance: 35e5,
  1362  			expectedCoins: []asset.Coins{
  1363  				{newOutput(&txHashes[0], 0, 19e5, wire.TxTreeRegular)},
  1364  				{newOutput(&txHashes[1], 0, 35e5, wire.TxTreeRegular)},
  1365  			},
  1366  			expectedRedeemScripts: [][]dex.Bytes{
  1367  				{nil},
  1368  				{nil},
  1369  			},
  1370  		},
  1371  		{ // "split not allowed, require multiple utxos per order"
  1372  			name: "split not allowed, require multiple utxos per order",
  1373  			multiOrder: &asset.MultiOrder{
  1374  				Values: []*asset.MultiOrderValue{
  1375  					{
  1376  						Value:        1e6,
  1377  						MaxSwapCount: 1,
  1378  					},
  1379  					{
  1380  						Value:        2e6,
  1381  						MaxSwapCount: 2,
  1382  					},
  1383  				},
  1384  				MaxFeeRate:    maxFeeRate,
  1385  				FeeSuggestion: feeSuggestion,
  1386  				Options: map[string]string{
  1387  					"swapsplit": "false",
  1388  				},
  1389  			},
  1390  			utxos: []walletjson.ListUnspentResult{
  1391  				{
  1392  					Confirmations: 1,
  1393  					Spendable:     true,
  1394  					TxID:          txIDs[0],
  1395  					Account:       tAcctName,
  1396  					ScriptPubKey:  scriptPubKeys[0],
  1397  					Address:       addresses[0],
  1398  					Amount:        6e5 / 1e8,
  1399  					Vout:          0,
  1400  				},
  1401  				{
  1402  					Confirmations: 1,
  1403  					Spendable:     true,
  1404  					TxID:          txIDs[1],
  1405  					Account:       tAcctName,
  1406  					ScriptPubKey:  scriptPubKeys[1],
  1407  					Address:       addresses[1],
  1408  					Amount:        5e5 / 1e8,
  1409  					Vout:          0,
  1410  				},
  1411  				{
  1412  					Confirmations: 1,
  1413  					Spendable:     true,
  1414  					TxID:          txIDs[2],
  1415  					Account:       tAcctName,
  1416  					ScriptPubKey:  scriptPubKeys[2],
  1417  					Address:       addresses[2],
  1418  					Amount:        22e5 / 1e8,
  1419  					Vout:          0,
  1420  				},
  1421  			},
  1422  			balance: 33e5,
  1423  			expectedCoins: []asset.Coins{
  1424  				{newOutput(&txHashes[0], 0, 6e5, wire.TxTreeRegular), newOutput(&txHashes[1], 0, 5e5, wire.TxTreeRegular)},
  1425  				{newOutput(&txHashes[2], 0, 22e5, wire.TxTreeRegular)},
  1426  			},
  1427  			expectedRedeemScripts: [][]dex.Bytes{
  1428  				{nil, nil},
  1429  				{nil},
  1430  			},
  1431  			expectedLockedCoins: []*wire.OutPoint{
  1432  				wire.NewOutPoint(&txHashes[0], 0, wire.TxTreeRegular),
  1433  				wire.NewOutPoint(&txHashes[1], 0, wire.TxTreeRegular),
  1434  				wire.NewOutPoint(&txHashes[2], 0, wire.TxTreeRegular),
  1435  			},
  1436  		},
  1437  		{ // "split not allowed, can only fund first order and respect maxLock"
  1438  			name: "split not allowed, can only fund first order and respect maxLock",
  1439  			multiOrder: &asset.MultiOrder{
  1440  				Values: []*asset.MultiOrderValue{
  1441  					{
  1442  						Value:        1e6,
  1443  						MaxSwapCount: 1,
  1444  					},
  1445  					{
  1446  						Value:        2e6,
  1447  						MaxSwapCount: 2,
  1448  					},
  1449  				},
  1450  				MaxFeeRate:    maxFeeRate,
  1451  				FeeSuggestion: feeSuggestion,
  1452  				Options: map[string]string{
  1453  					"swapsplit": "false",
  1454  				},
  1455  			},
  1456  			maxLock: 32e5,
  1457  			utxos: []walletjson.ListUnspentResult{
  1458  				{
  1459  					Confirmations: 1,
  1460  					Spendable:     true,
  1461  					TxID:          txIDs[2],
  1462  					Account:       tAcctName,
  1463  					ScriptPubKey:  scriptPubKeys[2],
  1464  					Address:       addresses[2],
  1465  					Amount:        1e6 / 1e8,
  1466  					Vout:          0,
  1467  				},
  1468  				{
  1469  					Confirmations: 1,
  1470  					Spendable:     true,
  1471  					TxID:          txIDs[0],
  1472  					Account:       tAcctName,
  1473  					ScriptPubKey:  scriptPubKeys[0],
  1474  					Address:       addresses[0],
  1475  					Amount:        11e5 / 1e8,
  1476  					Vout:          0,
  1477  				},
  1478  				{
  1479  					Confirmations: 1,
  1480  					Spendable:     true,
  1481  					TxID:          txIDs[1],
  1482  					Account:       tAcctName,
  1483  					ScriptPubKey:  scriptPubKeys[1],
  1484  					Address:       addresses[1],
  1485  					Amount:        25e5 / 1e8,
  1486  					Vout:          0,
  1487  				},
  1488  			},
  1489  			balance: 46e5,
  1490  			expectedCoins: []asset.Coins{
  1491  				{newOutput(&txHashes[0], 0, 11e5, wire.TxTreeRegular)},
  1492  			},
  1493  			expectedRedeemScripts: [][]dex.Bytes{
  1494  				{nil},
  1495  			},
  1496  			expectedLockedCoins: []*wire.OutPoint{
  1497  				wire.NewOutPoint(&txHashes[0], 0, wire.TxTreeRegular),
  1498  			},
  1499  		},
  1500  		{ // "split not allowed, can only fund first order and respect bond reserves"
  1501  			name: "no split allowed, can only fund first order and respect bond reserves",
  1502  			multiOrder: &asset.MultiOrder{
  1503  				Values: []*asset.MultiOrderValue{
  1504  					{
  1505  						Value:        1e6,
  1506  						MaxSwapCount: 1,
  1507  					},
  1508  					{
  1509  						Value:        2e6,
  1510  						MaxSwapCount: 2,
  1511  					},
  1512  				},
  1513  				MaxFeeRate:    maxFeeRate,
  1514  				FeeSuggestion: feeSuggestion,
  1515  				Options: map[string]string{
  1516  					multiSplitKey: "false",
  1517  				},
  1518  			},
  1519  			maxLock:      46e5,
  1520  			bondReserves: 12e5,
  1521  			utxos: []walletjson.ListUnspentResult{
  1522  				{
  1523  					Confirmations: 1,
  1524  					Spendable:     true,
  1525  					TxID:          txIDs[2],
  1526  					Account:       tAcctName,
  1527  					ScriptPubKey:  scriptPubKeys[2],
  1528  					Address:       addresses[2],
  1529  					Amount:        1e6 / 1e8,
  1530  					Vout:          0,
  1531  				},
  1532  				{
  1533  					Confirmations: 1,
  1534  					Spendable:     true,
  1535  					TxID:          txIDs[0],
  1536  					Account:       tAcctName,
  1537  					ScriptPubKey:  scriptPubKeys[0],
  1538  					Address:       addresses[0],
  1539  					Amount:        11e5 / 1e8,
  1540  					Vout:          0,
  1541  				},
  1542  				{
  1543  					Confirmations: 1,
  1544  					Spendable:     true,
  1545  					TxID:          txIDs[1],
  1546  					Account:       tAcctName,
  1547  					ScriptPubKey:  scriptPubKeys[1],
  1548  					Address:       addresses[1],
  1549  					Amount:        25e5 / 1e8,
  1550  					Vout:          0,
  1551  				},
  1552  			},
  1553  			balance: 46e5,
  1554  			expectedCoins: []asset.Coins{
  1555  				{newOutput(&txHashes[0], 0, 11e5, wire.TxTreeRegular)},
  1556  			},
  1557  			expectedRedeemScripts: [][]dex.Bytes{
  1558  				{nil},
  1559  			},
  1560  			expectedLockedCoins: []*wire.OutPoint{
  1561  				wire.NewOutPoint(&txHashes[0], 0, wire.TxTreeRegular),
  1562  			},
  1563  		},
  1564  		{ // "split not allowed, need to fund in increasing order"
  1565  			name: "no split, need to fund in increasing order",
  1566  			multiOrder: &asset.MultiOrder{
  1567  				Values: []*asset.MultiOrderValue{
  1568  					{
  1569  						Value:        2e6,
  1570  						MaxSwapCount: 2,
  1571  					},
  1572  					{
  1573  						Value:        11e5,
  1574  						MaxSwapCount: 1,
  1575  					},
  1576  					{
  1577  						Value:        9e5,
  1578  						MaxSwapCount: 1,
  1579  					},
  1580  				},
  1581  				MaxFeeRate:    maxFeeRate,
  1582  				FeeSuggestion: feeSuggestion,
  1583  				Options: map[string]string{
  1584  					multiSplitKey: "false",
  1585  				},
  1586  			},
  1587  			maxLock: 50e5,
  1588  			utxos: []walletjson.ListUnspentResult{
  1589  				{
  1590  					Confirmations: 1,
  1591  					Spendable:     true,
  1592  					TxID:          txIDs[0],
  1593  					Account:       tAcctName,
  1594  					ScriptPubKey:  scriptPubKeys[0],
  1595  					Address:       addresses[0],
  1596  					Amount:        11e5 / 1e8,
  1597  					Vout:          0,
  1598  				},
  1599  				{
  1600  					Confirmations: 1,
  1601  					Spendable:     true,
  1602  					TxID:          txIDs[1],
  1603  					Account:       tAcctName,
  1604  					ScriptPubKey:  scriptPubKeys[1],
  1605  					Address:       addresses[1],
  1606  					Amount:        13e5 / 1e8,
  1607  					Vout:          0,
  1608  				},
  1609  				{
  1610  					Confirmations: 1,
  1611  					Spendable:     true,
  1612  					TxID:          txIDs[2],
  1613  					Account:       tAcctName,
  1614  					ScriptPubKey:  scriptPubKeys[2],
  1615  					Address:       addresses[2],
  1616  					Amount:        26e5 / 1e8,
  1617  					Vout:          0,
  1618  				},
  1619  			},
  1620  			balance: 50e5,
  1621  			expectedCoins: []asset.Coins{
  1622  				{newOutput(&txHashes[2], 0, 26e5, wire.TxTreeRegular)},
  1623  				{newOutput(&txHashes[1], 0, 13e5, wire.TxTreeRegular)},
  1624  				{newOutput(&txHashes[0], 0, 11e5, wire.TxTreeRegular)},
  1625  			},
  1626  			expectedRedeemScripts: [][]dex.Bytes{
  1627  				{nil},
  1628  				{nil},
  1629  				{nil},
  1630  			},
  1631  			expectedLockedCoins: []*wire.OutPoint{
  1632  				wire.NewOutPoint(&txHashes[0], 0, wire.TxTreeRegular),
  1633  				wire.NewOutPoint(&txHashes[1], 0, wire.TxTreeRegular),
  1634  				wire.NewOutPoint(&txHashes[2], 0, wire.TxTreeRegular),
  1635  			},
  1636  		},
  1637  		{ // "split allowed, no split required"
  1638  			name: "split allowed, no split required",
  1639  			multiOrder: &asset.MultiOrder{
  1640  				Values: []*asset.MultiOrderValue{
  1641  					{
  1642  						Value:        1e6,
  1643  						MaxSwapCount: 1,
  1644  					},
  1645  					{
  1646  						Value:        2e6,
  1647  						MaxSwapCount: 2,
  1648  					},
  1649  				},
  1650  				MaxFeeRate:    maxFeeRate,
  1651  				FeeSuggestion: feeSuggestion,
  1652  				Options: map[string]string{
  1653  					multiSplitKey: "true",
  1654  				},
  1655  			},
  1656  			allOrNothing: false,
  1657  			maxLock:      43e5,
  1658  			utxos: []walletjson.ListUnspentResult{
  1659  				{
  1660  					Confirmations: 1,
  1661  					Spendable:     true,
  1662  					TxID:          txIDs[2],
  1663  					Account:       tAcctName,
  1664  					ScriptPubKey:  scriptPubKeys[2],
  1665  					Address:       addresses[2],
  1666  					Amount:        1e6 / 1e8,
  1667  					Vout:          0,
  1668  				},
  1669  				{
  1670  					Confirmations: 1,
  1671  					Spendable:     true,
  1672  					TxID:          txIDs[0],
  1673  					Account:       tAcctName,
  1674  					ScriptPubKey:  scriptPubKeys[0],
  1675  					Address:       addresses[0],
  1676  					Amount:        11e5 / 1e8,
  1677  					Vout:          0,
  1678  				},
  1679  				{
  1680  					Confirmations: 1,
  1681  					Spendable:     true,
  1682  					TxID:          txIDs[1],
  1683  					Account:       tAcctName,
  1684  					ScriptPubKey:  scriptPubKeys[1],
  1685  					Address:       addresses[1],
  1686  					Amount:        22e5 / 1e8,
  1687  					Vout:          0,
  1688  				},
  1689  			},
  1690  			balance: 43e5,
  1691  			expectedCoins: []asset.Coins{
  1692  				{newOutput(&txHashes[0], 0, 11e5, wire.TxTreeRegular)},
  1693  				{newOutput(&txHashes[1], 0, 22e5, wire.TxTreeRegular)},
  1694  			},
  1695  			expectedRedeemScripts: [][]dex.Bytes{
  1696  				{nil},
  1697  				{nil},
  1698  			},
  1699  			expectedLockedCoins: []*wire.OutPoint{
  1700  				wire.NewOutPoint(&txHashes[1], 0, wire.TxTreeRegular),
  1701  				wire.NewOutPoint(&txHashes[2], 0, wire.TxTreeRegular),
  1702  			},
  1703  		},
  1704  		{ // "split allowed, can fund both with split"
  1705  			name: "split allowed, can fund both with split",
  1706  			multiOrder: &asset.MultiOrder{
  1707  				Values: []*asset.MultiOrderValue{
  1708  					{
  1709  						Value:        15e5,
  1710  						MaxSwapCount: 2,
  1711  					},
  1712  					{
  1713  						Value:        15e5,
  1714  						MaxSwapCount: 2,
  1715  					},
  1716  				},
  1717  				MaxFeeRate:    maxFeeRate,
  1718  				FeeSuggestion: feeSuggestion,
  1719  				Options: map[string]string{
  1720  					multiSplitKey: "true",
  1721  				},
  1722  			},
  1723  			utxos: []walletjson.ListUnspentResult{
  1724  				{
  1725  					Confirmations: 1,
  1726  					Spendable:     true,
  1727  					TxID:          txIDs[0],
  1728  					Account:       tAcctName,
  1729  					ScriptPubKey:  scriptPubKeys[0],
  1730  					Address:       addresses[0],
  1731  					Amount:        1e6 / 1e8,
  1732  					Vout:          0,
  1733  				},
  1734  				{
  1735  					Confirmations: 1,
  1736  					Spendable:     true,
  1737  					TxID:          txIDs[1],
  1738  					Account:       tAcctName,
  1739  					ScriptPubKey:  scriptPubKeys[1],
  1740  					Address:       addresses[1],
  1741  					Amount:        (2*float64(requiredForOrder(15e5, 2)) + float64(expectedSplitFee(2, 2)) - 1e6) / 1e8,
  1742  					Vout:          0,
  1743  				},
  1744  			},
  1745  			maxLock:         2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2),
  1746  			balance:         2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2),
  1747  			expectSendRawTx: true,
  1748  			expectedInputs: []*wire.TxIn{
  1749  				{
  1750  					PreviousOutPoint: wire.OutPoint{
  1751  						Hash:  txHashes[1],
  1752  						Index: 0,
  1753  					},
  1754  				},
  1755  				{
  1756  					PreviousOutPoint: wire.OutPoint{
  1757  						Hash:  txHashes[0],
  1758  						Index: 0,
  1759  					},
  1760  				},
  1761  			},
  1762  			expectedOutputs: []*wire.TxOut{
  1763  				wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}),
  1764  				wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}),
  1765  			},
  1766  			expectedSplitFee: expectedSplitFee(2, 2),
  1767  			expectedRedeemScripts: [][]dex.Bytes{
  1768  				{nil},
  1769  				{nil},
  1770  			},
  1771  		},
  1772  		{ // "split allowed, cannot fund both with split"
  1773  			name: "split allowed, cannot fund both with split",
  1774  			multiOrder: &asset.MultiOrder{
  1775  				Values: []*asset.MultiOrderValue{
  1776  					{
  1777  						Value:        15e5,
  1778  						MaxSwapCount: 2,
  1779  					},
  1780  					{
  1781  						Value:        15e5,
  1782  						MaxSwapCount: 2,
  1783  					},
  1784  				},
  1785  				MaxFeeRate:    maxFeeRate,
  1786  				FeeSuggestion: feeSuggestion,
  1787  				Options: map[string]string{
  1788  					multiSplitKey: "true",
  1789  				},
  1790  			},
  1791  			utxos: []walletjson.ListUnspentResult{
  1792  				{
  1793  					Confirmations: 1,
  1794  					Spendable:     true,
  1795  					TxID:          txIDs[0],
  1796  					Account:       tAcctName,
  1797  					ScriptPubKey:  scriptPubKeys[0],
  1798  					Address:       addresses[0],
  1799  					Amount:        1e6 / 1e8,
  1800  					Vout:          0,
  1801  				},
  1802  				{
  1803  					Confirmations: 1,
  1804  					Spendable:     true,
  1805  					TxID:          txIDs[1],
  1806  					Account:       tAcctName,
  1807  					ScriptPubKey:  scriptPubKeys[1],
  1808  					Address:       addresses[1],
  1809  					Amount:        (2*float64(requiredForOrder(15e5, 2)) + float64(expectedSplitFee(2, 2)) - 1e6) / 1e8,
  1810  					Vout:          0,
  1811  				},
  1812  			},
  1813  			maxLock:   2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2) - 1,
  1814  			balance:   2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(2, 2) - 1,
  1815  			expectErr: true,
  1816  		},
  1817  		{ // "can fund both with split and respect maxLock"
  1818  			name: "can fund both with split and respect maxLock",
  1819  			multiOrder: &asset.MultiOrder{
  1820  				Values: []*asset.MultiOrderValue{
  1821  					{
  1822  						Value:        15e5,
  1823  						MaxSwapCount: 2,
  1824  					},
  1825  					{
  1826  						Value:        15e5,
  1827  						MaxSwapCount: 2,
  1828  					},
  1829  				},
  1830  				MaxFeeRate:    maxFeeRate,
  1831  				FeeSuggestion: feeSuggestion,
  1832  				Options: map[string]string{
  1833  					multiSplitKey: "true",
  1834  				},
  1835  			},
  1836  			utxos: []walletjson.ListUnspentResult{
  1837  				{
  1838  					Confirmations: 1,
  1839  					Spendable:     true,
  1840  					TxID:          txIDs[0],
  1841  					Account:       tAcctName,
  1842  					ScriptPubKey:  scriptPubKeys[0],
  1843  					Address:       addresses[0],
  1844  					Amount:        float64(50e5) / 1e8,
  1845  					Vout:          0,
  1846  				},
  1847  			},
  1848  			balance:         50e5,
  1849  			maxLock:         2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 2),
  1850  			expectSendRawTx: true,
  1851  			expectedInputs: []*wire.TxIn{
  1852  				{
  1853  					PreviousOutPoint: wire.OutPoint{
  1854  						Hash:  txHashes[0],
  1855  						Index: 0,
  1856  					},
  1857  				},
  1858  			},
  1859  			expectedOutputs: []*wire.TxOut{
  1860  				wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}),
  1861  				wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}),
  1862  			},
  1863  			expectedChange:   50e5 - (2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3)),
  1864  			expectedSplitFee: expectedSplitFee(1, 3),
  1865  			expectedRedeemScripts: [][]dex.Bytes{
  1866  				{nil},
  1867  				{nil},
  1868  			},
  1869  		},
  1870  		{ // "cannot fund both with split and respect maxLock"
  1871  			name: "cannot fund both with split and respect maxLock",
  1872  			multiOrder: &asset.MultiOrder{
  1873  				Values: []*asset.MultiOrderValue{
  1874  					{
  1875  						Value:        15e5,
  1876  						MaxSwapCount: 2,
  1877  					},
  1878  					{
  1879  						Value:        15e5,
  1880  						MaxSwapCount: 2,
  1881  					},
  1882  				},
  1883  				MaxFeeRate:    maxFeeRate,
  1884  				FeeSuggestion: feeSuggestion,
  1885  				Options: map[string]string{
  1886  					multiSplitKey: "true",
  1887  				},
  1888  			},
  1889  			utxos: []walletjson.ListUnspentResult{
  1890  				{
  1891  					Confirmations: 1,
  1892  					Spendable:     true,
  1893  					TxID:          txIDs[0],
  1894  					Account:       tAcctName,
  1895  					ScriptPubKey:  scriptPubKeys[0],
  1896  					Address:       addresses[0],
  1897  					Amount:        float64(50e5) / 1e8,
  1898  					Vout:          0,
  1899  				},
  1900  			},
  1901  			balance:   50e5,
  1902  			maxLock:   2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 2) - 1,
  1903  			expectErr: true,
  1904  		},
  1905  		{ // "split allowed, can fund both with split with bond reserves"
  1906  			name: "split allowed, can fund both with split with bond reserves",
  1907  			multiOrder: &asset.MultiOrder{
  1908  				Values: []*asset.MultiOrderValue{
  1909  					{
  1910  						Value:        15e5,
  1911  						MaxSwapCount: 2,
  1912  					},
  1913  					{
  1914  						Value:        15e5,
  1915  						MaxSwapCount: 2,
  1916  					},
  1917  				},
  1918  				MaxFeeRate:    maxFeeRate,
  1919  				FeeSuggestion: feeSuggestion,
  1920  				Options: map[string]string{
  1921  					multiSplitKey: "true",
  1922  				},
  1923  			},
  1924  			bondReserves: 2e6,
  1925  			utxos: []walletjson.ListUnspentResult{
  1926  				{
  1927  					Confirmations: 1,
  1928  					Spendable:     true,
  1929  					TxID:          txIDs[0],
  1930  					Account:       tAcctName,
  1931  					ScriptPubKey:  scriptPubKeys[0],
  1932  					Address:       addresses[0],
  1933  					Amount:        (2*float64(requiredForOrder(15e5, 2)) + 2e6 + float64(expectedSplitFee(1, 3))) / 1e8,
  1934  					Vout:          0,
  1935  				},
  1936  			},
  1937  			balance:         2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3),
  1938  			maxLock:         2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3),
  1939  			expectSendRawTx: true,
  1940  			expectedInputs: []*wire.TxIn{
  1941  				{
  1942  					PreviousOutPoint: wire.OutPoint{
  1943  						Hash:  txHashes[0],
  1944  						Index: 0,
  1945  					},
  1946  				},
  1947  			},
  1948  			expectedOutputs: []*wire.TxOut{
  1949  				wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}),
  1950  				wire.NewTxOut(requiredForOrder(15e5, 2), []byte{}),
  1951  			},
  1952  			expectedChange:   2e6,
  1953  			expectedSplitFee: expectedSplitFee(1, 3),
  1954  			expectedRedeemScripts: [][]dex.Bytes{
  1955  				{nil},
  1956  				{nil},
  1957  			},
  1958  		},
  1959  		{ // "split allowed, cannot fund both with split and keep and bond reserves"
  1960  			name: "split allowed, cannot fund both with split and keep and bond reserves",
  1961  			multiOrder: &asset.MultiOrder{
  1962  				Values: []*asset.MultiOrderValue{
  1963  					{
  1964  						Value:        15e5,
  1965  						MaxSwapCount: 2,
  1966  					},
  1967  					{
  1968  						Value:        15e5,
  1969  						MaxSwapCount: 2,
  1970  					},
  1971  				},
  1972  				MaxFeeRate:    maxFeeRate,
  1973  				FeeSuggestion: feeSuggestion,
  1974  				Options: map[string]string{
  1975  					multiSplitKey: "true",
  1976  				},
  1977  			},
  1978  			bondReserves: 2e6,
  1979  			utxos: []walletjson.ListUnspentResult{
  1980  				{
  1981  					Confirmations: 1,
  1982  					Spendable:     true,
  1983  					TxID:          txIDs[0],
  1984  					Account:       tAcctName,
  1985  					ScriptPubKey:  scriptPubKeys[0],
  1986  					Address:       addresses[0],
  1987  					Amount:        ((2*float64(requiredForOrder(15e5, 2)) + 2e6 + float64(expectedSplitFee(1, 3))) / 1e8) - 1/1e8,
  1988  					Vout:          0,
  1989  				},
  1990  			},
  1991  			balance:   2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3) - 1,
  1992  			maxLock:   2e6 + 2*uint64(requiredForOrder(15e5, 2)) + expectedSplitFee(1, 3) - 1,
  1993  			expectErr: true,
  1994  		},
  1995  		{ // "split with buffer"
  1996  			name: "split with buffer",
  1997  			multiOrder: &asset.MultiOrder{
  1998  				Values: []*asset.MultiOrderValue{
  1999  					{
  2000  						Value:        15e5,
  2001  						MaxSwapCount: 2,
  2002  					},
  2003  					{
  2004  						Value:        15e5,
  2005  						MaxSwapCount: 2,
  2006  					},
  2007  				},
  2008  				MaxFeeRate:    maxFeeRate,
  2009  				FeeSuggestion: feeSuggestion,
  2010  				Options: map[string]string{
  2011  					multiSplitKey:       "true",
  2012  					multiSplitBufferKey: "10",
  2013  				},
  2014  			},
  2015  			utxos: []walletjson.ListUnspentResult{
  2016  				{
  2017  					Confirmations: 1,
  2018  					Spendable:     true,
  2019  					TxID:          txIDs[0],
  2020  					Account:       tAcctName,
  2021  					ScriptPubKey:  scriptPubKeys[0],
  2022  					Address:       addresses[0],
  2023  					Amount:        (2*float64(requiredForOrder(15e5, 2)*110/100) + float64(expectedSplitFee(1, 2))) / 1e8,
  2024  					Vout:          0,
  2025  				},
  2026  			},
  2027  			balance:         2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2),
  2028  			maxLock:         2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2),
  2029  			expectSendRawTx: true,
  2030  			expectedInputs: []*wire.TxIn{
  2031  				{
  2032  					PreviousOutPoint: wire.OutPoint{
  2033  						Hash:  txHashes[0],
  2034  						Index: 0,
  2035  					},
  2036  				},
  2037  			},
  2038  			expectedOutputs: []*wire.TxOut{
  2039  				wire.NewTxOut(requiredForOrder(15e5, 2)*110/100, []byte{}),
  2040  				wire.NewTxOut(requiredForOrder(15e5, 2)*110/100, []byte{}),
  2041  			},
  2042  			expectedSplitFee: expectedSplitFee(1, 2),
  2043  			expectedRedeemScripts: [][]dex.Bytes{
  2044  				{nil},
  2045  				{nil},
  2046  			},
  2047  		},
  2048  		{ // "split, maxLock too low to fund buffer"
  2049  			name: "split, maxLock too low to fund buffer",
  2050  			multiOrder: &asset.MultiOrder{
  2051  				Values: []*asset.MultiOrderValue{
  2052  					{
  2053  						Value:        15e5,
  2054  						MaxSwapCount: 2,
  2055  					},
  2056  					{
  2057  						Value:        15e5,
  2058  						MaxSwapCount: 2,
  2059  					},
  2060  				},
  2061  				MaxFeeRate:    maxFeeRate,
  2062  				FeeSuggestion: feeSuggestion,
  2063  				Options: map[string]string{
  2064  					multiSplitKey:       "true",
  2065  					multiSplitBufferKey: "10",
  2066  				},
  2067  			},
  2068  			utxos: []walletjson.ListUnspentResult{
  2069  				{
  2070  					Confirmations: 1,
  2071  					Spendable:     true,
  2072  					TxID:          txIDs[0],
  2073  					Account:       tAcctName,
  2074  					ScriptPubKey:  scriptPubKeys[0],
  2075  					Address:       addresses[0],
  2076  					Amount:        (2*float64(requiredForOrder(15e5, 2)*110/100) + float64(expectedSplitFee(1, 2))) / 1e8,
  2077  					Vout:          0,
  2078  				},
  2079  			},
  2080  			balance:   2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2),
  2081  			maxLock:   2*uint64(requiredForOrder(15e5, 2)*110/100) + expectedSplitFee(1, 2) - 1,
  2082  			expectErr: true,
  2083  		},
  2084  		{ // "only one order needs a split, rest can be funded without"
  2085  			name: "only one order needs a split, rest can be funded without",
  2086  			multiOrder: &asset.MultiOrder{
  2087  				Values: []*asset.MultiOrderValue{
  2088  					{
  2089  						Value:        1e6,
  2090  						MaxSwapCount: 2,
  2091  					},
  2092  					{
  2093  						Value:        1e6,
  2094  						MaxSwapCount: 2,
  2095  					},
  2096  					{
  2097  						Value:        1e6,
  2098  						MaxSwapCount: 2,
  2099  					},
  2100  				},
  2101  				MaxFeeRate:    maxFeeRate,
  2102  				FeeSuggestion: feeSuggestion,
  2103  				Options: map[string]string{
  2104  					multiSplitKey: "true",
  2105  				},
  2106  			},
  2107  			utxos: []walletjson.ListUnspentResult{
  2108  				{
  2109  					Confirmations: 1,
  2110  					Spendable:     true,
  2111  					TxID:          txIDs[0],
  2112  					Account:       tAcctName,
  2113  					ScriptPubKey:  scriptPubKeys[0],
  2114  					Address:       addresses[0],
  2115  					Amount:        12e5 / 1e8,
  2116  					Vout:          0,
  2117  				},
  2118  				{
  2119  					Confirmations: 1,
  2120  					Spendable:     true,
  2121  					TxID:          txIDs[1],
  2122  					Account:       tAcctName,
  2123  					ScriptPubKey:  scriptPubKeys[1],
  2124  					Address:       addresses[1],
  2125  					Amount:        12e5 / 1e8,
  2126  					Vout:          0,
  2127  				},
  2128  				{
  2129  					Confirmations: 1,
  2130  					Spendable:     true,
  2131  					TxID:          txIDs[2],
  2132  					Account:       tAcctName,
  2133  					ScriptPubKey:  scriptPubKeys[2],
  2134  					Address:       addresses[2],
  2135  					Amount:        120e5 / 1e8,
  2136  					Vout:          0,
  2137  				},
  2138  			},
  2139  			maxLock:         50e5,
  2140  			balance:         144e5,
  2141  			expectSendRawTx: true,
  2142  			expectedInputs: []*wire.TxIn{
  2143  				{
  2144  					PreviousOutPoint: wire.OutPoint{
  2145  						Hash:  txHashes[2],
  2146  						Index: 0,
  2147  					},
  2148  				},
  2149  			},
  2150  			expectedOutputs: []*wire.TxOut{
  2151  				wire.NewTxOut(requiredForOrder(1e6, 2), []byte{}),
  2152  				wire.NewTxOut(120e5-requiredForOrder(1e6, 2)-int64(expectedSplitFee(1, 2)), []byte{}),
  2153  			},
  2154  			expectedSplitFee: expectedSplitFee(1, 2),
  2155  			expectedRedeemScripts: [][]dex.Bytes{
  2156  				{nil},
  2157  				{nil},
  2158  				{nil},
  2159  			},
  2160  			expectedCoins: []asset.Coins{
  2161  				{newOutput(&txHashes[0], 0, 12e5, wire.TxTreeRegular)},
  2162  				{newOutput(&txHashes[1], 0, 12e5, wire.TxTreeRegular)},
  2163  				nil,
  2164  			},
  2165  		},
  2166  		{ // "only one order needs a split due to bond reserves, rest funded without"
  2167  			name: "only one order needs a split due to bond reserves, rest funded without",
  2168  			multiOrder: &asset.MultiOrder{
  2169  				Values: []*asset.MultiOrderValue{
  2170  					{
  2171  						Value:        1e6,
  2172  						MaxSwapCount: 2,
  2173  					},
  2174  					{
  2175  						Value:        1e6,
  2176  						MaxSwapCount: 2,
  2177  					},
  2178  					{
  2179  						Value:        1e6,
  2180  						MaxSwapCount: 2,
  2181  					},
  2182  				},
  2183  				MaxFeeRate:    maxFeeRate,
  2184  				FeeSuggestion: feeSuggestion,
  2185  				Options: map[string]string{
  2186  					multiSplitKey: "true",
  2187  				},
  2188  			},
  2189  			utxos: []walletjson.ListUnspentResult{
  2190  				{
  2191  					Confirmations: 1,
  2192  					Spendable:     true,
  2193  					TxID:          txIDs[0],
  2194  					Account:       tAcctName,
  2195  					ScriptPubKey:  scriptPubKeys[0],
  2196  					Address:       addresses[0],
  2197  					Amount:        12e5 / 1e8,
  2198  					Vout:          0,
  2199  				},
  2200  				{
  2201  					Confirmations: 1,
  2202  					Spendable:     true,
  2203  					TxID:          txIDs[1],
  2204  					Account:       tAcctName,
  2205  					ScriptPubKey:  scriptPubKeys[1],
  2206  					Address:       addresses[1],
  2207  					Amount:        12e5 / 1e8,
  2208  					Vout:          0,
  2209  				},
  2210  				{
  2211  					Confirmations: 1,
  2212  					Spendable:     true,
  2213  					TxID:          txIDs[2],
  2214  					Account:       tAcctName,
  2215  					ScriptPubKey:  scriptPubKeys[2],
  2216  					Address:       addresses[2],
  2217  					Amount:        120e5 / 1e8,
  2218  					Vout:          0,
  2219  				},
  2220  			},
  2221  			maxLock:         0,
  2222  			bondReserves:    1e6,
  2223  			balance:         144e5,
  2224  			expectSendRawTx: true,
  2225  			expectedInputs: []*wire.TxIn{
  2226  				{
  2227  					PreviousOutPoint: wire.OutPoint{
  2228  						Hash:  txHashes[2],
  2229  						Index: 0,
  2230  					},
  2231  				},
  2232  			},
  2233  			expectedOutputs: []*wire.TxOut{
  2234  				wire.NewTxOut(requiredForOrder(1e6, 2), []byte{}),
  2235  				wire.NewTxOut(120e5-requiredForOrder(1e6, 2)-int64(expectedSplitFee(1, 2)), []byte{}),
  2236  			},
  2237  			expectedSplitFee: expectedSplitFee(1, 2),
  2238  			expectedRedeemScripts: [][]dex.Bytes{
  2239  				{nil},
  2240  				{nil},
  2241  				{nil},
  2242  			},
  2243  			expectedCoins: []asset.Coins{
  2244  				{newOutput(&txHashes[0], 0, 12e5, wire.TxTreeRegular)},
  2245  				{newOutput(&txHashes[1], 0, 12e5, wire.TxTreeRegular)},
  2246  				nil,
  2247  			},
  2248  		},
  2249  		{ // "only one order needs a split due to maxLock, rest funded without"
  2250  			name: "only one order needs a split due to maxLock, rest funded without",
  2251  			multiOrder: &asset.MultiOrder{
  2252  				Values: []*asset.MultiOrderValue{
  2253  					{
  2254  						Value:        1e6,
  2255  						MaxSwapCount: 2,
  2256  					},
  2257  					{
  2258  						Value:        1e6,
  2259  						MaxSwapCount: 2,
  2260  					},
  2261  					{
  2262  						Value:        1e6,
  2263  						MaxSwapCount: 2,
  2264  					},
  2265  				},
  2266  				MaxFeeRate:    maxFeeRate,
  2267  				FeeSuggestion: feeSuggestion,
  2268  				Options: map[string]string{
  2269  					multiSplitKey: "true",
  2270  				},
  2271  			},
  2272  			utxos: []walletjson.ListUnspentResult{
  2273  				{
  2274  					Confirmations: 1,
  2275  					Spendable:     true,
  2276  					TxID:          txIDs[0],
  2277  					Account:       tAcctName,
  2278  					ScriptPubKey:  scriptPubKeys[0],
  2279  					Address:       addresses[0],
  2280  					Amount:        12e5 / 1e8,
  2281  					Vout:          0,
  2282  				},
  2283  				{
  2284  					Confirmations: 1,
  2285  					Spendable:     true,
  2286  					TxID:          txIDs[1],
  2287  					Account:       tAcctName,
  2288  					ScriptPubKey:  scriptPubKeys[1],
  2289  					Address:       addresses[1],
  2290  					Amount:        12e5 / 1e8,
  2291  					Vout:          0,
  2292  				},
  2293  				{
  2294  					Confirmations: 1,
  2295  					Spendable:     true,
  2296  					TxID:          txIDs[2],
  2297  					Account:       tAcctName,
  2298  					ScriptPubKey:  scriptPubKeys[2],
  2299  					Address:       addresses[2],
  2300  					Amount:        9e5 / 1e8,
  2301  					Vout:          0,
  2302  				},
  2303  				{
  2304  					Confirmations: 1,
  2305  					Spendable:     true,
  2306  					TxID:          txIDs[3],
  2307  					Account:       tAcctName,
  2308  					ScriptPubKey:  scriptPubKeys[3],
  2309  					Address:       addresses[3],
  2310  					Amount:        9e5 / 1e8,
  2311  					Vout:          0,
  2312  				},
  2313  			},
  2314  			maxLock:         35e5,
  2315  			bondReserves:    0,
  2316  			balance:         42e5,
  2317  			expectSendRawTx: true,
  2318  			expectedInputs: []*wire.TxIn{
  2319  				{
  2320  					PreviousOutPoint: wire.OutPoint{
  2321  						Hash:  txHashes[3],
  2322  						Index: 0,
  2323  					},
  2324  				},
  2325  				{
  2326  					PreviousOutPoint: wire.OutPoint{
  2327  						Hash:  txHashes[2],
  2328  						Index: 0,
  2329  					},
  2330  				},
  2331  			},
  2332  			expectedOutputs: []*wire.TxOut{
  2333  				wire.NewTxOut(requiredForOrder(1e6, 2), []byte{}),
  2334  				wire.NewTxOut(18e5-requiredForOrder(1e6, 2)-int64(expectedSplitFee(2, 2)), []byte{}),
  2335  			},
  2336  			expectedSplitFee: expectedSplitFee(2, 2),
  2337  			expectedRedeemScripts: [][]dex.Bytes{
  2338  				{nil},
  2339  				{nil},
  2340  				{nil},
  2341  			},
  2342  			expectedCoins: []asset.Coins{
  2343  				{newOutput(&txHashes[0], 0, 12e5, wire.TxTreeRegular)},
  2344  				{newOutput(&txHashes[1], 0, 12e5, wire.TxTreeRegular)},
  2345  				nil,
  2346  			},
  2347  		},
  2348  	}
  2349  
  2350  	for _, test := range tests {
  2351  		node.unspent = test.utxos
  2352  		node.newAddr = tPKHAddr
  2353  		node.changeAddr = tPKHAddr
  2354  		node.signFunc = func(msgTx *wire.MsgTx) (*wire.MsgTx, bool, error) {
  2355  			return signFunc(msgTx, dexdcr.P2PKHSigScriptSize)
  2356  		}
  2357  		node.sentRawTx = nil
  2358  		node.lockedCoins = nil
  2359  		node.balanceResult = &walletjson.GetBalanceResult{
  2360  			Balances: []walletjson.GetAccountBalanceResult{
  2361  				{
  2362  					AccountName: tAcctName,
  2363  					Spendable:   toDCR(test.balance),
  2364  				},
  2365  			},
  2366  		}
  2367  		wallet.fundingCoins = make(map[outPoint]*fundingCoin)
  2368  		wallet.bondReserves.Store(test.bondReserves)
  2369  
  2370  		allCoins, _, splitFee, err := wallet.FundMultiOrder(test.multiOrder, test.maxLock)
  2371  		if test.expectErr {
  2372  			if err == nil {
  2373  				t.Fatalf("%s: no error returned", test.name)
  2374  			}
  2375  			if strings.Contains(err.Error(), "insufficient funds") {
  2376  				t.Fatalf("%s: unexpected insufficient funds error", test.name)
  2377  			}
  2378  			continue
  2379  		}
  2380  		if err != nil {
  2381  			t.Fatalf("%s: unexpected error: %v", test.name, err)
  2382  		}
  2383  
  2384  		if !test.expectSendRawTx { // no split
  2385  			if node.sentRawTx != nil {
  2386  				t.Fatalf("%s: unexpected transaction sent", test.name)
  2387  			}
  2388  			if len(allCoins) != len(test.expectedCoins) {
  2389  				t.Fatalf("%s: expected %d coins, got %d", test.name, len(test.expectedCoins), len(allCoins))
  2390  			}
  2391  			for i := range allCoins {
  2392  				if len(allCoins[i]) != len(test.expectedCoins[i]) {
  2393  					t.Fatalf("%s: expected %d coins in set %d, got %d", test.name, len(test.expectedCoins[i]), i, len(allCoins[i]))
  2394  				}
  2395  				actual := allCoins[i]
  2396  				expected := test.expectedCoins[i]
  2397  				sort.Slice(actual, func(i, j int) bool {
  2398  					return bytes.Compare(actual[i].ID(), actual[j].ID()) < 0
  2399  				})
  2400  				sort.Slice(expected, func(i, j int) bool {
  2401  					return bytes.Compare(expected[i].ID(), expected[j].ID()) < 0
  2402  				})
  2403  				for j := range actual {
  2404  					if !bytes.Equal(actual[j].ID(), expected[j].ID()) {
  2405  						t.Fatalf("%s: unexpected coin in set %d. expected %s, got %s", test.name, i, expected[j].ID(), actual[j].ID())
  2406  					}
  2407  					if actual[j].Value() != expected[j].Value() {
  2408  						t.Fatalf("%s: unexpected coin value in set %d. expected %d, got %d", test.name, i, expected[j].Value(), actual[j].Value())
  2409  					}
  2410  				}
  2411  			}
  2412  		} else { // expectSplit
  2413  			if node.sentRawTx == nil {
  2414  				t.Fatalf("%s: SendRawTransaction not called", test.name)
  2415  			}
  2416  			if len(node.sentRawTx.TxIn) != len(test.expectedInputs) {
  2417  				t.Fatalf("%s: expected %d inputs, got %d", test.name, len(test.expectedInputs), len(node.sentRawTx.TxIn))
  2418  			}
  2419  			for i, actualIn := range node.sentRawTx.TxIn {
  2420  				expectedIn := test.expectedInputs[i]
  2421  				if !bytes.Equal(actualIn.PreviousOutPoint.Hash[:], expectedIn.PreviousOutPoint.Hash[:]) {
  2422  					t.Fatalf("%s: unexpected input %d hash. expected %s, got %s", test.name, i, expectedIn.PreviousOutPoint.Hash, actualIn.PreviousOutPoint.Hash)
  2423  				}
  2424  				if actualIn.PreviousOutPoint.Index != expectedIn.PreviousOutPoint.Index {
  2425  					t.Fatalf("%s: unexpected input %d index. expected %d, got %d", test.name, i, expectedIn.PreviousOutPoint.Index, actualIn.PreviousOutPoint.Index)
  2426  				}
  2427  			}
  2428  			expectedNumOutputs := len(test.expectedOutputs)
  2429  			if test.expectedChange > 0 {
  2430  				expectedNumOutputs++
  2431  			}
  2432  			if len(node.sentRawTx.TxOut) != expectedNumOutputs {
  2433  				t.Fatalf("%s: expected %d outputs, got %d", test.name, expectedNumOutputs, len(node.sentRawTx.TxOut))
  2434  			}
  2435  
  2436  			for i, expectedOut := range test.expectedOutputs {
  2437  				actualOut := node.sentRawTx.TxOut[i]
  2438  				if actualOut.Value != expectedOut.Value {
  2439  					t.Fatalf("%s: unexpected output %d value. expected %d, got %d", test.name, i, expectedOut.Value, actualOut.Value)
  2440  				}
  2441  			}
  2442  			if test.expectedChange > 0 {
  2443  				actualOut := node.sentRawTx.TxOut[len(node.sentRawTx.TxOut)-1]
  2444  				if uint64(actualOut.Value) != test.expectedChange {
  2445  					t.Fatalf("%s: unexpected change value. expected %d, got %d", test.name, test.expectedChange, actualOut.Value)
  2446  				}
  2447  			}
  2448  
  2449  			if len(test.multiOrder.Values) != len(allCoins) {
  2450  				t.Fatalf("%s: expected %d coins, got %d", test.name, len(test.multiOrder.Values), len(allCoins))
  2451  			}
  2452  			splitTxID := node.sentRawTx.TxHash()
  2453  
  2454  			// This means all coins are split outputs
  2455  			if test.expectedCoins == nil {
  2456  				for i, actualCoin := range allCoins {
  2457  					actualOut := actualCoin[0].(*output)
  2458  					expectedOut := node.sentRawTx.TxOut[i]
  2459  					if uint64(expectedOut.Value) != actualOut.value {
  2460  						t.Fatalf("%s: unexpected output %d value. expected %d, got %d", test.name, i, expectedOut.Value, actualOut.value)
  2461  					}
  2462  					if !bytes.Equal(actualOut.pt.txHash[:], splitTxID[:]) {
  2463  						t.Fatalf("%s: unexpected output %d txid. expected %s, got %s", test.name, i, splitTxID, actualOut.pt.txHash)
  2464  					}
  2465  				}
  2466  			} else {
  2467  				var splitTxOutputIndex int
  2468  				for i := range allCoins {
  2469  					actual := allCoins[i]
  2470  					expected := test.expectedCoins[i]
  2471  
  2472  					// This means the coins are the split outputs
  2473  					if expected == nil {
  2474  						actualOut := actual[0].(*output)
  2475  						expectedOut := node.sentRawTx.TxOut[splitTxOutputIndex]
  2476  						if uint64(expectedOut.Value) != actualOut.value {
  2477  							t.Fatalf("%s: unexpected output %d value. expected %d, got %d", test.name, i, expectedOut.Value, actualOut.value)
  2478  						}
  2479  						if !bytes.Equal(actualOut.pt.txHash[:], splitTxID[:]) {
  2480  							t.Fatalf("%s: unexpected output %d txid. expected %s, got %s", test.name, i, splitTxID, actualOut.pt.txHash)
  2481  						}
  2482  						splitTxOutputIndex++
  2483  						continue
  2484  					}
  2485  
  2486  					if len(actual) != len(expected) {
  2487  						t.Fatalf("%s: expected %d coins in set %d, got %d", test.name, len(test.expectedCoins[i]), i, len(allCoins[i]))
  2488  					}
  2489  					sort.Slice(actual, func(i, j int) bool {
  2490  						return bytes.Compare(actual[i].ID(), actual[j].ID()) < 0
  2491  					})
  2492  					sort.Slice(expected, func(i, j int) bool {
  2493  						return bytes.Compare(expected[i].ID(), expected[j].ID()) < 0
  2494  					})
  2495  					for j := range actual {
  2496  						if !bytes.Equal(actual[j].ID(), expected[j].ID()) {
  2497  							t.Fatalf("%s: unexpected coin in set %d. expected %s, got %s", test.name, i, expected[j].ID(), actual[j].ID())
  2498  						}
  2499  						if actual[j].Value() != expected[j].Value() {
  2500  							t.Fatalf("%s: unexpected coin value in set %d. expected %d, got %d", test.name, i, expected[j].Value(), actual[j].Value())
  2501  						}
  2502  					}
  2503  				}
  2504  			}
  2505  
  2506  			// Each split output should be locked
  2507  			if len(node.lockedCoins) != len(allCoins) {
  2508  				t.Fatalf("%s: expected %d locked coins, got %d", test.name, len(allCoins), len(node.lockedCoins))
  2509  			}
  2510  
  2511  		}
  2512  
  2513  		// Check that the right coins are locked and in the fundingCoins map
  2514  		var totalNumCoins int
  2515  		for _, coins := range allCoins {
  2516  			totalNumCoins += len(coins)
  2517  		}
  2518  		if totalNumCoins != len(wallet.fundingCoins) {
  2519  			t.Fatalf("%s: expected %d funding coins in wallet, got %d", test.name, totalNumCoins, len(wallet.fundingCoins))
  2520  		}
  2521  		//totalNumCoins += len(test.expectedInputs)
  2522  		if totalNumCoins != len(node.lockedCoins) {
  2523  			t.Fatalf("%s: expected %d locked coins, got %d", test.name, totalNumCoins, len(node.lockedCoins))
  2524  		}
  2525  		lockedCoins := make(map[wire.OutPoint]any)
  2526  		for _, coin := range node.lockedCoins {
  2527  			lockedCoins[*coin] = true
  2528  		}
  2529  		checkLockedCoin := func(txHash chainhash.Hash, vout uint32) {
  2530  			if _, ok := lockedCoins[wire.OutPoint{Hash: txHash, Index: vout, Tree: wire.TxTreeRegular}]; !ok {
  2531  				t.Fatalf("%s: expected locked coin %s:%d not found", test.name, txHash, vout)
  2532  			}
  2533  		}
  2534  		checkFundingCoin := func(txHash chainhash.Hash, vout uint32) {
  2535  			if _, ok := wallet.fundingCoins[outPoint{txHash: txHash, vout: vout}]; !ok {
  2536  				t.Fatalf("%s: expected locked coin %s:%d not found in wallet", test.name, txHash, vout)
  2537  			}
  2538  		}
  2539  		for _, coins := range allCoins {
  2540  			for _, coin := range coins {
  2541  				// decode coin to output
  2542  				out := coin.(*output)
  2543  				checkLockedCoin(out.pt.txHash, out.pt.vout)
  2544  				checkFundingCoin(out.pt.txHash, out.pt.vout)
  2545  			}
  2546  		}
  2547  		//for _, expectedIn := range test.expectedInputs {
  2548  		//	checkLockedCoin(expectedIn.PreviousOutPoint.Hash, expectedIn.PreviousOutPoint.Index)
  2549  		//}
  2550  
  2551  		if test.expectedSplitFee != splitFee {
  2552  			t.Fatalf("%s: unexpected split fee. expected %d, got %d", test.name, test.expectedSplitFee, splitFee)
  2553  		}
  2554  	}
  2555  }
  2556  
  2557  func TestFundEdges(t *testing.T) {
  2558  	wallet, node, shutdown := tNewWallet()
  2559  	defer shutdown()
  2560  
  2561  	swapVal := uint64(1e8)
  2562  	lots := swapVal / tLotSize
  2563  
  2564  	// Swap fees
  2565  	//
  2566  	// fee_rate: 24 atoms / byte (dex MaxFeeRate)
  2567  	// swap_size: 251 bytes
  2568  	// swap_size_base: 85 bytes (251 - 166 p2pkh input)
  2569  	// lot_size: 1e7
  2570  	// swap_value: 1e8
  2571  	//  lots = swap_size / lot_size = 10
  2572  	//  base_tx_bytes = (lots - 1) * swap_size + swap_size_base = 9 * 251 + 85 = 2344
  2573  	//  base_fees = 56256
  2574  	//  backing_bytes: 1x P2PKH inputs = dexdcr.P2PKHInputSize = 166 bytes
  2575  	//  backing_fees: 166 * fee_rate(24 atoms/byte) = 3984 atoms
  2576  	//  total_bytes  = base_tx_bytes + backing_bytes = 2344 + 166 = 2510
  2577  	// total_fees: base_fees + backing_fees = 56256 + 3984 = 60240 atoms
  2578  	//          OR total_bytes * fee_rate = 2510 * 24 = 60240
  2579  	// base_best_case_bytes = swap_size_base + (lots - 1) * swap_output_size (P2SHOutputSize) + backing_bytes
  2580  	//                      = 85 + 9*34 + 166 = 557
  2581  	const swapSize = 251
  2582  	const totalBytes = 2510
  2583  	const bestCaseBytes = swapSize
  2584  	const swapOutputSize = 34
  2585  	fees := uint64(totalBytes) * tDCR.MaxFeeRate
  2586  	p2pkhUnspent := walletjson.ListUnspentResult{
  2587  		TxID:          tTxID,
  2588  		Address:       tPKHAddr.String(),
  2589  		Account:       tAcctName,
  2590  		Amount:        float64(swapVal+fees-1) / 1e8, // one atom less than needed
  2591  		Confirmations: 5,
  2592  		ScriptPubKey:  hex.EncodeToString(tP2PKHScript),
  2593  		Spendable:     true,
  2594  	}
  2595  
  2596  	node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent}
  2597  	ord := &asset.Order{
  2598  		Version:       version,
  2599  		Value:         swapVal,
  2600  		MaxSwapCount:  lots,
  2601  		MaxFeeRate:    tDCR.MaxFeeRate,
  2602  		FeeSuggestion: feeSuggestion,
  2603  	}
  2604  
  2605  	// MaxOrder will try a split tx, which will work with one lot less.
  2606  	var feeReduction uint64 = swapSize * tDCR.MaxFeeRate
  2607  	estFeeReduction := swapSize * feeSuggestion
  2608  	splitFees := splitTxBaggage * tDCR.MaxFeeRate
  2609  	checkMaxOrder(t, wallet, lots-1, swapVal-tLotSize,
  2610  		fees+splitFees-feeReduction,                               // max fees
  2611  		(totalBytes+splitTxBaggage)*feeSuggestion-estFeeReduction, // worst case
  2612  		(bestCaseBytes+splitTxBaggage)*feeSuggestion)              // best case
  2613  
  2614  	_, _, _, err := wallet.FundOrder(ord)
  2615  	if err == nil {
  2616  		t.Fatalf("no error when not enough funds in single p2pkh utxo")
  2617  	}
  2618  
  2619  	// Now add the needed atoms and try again. The fees will reflect that the
  2620  	// split was skipped because insufficient available for splitFees.
  2621  	p2pkhUnspent.Amount = float64(swapVal+fees) / 1e8
  2622  	node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent}
  2623  
  2624  	checkMaxOrder(t, wallet, lots, swapVal, fees, totalBytes*feeSuggestion,
  2625  		bestCaseBytes*feeSuggestion)
  2626  
  2627  	_, _, _, err = wallet.FundOrder(ord)
  2628  	if err != nil {
  2629  		t.Fatalf("should be enough to fund with a single p2pkh utxo: %v", err)
  2630  	}
  2631  
  2632  	// For a split transaction, we would need to cover the splitTxBaggage as
  2633  	// well.
  2634  	wallet.config().useSplitTx = true
  2635  	node.newAddr = tPKHAddr
  2636  	node.changeAddr = tPKHAddr
  2637  	node.signFunc = func(msgTx *wire.MsgTx) (*wire.MsgTx, bool, error) {
  2638  		return signFunc(msgTx, dexdcr.P2PKHSigScriptSize)
  2639  	}
  2640  
  2641  	fees = uint64(totalBytes+splitTxBaggage) * tDCR.MaxFeeRate
  2642  	v := swapVal + fees - 1
  2643  	node.unspent[0].Amount = float64(v) / 1e8
  2644  	coins, _, _, err := wallet.FundOrder(ord)
  2645  	if err != nil {
  2646  		t.Fatalf("error when skipping split tx because not enough to cover baggage: %v", err)
  2647  	}
  2648  	if coins[0].Value() != v {
  2649  		t.Fatalf("split performed when baggage wasn't covered")
  2650  	}
  2651  	// Now get the split.
  2652  	v = swapVal + fees
  2653  	node.unspent[0].Amount = float64(v) / 1e8
  2654  
  2655  	checkMaxOrder(t, wallet, lots, swapVal, fees, (totalBytes+splitTxBaggage)*feeSuggestion,
  2656  		(bestCaseBytes+splitTxBaggage)*feeSuggestion) // fees include split (did not fall back to no split)
  2657  
  2658  	coins, _, _, err = wallet.FundOrder(ord)
  2659  	if err != nil {
  2660  		t.Fatalf("error funding split tx: %v", err)
  2661  	}
  2662  	if coins[0].Value() == v {
  2663  		t.Fatalf("split performed when baggage wasn't covered")
  2664  	}
  2665  
  2666  	// Split transactions require a fee suggestion.
  2667  	// TODO:
  2668  	// 1.0: Error when no suggestion.
  2669  	// ord.FeeSuggestion = 0
  2670  	// _, _, err = wallet.FundOrder(ord)
  2671  	// if err == nil {
  2672  	// 	t.Fatalf("no error for no fee suggestions on split tx")
  2673  	// }
  2674  	ord.FeeSuggestion = tDCR.MaxFeeRate + 1
  2675  	_, _, _, err = wallet.FundOrder(ord)
  2676  	if err == nil {
  2677  		t.Fatalf("no error for high fee suggestions on split tx")
  2678  	}
  2679  	// Check success again.
  2680  	ord.FeeSuggestion = tDCR.MaxFeeRate
  2681  	_, _, _, err = wallet.FundOrder(ord)
  2682  	if err != nil {
  2683  		t.Fatalf("error fixing split tx: %v", err)
  2684  	}
  2685  	wallet.config().useSplitTx = false
  2686  
  2687  	// TODO: test version mismatch
  2688  }
  2689  
  2690  func TestSwap(t *testing.T) {
  2691  	wallet, node, shutdown := tNewWallet()
  2692  	defer shutdown()
  2693  
  2694  	swapVal := toAtoms(5)
  2695  	coins := asset.Coins{
  2696  		newOutput(tTxHash, 0, toAtoms(3), wire.TxTreeRegular),
  2697  		newOutput(tTxHash, 0, toAtoms(3), wire.TxTreeRegular),
  2698  	}
  2699  
  2700  	privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330")
  2701  
  2702  	node.changeAddr = tPKHAddr
  2703  	var err error
  2704  	node.privWIF, err = dcrutil.NewWIF(privBytes, tChainParams.PrivateKeyID, dcrec.STEcdsaSecp256k1)
  2705  	if err != nil {
  2706  		t.Fatalf("NewWIF error: %v", err)
  2707  	}
  2708  
  2709  	node.newAddr = tPKHAddr
  2710  	node.changeAddr = tPKHAddr
  2711  
  2712  	secretHash, _ := hex.DecodeString("5124208c80d33507befa517c08ed01aa8d33adbf37ecd70fb5f9352f7a51a88d")
  2713  	contract := &asset.Contract{
  2714  		Address:    tPKHAddr.String(),
  2715  		Value:      swapVal,
  2716  		SecretHash: secretHash,
  2717  		LockTime:   uint64(time.Now().Unix()),
  2718  	}
  2719  
  2720  	swaps := &asset.Swaps{
  2721  		Inputs:     coins,
  2722  		Contracts:  []*asset.Contract{contract},
  2723  		LockChange: true,
  2724  		FeeRate:    tDCR.MaxFeeRate,
  2725  	}
  2726  
  2727  	// Aim for 3 signature cycles.
  2728  	sigSizer := 0
  2729  	signFunc := func(msgTx *wire.MsgTx) (*wire.MsgTx, bool, error) {
  2730  		// Set the sigScripts to random bytes of the correct length for spending a
  2731  		// p2pkh output.
  2732  		scriptSize := dexdcr.P2PKHSigScriptSize
  2733  		// Oscillate the signature size to work the fee optimization loop.
  2734  		if sigSizer%2 == 0 {
  2735  			scriptSize -= 2
  2736  		}
  2737  		sigSizer++
  2738  		return signFunc(msgTx, scriptSize)
  2739  	}
  2740  
  2741  	node.signFunc = signFunc
  2742  	// reset the signFunc after this test so captured variables are free
  2743  	defer func() { node.signFunc = defaultSignFunc }()
  2744  
  2745  	// This time should succeed.
  2746  	_, changeCoin, feesPaid, err := wallet.Swap(swaps)
  2747  	if err != nil {
  2748  		t.Fatalf("swap error: %v", err)
  2749  	}
  2750  
  2751  	// Make sure the change coin is locked.
  2752  	if len(node.lockedCoins) != 1 {
  2753  		t.Fatalf("change coin not locked")
  2754  	}
  2755  	txHash, _, _ := decodeCoinID(changeCoin.ID())
  2756  	if txHash.String() != node.lockedCoins[0].Hash.String() {
  2757  		t.Fatalf("wrong coin locked during swap")
  2758  	}
  2759  
  2760  	// Fees should be returned.
  2761  	minFees := tDCR.MaxFeeRate * uint64(node.sentRawTx.SerializeSize())
  2762  	if feesPaid < minFees {
  2763  		t.Fatalf("sent fees, %d, less than required fees, %d", feesPaid, minFees)
  2764  	}
  2765  
  2766  	// Not enough funds
  2767  	swaps.Inputs = coins[:1]
  2768  	_, _, _, err = wallet.Swap(swaps)
  2769  	if err == nil {
  2770  		t.Fatalf("no error for listunspent not enough funds")
  2771  	}
  2772  	swaps.Inputs = coins
  2773  
  2774  	// AddressPKH error
  2775  	node.newAddrErr = tErr
  2776  	_, _, _, err = wallet.Swap(swaps)
  2777  	if err == nil {
  2778  		t.Fatalf("no error for getnewaddress rpc error")
  2779  	}
  2780  	node.newAddrErr = nil
  2781  
  2782  	// ChangeAddress error
  2783  	node.changeAddrErr = tErr
  2784  	_, _, _, err = wallet.Swap(swaps)
  2785  	if err == nil {
  2786  		t.Fatalf("no error for getrawchangeaddress rpc error")
  2787  	}
  2788  	node.changeAddrErr = nil
  2789  
  2790  	// SignTx error
  2791  	node.signFunc = func(msgTx *wire.MsgTx) (*wire.MsgTx, bool, error) {
  2792  		return nil, false, tErr
  2793  	}
  2794  	_, _, _, err = wallet.Swap(swaps)
  2795  	if err == nil {
  2796  		t.Fatalf("no error for signrawtransactionwithwallet rpc error")
  2797  	}
  2798  
  2799  	// incomplete signatures
  2800  	node.signFunc = func(msgTx *wire.MsgTx) (*wire.MsgTx, bool, error) {
  2801  		return msgTx, false, nil
  2802  	}
  2803  	_, _, _, err = wallet.Swap(swaps)
  2804  	if err == nil {
  2805  		t.Fatalf("no error for incomplete signature rpc error")
  2806  	}
  2807  	node.signFunc = signFunc
  2808  
  2809  	// Make sure we can succeed again.
  2810  	_, _, _, err = wallet.Swap(swaps)
  2811  	if err != nil {
  2812  		t.Fatalf("re-swap error: %v", err)
  2813  	}
  2814  }
  2815  
  2816  type TAuditInfo struct{}
  2817  
  2818  func (ai *TAuditInfo) Recipient() string     { return tPKHAddr.String() }
  2819  func (ai *TAuditInfo) Expiration() time.Time { return time.Time{} }
  2820  func (ai *TAuditInfo) Coin() asset.Coin      { return &tCoin{} }
  2821  func (ai *TAuditInfo) Contract() dex.Bytes   { return nil }
  2822  func (ai *TAuditInfo) SecretHash() dex.Bytes { return nil }
  2823  
  2824  func TestRedeem(t *testing.T) {
  2825  	wallet, node, shutdown := tNewWallet()
  2826  	defer shutdown()
  2827  
  2828  	swapVal := toAtoms(5)
  2829  	secret := randBytes(32)
  2830  	secretHash := sha256.Sum256(secret)
  2831  	lockTime := time.Now().Add(time.Hour * 12)
  2832  	addr := tPKHAddr.String()
  2833  
  2834  	contract, err := dexdcr.MakeContract(addr, addr, secretHash[:], lockTime.Unix(), tChainParams)
  2835  	if err != nil {
  2836  		t.Fatalf("error making swap contract: %v", err)
  2837  	}
  2838  
  2839  	coin := newOutput(tTxHash, 0, swapVal, wire.TxTreeRegular)
  2840  
  2841  	ci := &asset.AuditInfo{
  2842  		Coin:       coin,
  2843  		Contract:   contract,
  2844  		Recipient:  tPKHAddr.String(),
  2845  		Expiration: lockTime,
  2846  	}
  2847  
  2848  	redemption := &asset.Redemption{
  2849  		Spends: ci,
  2850  		Secret: secret,
  2851  	}
  2852  
  2853  	privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330")
  2854  
  2855  	node.newAddr = tPKHAddr
  2856  	node.privWIF, err = dcrutil.NewWIF(privBytes, tChainParams.PrivateKeyID, dcrec.STEcdsaSecp256k1)
  2857  	if err != nil {
  2858  		t.Fatalf("NewWIF error: %v", err)
  2859  	}
  2860  
  2861  	redemptions := &asset.RedeemForm{
  2862  		Redemptions: []*asset.Redemption{redemption},
  2863  	}
  2864  
  2865  	_, _, feesPaid, err := wallet.Redeem(redemptions)
  2866  	if err != nil {
  2867  		t.Fatalf("redeem error: %v", err)
  2868  	}
  2869  
  2870  	// Check that fees are returned.
  2871  	minFees := optimalFeeRate * uint64(node.sentRawTx.SerializeSize())
  2872  	if feesPaid < minFees {
  2873  		t.Fatalf("sent fees, %d, less than expected minimum fees, %d", feesPaid, minFees)
  2874  	}
  2875  
  2876  	// No audit info
  2877  	redemption.Spends = nil
  2878  	_, _, _, err = wallet.Redeem(redemptions)
  2879  	if err == nil {
  2880  		t.Fatalf("no error for nil AuditInfo")
  2881  	}
  2882  	redemption.Spends = ci
  2883  
  2884  	// Spoofing AuditInfo is not allowed.
  2885  	redemption.Spends = &asset.AuditInfo{}
  2886  	_, _, _, err = wallet.Redeem(redemptions)
  2887  	if err == nil {
  2888  		t.Fatalf("no error for spoofed AuditInfo")
  2889  	}
  2890  	redemption.Spends = ci
  2891  
  2892  	// Wrong secret hash
  2893  	redemption.Secret = randBytes(32)
  2894  	_, _, _, err = wallet.Redeem(redemptions)
  2895  	if err == nil {
  2896  		t.Fatalf("no error for wrong secret")
  2897  	}
  2898  	redemption.Secret = secret
  2899  
  2900  	// too low of value
  2901  	coin.value = 200
  2902  	_, _, _, err = wallet.Redeem(redemptions)
  2903  	if err == nil {
  2904  		t.Fatalf("no error for redemption not worth the fees")
  2905  	}
  2906  	coin.value = swapVal
  2907  
  2908  	// New address error
  2909  	node.newAddrErr = tErr
  2910  	_, _, _, err = wallet.Redeem(redemptions)
  2911  	if err == nil {
  2912  		t.Fatalf("no error for new address error")
  2913  	}
  2914  
  2915  	// Change address error
  2916  	node.changeAddrErr = tErr
  2917  	_, _, _, err = wallet.Redeem(redemptions)
  2918  	if err == nil {
  2919  		t.Fatalf("no error for change address error")
  2920  	}
  2921  	node.changeAddrErr = nil
  2922  
  2923  	// Missing priv key error
  2924  	node.privWIFErr = tErr
  2925  	_, _, _, err = wallet.Redeem(redemptions)
  2926  	if err == nil {
  2927  		t.Fatalf("no error for missing private key")
  2928  	}
  2929  	node.privWIFErr = nil
  2930  
  2931  	// Send error
  2932  	node.sendRawErr = tErr
  2933  	_, _, _, err = wallet.Redeem(redemptions)
  2934  	if err == nil {
  2935  		t.Fatalf("no error for send error")
  2936  	}
  2937  	node.sendRawErr = nil
  2938  
  2939  	// Wrong hash
  2940  	var h chainhash.Hash
  2941  	h[0] = 0x01
  2942  	node.sendRawHash = &h
  2943  	_, _, _, err = wallet.Redeem(redemptions)
  2944  	if err == nil {
  2945  		t.Fatalf("no error for wrong return hash")
  2946  	}
  2947  	node.sendRawHash = nil
  2948  }
  2949  
  2950  const (
  2951  	txCatReceive = "recv"
  2952  	txCatSend    = "send"
  2953  	//txCatGenerate = "generate"
  2954  )
  2955  
  2956  func TestSignMessage(t *testing.T) {
  2957  	wallet, node, shutdown := tNewWallet()
  2958  	defer shutdown()
  2959  
  2960  	vout := uint32(5)
  2961  	privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330")
  2962  	privKey := secp256k1.PrivKeyFromBytes(privBytes)
  2963  	pubKey := privKey.PubKey()
  2964  
  2965  	msg := randBytes(36)
  2966  	pk := pubKey.SerializeCompressed()
  2967  	msgHash := chainhash.HashB(msg)
  2968  	signature := ecdsa.Sign(privKey, msgHash)
  2969  	sig := signature.Serialize()
  2970  
  2971  	var err error
  2972  	node.privWIF, err = dcrutil.NewWIF(privBytes, tChainParams.PrivateKeyID, dcrec.STEcdsaSecp256k1)
  2973  	if err != nil {
  2974  		t.Fatalf("NewWIF error: %v", err)
  2975  	}
  2976  
  2977  	op := newOutput(tTxHash, vout, 5e7, wire.TxTreeRegular)
  2978  
  2979  	wallet.fundingCoins[op.pt] = &fundingCoin{
  2980  		addr: tPKHAddr.String(),
  2981  	}
  2982  
  2983  	check := func() {
  2984  		pubkeys, sigs, err := wallet.SignMessage(op, msg)
  2985  		if err != nil {
  2986  			t.Fatalf("SignMessage error: %v", err)
  2987  		}
  2988  		if len(pubkeys) != 1 {
  2989  			t.Fatalf("expected 1 pubkey, received %d", len(pubkeys))
  2990  		}
  2991  		if len(sigs) != 1 {
  2992  			t.Fatalf("expected 1 sig, received %d", len(sigs))
  2993  		}
  2994  		if !bytes.Equal(pk, pubkeys[0]) {
  2995  			t.Fatalf("wrong pubkey. expected %x, got %x", pubkeys[0], pk)
  2996  		}
  2997  		if !bytes.Equal(sig, sigs[0]) {
  2998  			t.Fatalf("wrong signature. exptected %x, got %x", sigs[0], sig)
  2999  		}
  3000  	}
  3001  
  3002  	check()
  3003  	delete(wallet.fundingCoins, op.pt)
  3004  	txOut := makeGetTxOutRes(0, 5, nil)
  3005  	txOut.ScriptPubKey.Addresses = []string{tPKHAddr.String()}
  3006  	node.txOutRes[newOutPoint(tTxHash, vout)] = txOut
  3007  	check()
  3008  
  3009  	// gettxout error
  3010  	node.txOutErr = tErr
  3011  	_, _, err = wallet.SignMessage(op, msg)
  3012  	if err == nil {
  3013  		t.Fatalf("no error for gettxout rpc error")
  3014  	}
  3015  	node.txOutErr = nil
  3016  
  3017  	// dumpprivkey error
  3018  	node.privWIFErr = tErr
  3019  	_, _, err = wallet.SignMessage(op, msg)
  3020  	if err == nil {
  3021  		t.Fatalf("no error for dumpprivkey rpc error")
  3022  	}
  3023  	node.privWIFErr = nil
  3024  
  3025  	// bad coin
  3026  	badCoin := &tCoin{id: make([]byte, 15)}
  3027  	_, _, err = wallet.SignMessage(badCoin, msg)
  3028  	if err == nil {
  3029  		t.Fatalf("no error for bad coin")
  3030  	}
  3031  }
  3032  
  3033  func TestAuditContract(t *testing.T) {
  3034  	wallet, _, shutdown := tNewWallet()
  3035  	defer shutdown()
  3036  
  3037  	secretHash, _ := hex.DecodeString("5124208c80d33507befa517c08ed01aa8d33adbf37ecd70fb5f9352f7a51a88d")
  3038  	lockTime := time.Now().Add(time.Hour * 12)
  3039  	addrStr := tPKHAddr.String()
  3040  	contract, err := dexdcr.MakeContract(addrStr, addrStr, secretHash, lockTime.Unix(), tChainParams)
  3041  	if err != nil {
  3042  		t.Fatalf("error making swap contract: %v", err)
  3043  	}
  3044  	addr, _ := stdaddr.NewAddressScriptHashV0(contract, tChainParams)
  3045  	_, pkScript := addr.PaymentScript()
  3046  
  3047  	// Prepare the contract tx data.
  3048  	contractTx := wire.NewMsgTx()
  3049  	contractTx.AddTxIn(&wire.TxIn{})
  3050  	contractTx.AddTxOut(&wire.TxOut{
  3051  		Value:    5 * int64(tLotSize),
  3052  		PkScript: pkScript,
  3053  	})
  3054  	contractTxData, err := contractTx.Bytes()
  3055  	if err != nil {
  3056  		t.Fatalf("error preparing contract txdata: %v", err)
  3057  	}
  3058  
  3059  	contractHash := contractTx.TxHash()
  3060  	contractVout := uint32(0)
  3061  	contractCoinID := toCoinID(&contractHash, contractVout)
  3062  
  3063  	audit, err := wallet.AuditContract(contractCoinID, contract, contractTxData, true)
  3064  	if err != nil {
  3065  		t.Fatalf("audit error: %v", err)
  3066  	}
  3067  	if audit.Recipient != addrStr {
  3068  		t.Fatalf("wrong recipient. wanted '%s', got '%s'", addrStr, audit.Recipient)
  3069  	}
  3070  	if !bytes.Equal(audit.Contract, contract) {
  3071  		t.Fatalf("contract not set to coin redeem script")
  3072  	}
  3073  	if audit.Expiration.Equal(lockTime) {
  3074  		t.Fatalf("wrong lock time. wanted %d, got %d", lockTime.Unix(), audit.Expiration.Unix())
  3075  	}
  3076  
  3077  	// Invalid txid
  3078  	_, err = wallet.AuditContract(make([]byte, 15), contract, contractTxData, false)
  3079  	if err == nil {
  3080  		t.Fatalf("no error for bad txid")
  3081  	}
  3082  
  3083  	// Wrong contract
  3084  	pkh, _ := hex.DecodeString("c6a704f11af6cbee8738ff19fc28cdc70aba0b82")
  3085  	wrongAddr, _ := stdaddr.NewAddressPubKeyHashEcdsaSecp256k1V0(pkh, tChainParams)
  3086  	wrongAddrStr := wrongAddr.String()
  3087  	wrongContract, err := dexdcr.MakeContract(wrongAddrStr, wrongAddrStr, secretHash, lockTime.Unix(), tChainParams)
  3088  	if err != nil {
  3089  		t.Fatalf("error making wrong swap contract: %v", err)
  3090  	}
  3091  	_, err = wallet.AuditContract(contractCoinID, wrongContract, contractTxData, false)
  3092  	if err == nil {
  3093  		t.Fatalf("no error for wrong contract")
  3094  	}
  3095  
  3096  	// Invalid contract
  3097  	_, wrongPkScript := wrongAddr.PaymentScript()
  3098  	_, err = wallet.AuditContract(contractCoinID, wrongPkScript, contractTxData, false) // addrPkScript not a valid contract
  3099  	if err == nil {
  3100  		t.Fatalf("no error for invalid contract")
  3101  	}
  3102  
  3103  	// No txdata
  3104  	_, err = wallet.AuditContract(contractCoinID, contract, nil, false)
  3105  	if err == nil {
  3106  		t.Fatalf("no error for no txdata")
  3107  	}
  3108  
  3109  	// Invalid txdata, zero inputs
  3110  	contractTx.TxIn = nil
  3111  	invalidContractTxData, err := contractTx.Bytes()
  3112  	if err != nil {
  3113  		t.Fatalf("error preparing invalid contract txdata: %v", err)
  3114  	}
  3115  	_, err = wallet.AuditContract(contractCoinID, contract, invalidContractTxData, false)
  3116  	if err == nil {
  3117  		t.Fatalf("no error for unknown txout")
  3118  	}
  3119  
  3120  	// Wrong txdata, wrong output script
  3121  	wrongContractTx := wire.NewMsgTx()
  3122  	wrongContractTx.AddTxIn(&wire.TxIn{})
  3123  	wrongContractTx.AddTxOut(&wire.TxOut{
  3124  		Value:    5 * int64(tLotSize),
  3125  		PkScript: wrongPkScript,
  3126  	})
  3127  	wrongContractTxData, err := wrongContractTx.Bytes()
  3128  	if err != nil {
  3129  		t.Fatalf("error preparing wrong contract txdata: %v", err)
  3130  	}
  3131  	_, err = wallet.AuditContract(contractCoinID, contract, wrongContractTxData, false)
  3132  	if err == nil {
  3133  		t.Fatalf("no error for unknown txout")
  3134  	}
  3135  }
  3136  
  3137  type tReceipt struct {
  3138  	coin       *tCoin
  3139  	contract   []byte
  3140  	expiration uint64
  3141  }
  3142  
  3143  func (r *tReceipt) Expiration() time.Time { return time.Unix(int64(r.expiration), 0).UTC() }
  3144  func (r *tReceipt) Coin() asset.Coin      { return r.coin }
  3145  func (r *tReceipt) Contract() dex.Bytes   { return r.contract }
  3146  
  3147  func TestFindRedemption(t *testing.T) {
  3148  	wallet, node, shutdown := tNewWallet()
  3149  	defer shutdown()
  3150  
  3151  	_, bestBlockHeight, err := node.GetBestBlock(context.Background())
  3152  	if err != nil {
  3153  		t.Fatalf("unexpected GetBestBlock error: %v", err)
  3154  	}
  3155  
  3156  	contractHeight := bestBlockHeight + 1
  3157  	contractVout := uint32(1)
  3158  
  3159  	secret := randBytes(32)
  3160  	secretHash := sha256.Sum256(secret)
  3161  	lockTime := time.Now().Add(time.Hour * 12)
  3162  	addrStr := tPKHAddr.String()
  3163  	contract, err := dexdcr.MakeContract(addrStr, addrStr, secretHash[:], lockTime.Unix(), tChainParams)
  3164  	if err != nil {
  3165  		t.Fatalf("error making swap contract: %v", err)
  3166  	}
  3167  	contractAddr, _ := stdaddr.NewAddressScriptHashV0(contract, tChainParams)
  3168  	_, contractP2SHScript := contractAddr.PaymentScript()
  3169  
  3170  	tPKHAddrV3, _ := stdaddr.DecodeAddress(tPKHAddr.String(), tChainParams)
  3171  	_, otherScript := tPKHAddrV3.PaymentScript()
  3172  
  3173  	redemptionScript, _ := dexdcr.RedeemP2SHContract(contract, randBytes(73), randBytes(33), secret)
  3174  	otherSpendScript, _ := txscript.NewScriptBuilder().
  3175  		AddData(randBytes(73)).
  3176  		AddData(randBytes(33)).
  3177  		Script()
  3178  
  3179  	// Prepare and add the contract transaction to the blockchain. Put the pay-to-contract script at index 1.
  3180  	inputs := []*wire.TxIn{makeRPCVin(&chainhash.Hash{}, 0, otherSpendScript)}
  3181  	outputScripts := []dex.Bytes{otherScript, contractP2SHScript}
  3182  	contractTx := makeRawTx(inputs, outputScripts)
  3183  	contractTxHash := contractTx.TxHash()
  3184  	coinID := toCoinID(&contractTxHash, contractVout)
  3185  	blockHash, _ := node.blockchain.addRawTx(contractHeight, contractTx)
  3186  	txHex, err := makeTxHex(inputs, outputScripts)
  3187  	if err != nil {
  3188  		t.Fatalf("error generating hex for contract tx: %v", err)
  3189  	}
  3190  	walletTx := &walletjson.GetTransactionResult{
  3191  		BlockHash:     blockHash.String(),
  3192  		Confirmations: 1,
  3193  		Details: []walletjson.GetTransactionDetailsResult{
  3194  			{
  3195  				Address:  contractAddr.String(),
  3196  				Category: txCatSend,
  3197  				Vout:     contractVout,
  3198  			},
  3199  		},
  3200  		Hex: txHex,
  3201  	}
  3202  
  3203  	// Add an intermediate block for good measure.
  3204  	node.blockchain.addRawTx(contractHeight+1, dummyTx())
  3205  
  3206  	// Prepare the redemption tx inputs including an input that spends the contract output.
  3207  	inputs = append(inputs, makeRPCVin(&contractTxHash, contractVout, redemptionScript))
  3208  
  3209  	// Add the redemption to mempool and check if wallet.FindRedemption finds it.
  3210  	redeemTx := makeRawTx(inputs, []dex.Bytes{otherScript})
  3211  	node.blockchain.addRawTx(-1, redeemTx)
  3212  	_, checkSecret, err := wallet.FindRedemption(tCtx, coinID, nil)
  3213  	if err != nil {
  3214  		t.Fatalf("error finding redemption: %v", err)
  3215  	}
  3216  	if !bytes.Equal(checkSecret, secret) {
  3217  		t.Fatalf("wrong secret. expected %x, got %x", secret, checkSecret)
  3218  	}
  3219  
  3220  	node.walletTxFn = func() (*walletjson.GetTransactionResult, error) {
  3221  		return walletTx, nil
  3222  	}
  3223  
  3224  	// Move the redemption to a new block and check if wallet.FindRedemption finds it.
  3225  	_, redeemBlock := node.blockchain.addRawTx(contractHeight+2, makeRawTx(inputs, []dex.Bytes{otherScript}))
  3226  	_, checkSecret, err = wallet.FindRedemption(tCtx, coinID, nil)
  3227  	if err != nil {
  3228  		t.Fatalf("error finding redemption: %v", err)
  3229  	}
  3230  	if !bytes.Equal(checkSecret, secret) {
  3231  		t.Fatalf("wrong secret. expected %x, got %x", secret, checkSecret)
  3232  	}
  3233  
  3234  	// gettransaction error
  3235  	node.walletTxFn = func() (*walletjson.GetTransactionResult, error) {
  3236  		return walletTx, tErr
  3237  	}
  3238  	_, _, err = wallet.FindRedemption(tCtx, coinID, nil)
  3239  	if err == nil {
  3240  		t.Fatalf("no error for gettransaction rpc error")
  3241  	}
  3242  	node.walletTxFn = func() (*walletjson.GetTransactionResult, error) {
  3243  		return walletTx, nil
  3244  	}
  3245  
  3246  	// getcfilterv2 error
  3247  	node.rawErr[methodGetCFilterV2] = tErr
  3248  	_, _, err = wallet.FindRedemption(tCtx, coinID, nil)
  3249  	if err == nil {
  3250  		t.Fatalf("no error for getcfilterv2 rpc error")
  3251  	}
  3252  	delete(node.rawErr, methodGetCFilterV2)
  3253  
  3254  	// missing redemption
  3255  	redeemBlock.Transactions[0].TxIn[1].PreviousOutPoint.Hash = chainhash.Hash{}
  3256  	ctx, cancel := context.WithTimeout(tCtx, 2*time.Second)
  3257  	defer cancel() // ctx should auto-cancel after 2 seconds, but this is apparently good practice to prevent leak
  3258  	_, k, err := wallet.FindRedemption(ctx, coinID, nil)
  3259  	if ctx.Err() == nil || k != nil {
  3260  		// Expected ctx to cancel after timeout and no secret should be found.
  3261  		t.Fatalf("unexpected result for missing redemption: secret: %v, err: %v", k, err)
  3262  	}
  3263  	redeemBlock.Transactions[0].TxIn[1].PreviousOutPoint.Hash = contractTxHash
  3264  
  3265  	// Canceled context
  3266  	deadCtx, cancelCtx := context.WithCancel(tCtx)
  3267  	cancelCtx()
  3268  	_, _, err = wallet.FindRedemption(deadCtx, coinID, nil)
  3269  	if err == nil {
  3270  		t.Fatalf("no error for canceled context")
  3271  	}
  3272  
  3273  	// Expect FindRedemption to error because of bad input sig.
  3274  	redeemBlock.Transactions[0].TxIn[1].SignatureScript = randBytes(100)
  3275  	_, _, err = wallet.FindRedemption(tCtx, coinID, nil)
  3276  	if err == nil {
  3277  		t.Fatalf("no error for wrong redemption")
  3278  	}
  3279  	redeemBlock.Transactions[0].TxIn[1].SignatureScript = redemptionScript
  3280  
  3281  	// Wrong script type for output
  3282  	walletTx.Hex, _ = makeTxHex(inputs, []dex.Bytes{otherScript, otherScript})
  3283  	_, _, err = wallet.FindRedemption(tCtx, coinID, nil)
  3284  	if err == nil {
  3285  		t.Fatalf("no error for wrong script type")
  3286  	}
  3287  	walletTx.Hex = txHex
  3288  
  3289  	// Sanity check to make sure it passes again.
  3290  	_, _, err = wallet.FindRedemption(tCtx, coinID, nil)
  3291  	if err != nil {
  3292  		t.Fatalf("error after clearing errors: %v", err)
  3293  	}
  3294  }
  3295  
  3296  func TestRefund(t *testing.T) {
  3297  	wallet, node, shutdown := tNewWallet()
  3298  	defer shutdown()
  3299  
  3300  	secret := randBytes(32)
  3301  	secretHash := sha256.Sum256(secret)
  3302  	lockTime := time.Now().Add(time.Hour * 12)
  3303  	addrStr := tPKHAddr.String()
  3304  	contract, err := dexdcr.MakeContract(addrStr, addrStr, secretHash[:], lockTime.Unix(), tChainParams)
  3305  	if err != nil {
  3306  		t.Fatalf("error making swap contract: %v", err)
  3307  	}
  3308  	const feeSuggestion = 100
  3309  
  3310  	tipHash, tipHeight := node.getBestBlock()
  3311  	var confs int64 = 1
  3312  	if tipHeight > 1 {
  3313  		confs = 2
  3314  	}
  3315  
  3316  	bigTxOut := makeGetTxOutRes(confs, 5, nil)
  3317  	bigOutID := newOutPoint(tTxHash, 0)
  3318  	node.txOutRes[bigOutID] = bigTxOut
  3319  	node.txOutRes[bigOutID].BestBlock = tipHash.String() // required to calculate the block for the output
  3320  	node.changeAddr = tPKHAddr
  3321  	node.newAddr = tPKHAddr
  3322  
  3323  	privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330")
  3324  	node.privWIF, err = dcrutil.NewWIF(privBytes, tChainParams.PrivateKeyID, dcrec.STEcdsaSecp256k1)
  3325  	if err != nil {
  3326  		t.Fatalf("NewWIF error: %v", err)
  3327  	}
  3328  
  3329  	contractOutput := newOutput(tTxHash, 0, 1e8, wire.TxTreeRegular)
  3330  	_, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion)
  3331  	if err != nil {
  3332  		t.Fatalf("refund error: %v", err)
  3333  	}
  3334  
  3335  	// Invalid coin
  3336  	badReceipt := &tReceipt{
  3337  		coin: &tCoin{id: make([]byte, 15)},
  3338  	}
  3339  	_, err = wallet.Refund(badReceipt.coin.id, badReceipt.contract, feeSuggestion)
  3340  	if err == nil {
  3341  		t.Fatalf("no error for bad receipt")
  3342  	}
  3343  
  3344  	// gettxout error
  3345  	node.txOutErr = tErr
  3346  	_, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion)
  3347  	if err == nil {
  3348  		t.Fatalf("no error for missing utxo")
  3349  	}
  3350  	node.txOutErr = nil
  3351  
  3352  	// bad contract
  3353  	badContractOutput := newOutput(tTxHash, 0, 1e8, wire.TxTreeRegular)
  3354  	_, err = wallet.Refund(badContractOutput.ID(), randBytes(50), feeSuggestion)
  3355  	if err == nil {
  3356  		t.Fatalf("no error for bad contract")
  3357  	}
  3358  
  3359  	// Too small.
  3360  	node.txOutRes[bigOutID] = newTxOutResult(nil, 100, 2)
  3361  	_, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion)
  3362  	if err == nil {
  3363  		t.Fatalf("no error for value < fees")
  3364  	}
  3365  	node.txOutRes[bigOutID] = bigTxOut
  3366  
  3367  	// signature error
  3368  	node.privWIFErr = tErr
  3369  	_, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion)
  3370  	if err == nil {
  3371  		t.Fatalf("no error for dumpprivkey rpc error")
  3372  	}
  3373  	node.privWIFErr = nil
  3374  
  3375  	// send error
  3376  	node.sendRawErr = tErr
  3377  	_, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion)
  3378  	if err == nil {
  3379  		t.Fatalf("no error for sendrawtransaction rpc error")
  3380  	}
  3381  	node.sendRawErr = nil
  3382  
  3383  	// bad checkhash
  3384  	var badHash chainhash.Hash
  3385  	badHash[0] = 0x05
  3386  	node.sendRawHash = &badHash
  3387  	_, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion)
  3388  	if err == nil {
  3389  		t.Fatalf("no error for tx hash")
  3390  	}
  3391  	node.sendRawHash = nil
  3392  
  3393  	// Sanity check that we can succeed again.
  3394  	_, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion)
  3395  	if err != nil {
  3396  		t.Fatalf("re-refund error: %v", err)
  3397  	}
  3398  }
  3399  
  3400  type tSenderType byte
  3401  
  3402  const (
  3403  	tSendSender tSenderType = iota
  3404  	tWithdrawSender
  3405  )
  3406  
  3407  func testSender(t *testing.T, senderType tSenderType) {
  3408  	wallet, node, shutdown := tNewWallet()
  3409  	defer shutdown()
  3410  
  3411  	var sendVal uint64 = 1e8
  3412  	var unspentVal uint64 = 100e8
  3413  	const feeSuggestion = 100
  3414  	funName := "Send"
  3415  	sender := func(addr string, val uint64) (asset.Coin, error) {
  3416  		return wallet.Send(addr, val, feeSuggestion)
  3417  	}
  3418  	if senderType == tWithdrawSender {
  3419  		funName = "Withdraw"
  3420  		// For withdraw, test with unspent total = withdraw value
  3421  		unspentVal = sendVal
  3422  		sender = func(addr string, val uint64) (asset.Coin, error) {
  3423  			return wallet.Withdraw(addr, val, feeSuggestion)
  3424  		}
  3425  	}
  3426  
  3427  	addr := tPKHAddr.String()
  3428  	node.changeAddr = tPKHAddr
  3429  
  3430  	node.unspent = []walletjson.ListUnspentResult{{
  3431  		TxID:          tTxID,
  3432  		Address:       tPKHAddr.String(),
  3433  		Account:       tAcctName,
  3434  		Amount:        float64(unspentVal) / 1e8,
  3435  		Confirmations: 5,
  3436  		ScriptPubKey:  hex.EncodeToString(tP2PKHScript),
  3437  		Spendable:     true,
  3438  	}}
  3439  	//node.unspent = append(node.unspent, node.unspent[0])
  3440  
  3441  	_, err := sender(addr, sendVal)
  3442  	if err != nil {
  3443  		t.Fatalf(funName+" error: %v", err)
  3444  	}
  3445  
  3446  	// invalid address
  3447  	_, err = sender("badaddr", sendVal)
  3448  	if err == nil {
  3449  		t.Fatalf("no error for bad address: %v", err)
  3450  	}
  3451  
  3452  	// GetRawChangeAddress error
  3453  	if senderType == tSendSender { // withdraw test does not get a change address
  3454  		node.changeAddrErr = tErr
  3455  		_, err = sender(addr, sendVal)
  3456  		if err == nil {
  3457  			t.Fatalf("no error for rawchangeaddress: %v", err)
  3458  		}
  3459  		node.changeAddrErr = nil
  3460  	}
  3461  
  3462  	// good again
  3463  	_, err = sender(addr, sendVal)
  3464  	if err != nil {
  3465  		t.Fatalf(funName+" error afterwards: %v", err)
  3466  	}
  3467  }
  3468  
  3469  func TestWithdraw(t *testing.T) {
  3470  	testSender(t, tWithdrawSender)
  3471  }
  3472  
  3473  func TestSend(t *testing.T) {
  3474  	testSender(t, tSendSender)
  3475  }
  3476  
  3477  func Test_withdraw(t *testing.T) {
  3478  	wallet, node, shutdown := tNewWallet()
  3479  	defer shutdown()
  3480  
  3481  	address := tPKHAddr.String()
  3482  	node.changeAddr = tPKHAddr
  3483  
  3484  	var unspentVal uint64 = 100e8
  3485  	node.unspent = []walletjson.ListUnspentResult{{
  3486  		TxID:          tTxID,
  3487  		Address:       tPKHAddr.String(),
  3488  		Account:       tAcctName,
  3489  		Amount:        float64(unspentVal) / 1e8,
  3490  		Confirmations: 5,
  3491  		ScriptPubKey:  hex.EncodeToString(tP2PKHScript),
  3492  		Spendable:     true,
  3493  	}}
  3494  
  3495  	addr, err := stdaddr.DecodeAddress(address, tChainParams)
  3496  	if err != nil {
  3497  		t.Fatal(err)
  3498  	}
  3499  
  3500  	// This should make a msgTx with one input and one output.
  3501  	msgTx, val, err := wallet.withdraw(addr, unspentVal, optimalFeeRate)
  3502  	if err != nil {
  3503  		t.Fatal(err)
  3504  	}
  3505  	if len(msgTx.TxOut) != 1 {
  3506  		t.Fatalf("expected 1 output, got %d", len(msgTx.TxOut))
  3507  	}
  3508  	if val != uint64(msgTx.TxOut[0].Value) {
  3509  		t.Errorf("expected non-change output to be %d, got %d", val, msgTx.TxOut[0].Value)
  3510  	}
  3511  	if val >= unspentVal {
  3512  		t.Errorf("expected output to be have fees deducted")
  3513  	}
  3514  
  3515  	// Then with unspentVal just slightly larger than send. This should still
  3516  	// make a msgTx with one output, but larger than before. The sent value is
  3517  	// SMALLER than requested because it was required for fees.
  3518  	avail := unspentVal + 77
  3519  	node.unspent[0].Amount = float64(avail) / 1e8
  3520  	msgTx, val, err = wallet.withdraw(addr, unspentVal, optimalFeeRate)
  3521  	if err != nil {
  3522  		t.Fatal(err)
  3523  	}
  3524  	if len(msgTx.TxOut) != 1 {
  3525  		t.Fatalf("expected 1 output, got %d", len(msgTx.TxOut))
  3526  	}
  3527  	if val != uint64(msgTx.TxOut[0].Value) {
  3528  		t.Errorf("expected non-change output to be %d, got %d", val, msgTx.TxOut[0].Value)
  3529  	}
  3530  	if val >= unspentVal {
  3531  		t.Errorf("expected output to be have fees deducted")
  3532  	}
  3533  
  3534  	// Still no change, but this time the sent value is LARGER than requested
  3535  	// because change would be dust, and we don't over pay fees.
  3536  	avail = unspentVal + 3000
  3537  	node.unspent[0].Amount = float64(avail) / 1e8
  3538  	msgTx, val, err = wallet.withdraw(addr, unspentVal, optimalFeeRate)
  3539  	if err != nil {
  3540  		t.Fatal(err)
  3541  	}
  3542  	if len(msgTx.TxOut) != 1 {
  3543  		t.Fatalf("expected 1 output, got %d", len(msgTx.TxOut))
  3544  	}
  3545  	if val <= unspentVal {
  3546  		t.Errorf("expected output to be more thrifty")
  3547  	}
  3548  
  3549  	// Then with unspentVal considerably larger than (double) send. This should
  3550  	// make a msgTx with two outputs as the change is no longer dust. The change
  3551  	// should be exactly unspentVal and the sent amount should be
  3552  	// unspentVal-fees.
  3553  	node.unspent[0].Amount = float64(unspentVal*2) / 1e8
  3554  	msgTx, val, err = wallet.withdraw(addr, unspentVal, optimalFeeRate)
  3555  	if err != nil {
  3556  		t.Fatal(err)
  3557  	}
  3558  	if len(msgTx.TxOut) != 2 {
  3559  		t.Fatalf("expected 2 outputs, got %d", len(msgTx.TxOut))
  3560  	}
  3561  	if val != uint64(msgTx.TxOut[0].Value) {
  3562  		t.Errorf("expected non-change output to be %d, got %d", val, msgTx.TxOut[0].Value)
  3563  	}
  3564  	if unspentVal != uint64(msgTx.TxOut[1].Value) {
  3565  		t.Errorf("expected change output to be %d, got %d", unspentVal, msgTx.TxOut[1].Value)
  3566  	}
  3567  }
  3568  
  3569  func Test_sendToAddress(t *testing.T) {
  3570  	wallet, node, shutdown := tNewWallet()
  3571  	defer shutdown()
  3572  
  3573  	address := tPKHAddr.String()
  3574  	node.changeAddr = tPKHAddr
  3575  
  3576  	var unspentVal uint64 = 100e8
  3577  	node.unspent = []walletjson.ListUnspentResult{{
  3578  		TxID:          tTxID,
  3579  		Address:       tPKHAddr.String(),
  3580  		Account:       tAcctName,
  3581  		Amount:        float64(unspentVal) / 1e8,
  3582  		Confirmations: 5,
  3583  		ScriptPubKey:  hex.EncodeToString(tP2PKHScript),
  3584  		Spendable:     true,
  3585  	}}
  3586  
  3587  	addr, err := stdaddr.DecodeAddress(address, tChainParams)
  3588  	if err != nil {
  3589  		t.Fatal(err)
  3590  	}
  3591  
  3592  	// This should return an error, not enough funds to send.
  3593  	_, _, _, err = wallet.sendToAddress(addr, unspentVal, optimalFeeRate)
  3594  	if err == nil {
  3595  		t.Fatal("Expected error, not enough funds to send.")
  3596  	}
  3597  
  3598  	// With a lower send value, send should be successful.
  3599  	var sendVal uint64 = 10e8
  3600  	node.unspent[0].Amount = float64(unspentVal)
  3601  	msgTx, val, _, err := wallet.sendToAddress(addr, sendVal, optimalFeeRate)
  3602  	if err != nil {
  3603  		t.Fatal(err)
  3604  	}
  3605  	if val != uint64(msgTx.TxOut[0].Value) {
  3606  		t.Errorf("expected non-change output to be %d, got %d", val, msgTx.TxOut[0].Value)
  3607  	}
  3608  	if val != sendVal {
  3609  		t.Errorf("expected non-change output to be %d, got %d", sendVal, val)
  3610  	}
  3611  }
  3612  
  3613  func TestLookupTxOutput(t *testing.T) {
  3614  	wallet, node, shutdown := tNewWallet()
  3615  	defer shutdown()
  3616  
  3617  	coinID := make([]byte, 36)
  3618  	copy(coinID[:32], tTxHash[:])
  3619  	op := newOutPoint(tTxHash, 0)
  3620  
  3621  	// Bad output coin
  3622  	op.vout = 10
  3623  	_, _, spent, err := wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout)
  3624  	if err == nil {
  3625  		t.Fatalf("no error for bad output coin")
  3626  	}
  3627  	if spent != 0 {
  3628  		t.Fatalf("spent is not 0 for bad output coin")
  3629  	}
  3630  	op.vout = 0
  3631  
  3632  	// Add the txOutRes with 2 confs and BestBlock correctly set.
  3633  	node.txOutRes[op] = makeGetTxOutRes(2, 1, tP2PKHScript)
  3634  	_, confs, spent, err := wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout)
  3635  	if err != nil {
  3636  		t.Fatalf("unexpected error for gettxout path: %v", err)
  3637  	}
  3638  	if confs != 2 {
  3639  		t.Fatalf("confs not retrieved from gettxout path. expected 2, got %d", confs)
  3640  	}
  3641  	if spent != 0 {
  3642  		t.Fatalf("expected spent = 0 for gettxout path, got true")
  3643  	}
  3644  
  3645  	// gettransaction error
  3646  	delete(node.txOutRes, op)
  3647  	walletTx := &walletjson.GetTransactionResult{}
  3648  	node.walletTxFn = func() (*walletjson.GetTransactionResult, error) {
  3649  		return walletTx, tErr
  3650  	}
  3651  	_, _, spent, err = wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout)
  3652  	if err == nil {
  3653  		t.Fatalf("no error for gettransaction error")
  3654  	}
  3655  	if spent != 0 {
  3656  		t.Fatalf("spent is not 0 with gettransaction error")
  3657  	}
  3658  	node.walletTxFn = func() (*walletjson.GetTransactionResult, error) {
  3659  		return walletTx, nil
  3660  	}
  3661  
  3662  	// wallet.lookupTxOutput will check if the tx is confirmed, its hex
  3663  	// is valid and contains an output at index 0, for the output to be
  3664  	// considered spent.
  3665  	tx := wire.NewMsgTx()
  3666  	tx.AddTxIn(&wire.TxIn{})
  3667  	tx.AddTxOut(&wire.TxOut{
  3668  		PkScript: tP2PKHScript,
  3669  	})
  3670  	txHex, err := msgTxToHex(tx)
  3671  	if err != nil {
  3672  		t.Fatalf("error preparing tx hex with 1 output: %v", err)
  3673  	}
  3674  	walletTx.Hex = txHex // unconfirmed = unspent
  3675  
  3676  	_, _, spent, err = wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout)
  3677  	if err != nil {
  3678  		t.Fatalf("unexpected error for gettransaction path (unconfirmed): %v", err)
  3679  	}
  3680  	if spent != 0 {
  3681  		t.Fatalf("expected spent = 0 for gettransaction path (unconfirmed), got true")
  3682  	}
  3683  
  3684  	// Confirmed wallet tx without gettxout response is spent.
  3685  	walletTx.Confirmations = 2
  3686  	_, _, spent, err = wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout)
  3687  	if err != nil {
  3688  		t.Fatalf("unexpected error for gettransaction path (confirmed): %v", err)
  3689  	}
  3690  	if spent != 1 {
  3691  		t.Fatalf("expected spent = 1 for gettransaction path (confirmed), got false")
  3692  	}
  3693  
  3694  	// In spv mode, spent status is unknown without a block filters scan.
  3695  	wallet.wallet.(*rpcWallet).spvMode = true
  3696  	_, _, spent, err = wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout)
  3697  	if err != nil {
  3698  		t.Fatalf("unexpected error for spv gettransaction path (non-wallet output): %v", err)
  3699  	}
  3700  	if spent != -1 {
  3701  		t.Fatalf("expected spent = -1 for spv gettransaction path (non-wallet output), got true")
  3702  	}
  3703  
  3704  	// In spv mode, output is spent if it pays to the wallet (but no txOutRes).
  3705  	/* what is the use case for this since a contract never pays to wallet?
  3706  	node.walletTx.Details = []walletjson.GetTransactionDetailsResult{{
  3707  		Vout:     0,
  3708  		Category: "receive", // output at index 0 pays to the wallet
  3709  	}}
  3710  	_, _, spent, err = wallet.lookupTxOutput(context.Background(), &op.txHash, op.vout)
  3711  	if err != nil {
  3712  		t.Fatalf("unexpected error for spv gettransaction path (wallet output): %v", err)
  3713  	}
  3714  	if spent != 1 {
  3715  		t.Fatalf("expected spent = 1 for spv gettransaction path (wallet output), got false")
  3716  	}
  3717  	*/
  3718  }
  3719  
  3720  func TestSendEdges(t *testing.T) {
  3721  	wallet, node, shutdown := tNewWallet()
  3722  	defer shutdown()
  3723  
  3724  	const feeRate uint64 = 3
  3725  
  3726  	const swapVal = 2e8 // leaving untyped. NewTxOut wants int64
  3727  
  3728  	contractAddr, _ := stdaddr.NewAddressScriptHashV0(randBytes(20), tChainParams)
  3729  	// See dexdcr.IsDust for the source of this dustCoverage voodoo.
  3730  	dustCoverage := (dexdcr.P2PKHOutputSize + 165) * feeRate * 3
  3731  	dexReqFees := dexdcr.InitTxSize * feeRate
  3732  
  3733  	_, pkScript := contractAddr.PaymentScript()
  3734  
  3735  	newBaseTx := func(funding uint64) *wire.MsgTx {
  3736  		baseTx := wire.NewMsgTx()
  3737  		baseTx.AddTxIn(wire.NewTxIn(new(wire.OutPoint), int64(funding), nil))
  3738  		baseTx.AddTxOut(wire.NewTxOut(swapVal, pkScript))
  3739  		return baseTx
  3740  	}
  3741  
  3742  	node.signFunc = func(tx *wire.MsgTx) (*wire.MsgTx, bool, error) {
  3743  		return signFunc(tx, dexdcr.P2PKHSigScriptSize)
  3744  	}
  3745  
  3746  	tests := []struct {
  3747  		name      string
  3748  		funding   uint64
  3749  		expChange bool
  3750  	}{
  3751  		{
  3752  			name:    "not enough for change output",
  3753  			funding: swapVal + dexReqFees - 1,
  3754  		},
  3755  		{
  3756  			// Still dust here, but a different path.
  3757  			name:    "exactly enough for change output",
  3758  			funding: swapVal + dexReqFees,
  3759  		},
  3760  		{
  3761  			name:    "more than enough for change output but still dust",
  3762  			funding: swapVal + dexReqFees + 1,
  3763  		},
  3764  		{
  3765  			name:    "1 atom short to not be dust",
  3766  			funding: swapVal + dexReqFees + dustCoverage - 1,
  3767  		},
  3768  		{
  3769  			name:      "exactly enough to not be dust",
  3770  			funding:   swapVal + dexReqFees + dustCoverage,
  3771  			expChange: true,
  3772  		},
  3773  	}
  3774  
  3775  	// tPKHAddrV3, _ := stdaddr.DecodeAddress(tPKHAddr.String(), tChainParams)
  3776  	node.changeAddr = tPKHAddr
  3777  
  3778  	for _, tt := range tests {
  3779  		tx, err := wallet.sendWithReturn(newBaseTx(tt.funding), feeRate, -1)
  3780  		if err != nil {
  3781  			t.Fatalf("sendWithReturn error: %v", err)
  3782  		}
  3783  
  3784  		if len(tx.TxOut) == 1 && tt.expChange {
  3785  			t.Fatalf("%s: no change added", tt.name)
  3786  		} else if len(tx.TxOut) == 2 && !tt.expChange {
  3787  			t.Fatalf("%s: change output added for dust. Output value = %d", tt.name, tx.TxOut[1].Value)
  3788  		}
  3789  	}
  3790  }
  3791  
  3792  func TestSyncStatus(t *testing.T) {
  3793  	wallet, node, shutdown := tNewWallet()
  3794  	defer shutdown()
  3795  
  3796  	node.rawRes[methodSyncStatus], node.rawErr[methodSyncStatus] = json.Marshal(&walletjson.SyncStatusResult{
  3797  		Synced:               true,
  3798  		InitialBlockDownload: false,
  3799  		HeadersFetchProgress: 1,
  3800  	})
  3801  	ss, err := wallet.SyncStatus()
  3802  	if err != nil {
  3803  		t.Fatalf("SyncStatus error (synced expected): %v", err)
  3804  	}
  3805  	if !ss.Synced {
  3806  		t.Fatalf("synced = false for progress=1")
  3807  	}
  3808  	if ss.BlockProgress() < 1 {
  3809  		t.Fatalf("progress not complete with sync true")
  3810  	}
  3811  
  3812  	node.rawErr[methodSyncStatus] = tErr
  3813  	_, err = wallet.SyncStatus()
  3814  	if err == nil {
  3815  		t.Fatalf("SyncStatus error not propagated")
  3816  	}
  3817  	node.rawErr[methodSyncStatus] = nil
  3818  
  3819  	nodeSyncStatusResult := &walletjson.SyncStatusResult{
  3820  		Synced:               false,
  3821  		InitialBlockDownload: false,
  3822  		HeadersFetchProgress: 0.5, // Headers: 200, WalletTip: 100
  3823  	}
  3824  	node.rawRes[methodSyncStatus], node.rawErr[methodSyncStatus] = json.Marshal(nodeSyncStatusResult)
  3825  	node.rawRes[methodGetPeerInfo], node.rawErr[methodGetPeerInfo] = json.Marshal([]*walletjson.GetPeerInfoResult{{StartingHeight: 1000}})
  3826  
  3827  	ss, err = wallet.SyncStatus()
  3828  	if err != nil {
  3829  		t.Fatalf("SyncStatus error (half-synced): %v", err)
  3830  	}
  3831  	if ss.Synced {
  3832  		t.Fatalf("synced = true for progress=0.5")
  3833  	}
  3834  	if ss.BlockProgress() != nodeSyncStatusResult.HeadersFetchProgress {
  3835  		t.Fatalf("progress out of range. Expected %.2f, got %.2f", nodeSyncStatusResult.HeadersFetchProgress, ss.BlockProgress())
  3836  	}
  3837  	if ss.Blocks != 500 {
  3838  		t.Fatalf("wrong header sync height. expected 500, got %d", ss.Blocks)
  3839  	}
  3840  }
  3841  
  3842  func TestPreSwap(t *testing.T) {
  3843  	wallet, node, shutdown := tNewWallet()
  3844  	defer shutdown()
  3845  
  3846  	// See math from TestFundEdges. 10 lots with max fee rate of 34 sats/vbyte.
  3847  
  3848  	swapVal := uint64(1e8)
  3849  	lots := swapVal / tLotSize // 10 lots
  3850  
  3851  	const totalBytes = 2510
  3852  	// base_best_case_bytes = swap_size_base + backing_bytes
  3853  	//                      = 85 + 166 = 251
  3854  	const bestCaseBytes = 251 // i.e. swapSize
  3855  
  3856  	backingFees := uint64(totalBytes) * tDCR.MaxFeeRate // total_bytes * fee_rate
  3857  
  3858  	minReq := swapVal + backingFees
  3859  
  3860  	fees := uint64(totalBytes) * tDCR.MaxFeeRate
  3861  	p2pkhUnspent := walletjson.ListUnspentResult{
  3862  		TxID:          tTxID,
  3863  		Address:       tPKHAddr.String(),
  3864  		Account:       tAcctName,
  3865  		Amount:        float64(swapVal+fees-1) / 1e8, // one atom less than needed
  3866  		Confirmations: 5,
  3867  		ScriptPubKey:  hex.EncodeToString(tP2PKHScript),
  3868  		Spendable:     true,
  3869  	}
  3870  
  3871  	node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent}
  3872  
  3873  	form := &asset.PreSwapForm{
  3874  		Version:       version,
  3875  		LotSize:       tLotSize,
  3876  		Lots:          lots,
  3877  		MaxFeeRate:    tDCR.MaxFeeRate,
  3878  		Immediate:     false,
  3879  		FeeSuggestion: feeSuggestion,
  3880  		// Redeem fields unneeded
  3881  	}
  3882  
  3883  	node.unspent[0].Amount = float64(minReq) / 1e8
  3884  
  3885  	// Initial success.
  3886  	preSwap, err := wallet.PreSwap(form)
  3887  	if err != nil {
  3888  		t.Fatalf("PreSwap error: %v", err)
  3889  	}
  3890  
  3891  	maxFees := totalBytes * tDCR.MaxFeeRate
  3892  	estHighFees := totalBytes * feeSuggestion
  3893  	estLowFees := bestCaseBytes * feeSuggestion
  3894  	checkSwapEstimate(t, preSwap.Estimate, lots, swapVal, maxFees, estHighFees, estLowFees)
  3895  
  3896  	// Too little funding is an error.
  3897  	node.unspent[0].Amount = float64(minReq-1) / 1e8
  3898  	_, err = wallet.PreSwap(form)
  3899  	if err == nil {
  3900  		t.Fatalf("no PreSwap error for not enough funds")
  3901  	}
  3902  	node.unspent[0].Amount = float64(minReq) / 1e8
  3903  
  3904  	// Success again.
  3905  	_, err = wallet.PreSwap(form)
  3906  	if err != nil {
  3907  		t.Fatalf("PreSwap error: %v", err)
  3908  	}
  3909  }
  3910  
  3911  func TestPreRedeem(t *testing.T) {
  3912  	wallet, _, shutdown := tNewWallet()
  3913  	defer shutdown()
  3914  
  3915  	preRedeem, err := wallet.PreRedeem(&asset.PreRedeemForm{
  3916  		Version: version,
  3917  		Lots:    5,
  3918  	})
  3919  	// Shouldn't actually be any path to error.
  3920  	if err != nil {
  3921  		t.Fatalf("PreRedeem non-segwit error: %v", err)
  3922  	}
  3923  
  3924  	// Just a sanity check.
  3925  	if preRedeem.Estimate.RealisticBestCase >= preRedeem.Estimate.RealisticWorstCase {
  3926  		t.Fatalf("best case > worst case")
  3927  	}
  3928  }
  3929  
  3930  func Test_dcrPerKBToAtomsPerByte(t *testing.T) {
  3931  	tests := []struct {
  3932  		name             string
  3933  		estimatedFeeRate float64
  3934  		want             uint64
  3935  		wantErr          bool
  3936  	}{
  3937  		{
  3938  			"catch negative", // but caller should check
  3939  			-0.0002,
  3940  			0,
  3941  			true,
  3942  		},
  3943  		{
  3944  			"ok 0", // but caller should check
  3945  			0.0,
  3946  			0,
  3947  			false,
  3948  		},
  3949  		{
  3950  			"ok 10",
  3951  			0.0001,
  3952  			10,
  3953  			false,
  3954  		},
  3955  		{
  3956  			"ok 11",
  3957  			0.00011,
  3958  			11,
  3959  			false,
  3960  		},
  3961  		{
  3962  			"ok 1",
  3963  			0.00001,
  3964  			1,
  3965  			false,
  3966  		},
  3967  		{
  3968  			"ok 1 rounded up",
  3969  			0.000002,
  3970  			1,
  3971  			false,
  3972  		},
  3973  		{
  3974  			"catch NaN err",
  3975  			math.NaN(),
  3976  			0,
  3977  			true,
  3978  		},
  3979  	}
  3980  	for _, tt := range tests {
  3981  		t.Run(tt.name, func(t *testing.T) {
  3982  			got, err := dcrPerKBToAtomsPerByte(tt.estimatedFeeRate)
  3983  			if (err != nil) != tt.wantErr {
  3984  				t.Errorf("dcrPerKBToAtomsPerByte() error = %v, wantErr %v", err, tt.wantErr)
  3985  				return
  3986  			}
  3987  			if got != tt.want {
  3988  				t.Errorf("dcrPerKBToAtomsPerByte() = %v, want %v", got, tt.want)
  3989  			}
  3990  		})
  3991  	}
  3992  }
  3993  
  3994  type tReconfigurer struct {
  3995  	*rpcWallet
  3996  	restart bool
  3997  	err     error
  3998  }
  3999  
  4000  func (r *tReconfigurer) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, net dex.Network, currentAddress string) (restartRequired bool, err error) {
  4001  	return r.restart, r.err
  4002  }
  4003  
  4004  func TestReconfigure(t *testing.T) {
  4005  	wallet, _, shutdown := tNewWalletMonitorBlocks(false)
  4006  	defer shutdown()
  4007  
  4008  	reconfigurer := tReconfigurer{
  4009  		rpcWallet: wallet.wallet.(*rpcWallet),
  4010  	}
  4011  
  4012  	wallet.wallet = &reconfigurer
  4013  
  4014  	ctx, cancel := context.WithCancel(context.Background())
  4015  	defer cancel()
  4016  
  4017  	cfg1 := &walletConfig{
  4018  		UseSplitTx:       true,
  4019  		FallbackFeeRate:  55,
  4020  		FeeRateLimit:     98,
  4021  		RedeemConfTarget: 7,
  4022  		ApiFeeFallback:   true,
  4023  	}
  4024  
  4025  	cfg2 := &walletConfig{
  4026  		UseSplitTx:       false,
  4027  		FallbackFeeRate:  66,
  4028  		FeeRateLimit:     97,
  4029  		RedeemConfTarget: 5,
  4030  		ApiFeeFallback:   false,
  4031  	}
  4032  
  4033  	// TODO: Test account names reconfiguration for rpcwallets.
  4034  	checkConfig := func(cfg *walletConfig) {
  4035  		if cfg.UseSplitTx != wallet.config().useSplitTx ||
  4036  			toAtoms(cfg.FallbackFeeRate/1000) != wallet.config().fallbackFeeRate ||
  4037  			toAtoms(cfg.FeeRateLimit/1000) != wallet.config().feeRateLimit ||
  4038  			cfg.RedeemConfTarget != wallet.config().redeemConfTarget ||
  4039  			cfg.ApiFeeFallback != wallet.config().apiFeeFallback {
  4040  			t.Fatalf("wallet not configured with the correct values")
  4041  		}
  4042  	}
  4043  
  4044  	settings1, err := config.Mapify(cfg1)
  4045  	if err != nil {
  4046  		t.Fatalf("failed to mapify: %v", err)
  4047  	}
  4048  
  4049  	settings2, err := config.Mapify(cfg2)
  4050  	if err != nil {
  4051  		t.Fatalf("failed to mapify: %v", err)
  4052  	}
  4053  
  4054  	walletCfg := &asset.WalletConfig{
  4055  		Type:     walletTypeDcrwRPC,
  4056  		Settings: settings1,
  4057  		DataDir:  "abcd",
  4058  	}
  4059  
  4060  	// restart = false
  4061  	restart, err := wallet.Reconfigure(ctx, walletCfg, "123456")
  4062  	if err != nil {
  4063  		t.Fatalf("did not expect an error")
  4064  	}
  4065  	if restart {
  4066  		t.Fatalf("expected false restart but got true")
  4067  	}
  4068  	checkConfig(cfg1)
  4069  
  4070  	// restart = 2
  4071  	reconfigurer.restart = true
  4072  	restart, err = wallet.Reconfigure(ctx, walletCfg, "123456")
  4073  	if err != nil {
  4074  		t.Fatalf("did not expect an error")
  4075  	}
  4076  	if !restart {
  4077  		t.Fatalf("expected true restart but got false")
  4078  	}
  4079  	checkConfig(cfg1)
  4080  
  4081  	// try to set new configs, but get error. config should not change.
  4082  	reconfigurer.err = errors.New("reconfigure error")
  4083  	walletCfg.Settings = settings2
  4084  	_, err = wallet.Reconfigure(ctx, walletCfg, "123456")
  4085  	if err == nil {
  4086  		t.Fatalf("expected an error")
  4087  	}
  4088  	checkConfig(cfg1)
  4089  }
  4090  
  4091  func TestEstimateSendTxFee(t *testing.T) {
  4092  	wallet, node, shutdown := tNewWallet()
  4093  	defer shutdown()
  4094  
  4095  	addr := tPKHAddr.String()
  4096  	node.changeAddr = tPKHAddr
  4097  	var unspentVal uint64 = 100e8
  4098  	unspents := make([]walletjson.ListUnspentResult, 0)
  4099  	balanceResult := &walletjson.GetBalanceResult{
  4100  		Balances: []walletjson.GetAccountBalanceResult{
  4101  			{
  4102  				AccountName: tAcctName,
  4103  			},
  4104  		},
  4105  	}
  4106  	node.balanceResult = balanceResult
  4107  
  4108  	var vout uint32
  4109  	addUtxo := func(atomAmt uint64, confs int64, updateUnspent bool) {
  4110  		if updateUnspent {
  4111  			node.unspent[0].Amount += float64(atomAmt) / 1e8
  4112  			return
  4113  		}
  4114  		utxo := walletjson.ListUnspentResult{
  4115  			TxID:          tTxID,
  4116  			Vout:          vout,
  4117  			Address:       tPKHAddr.String(),
  4118  			Account:       tAcctName,
  4119  			Amount:        float64(atomAmt) / 1e8,
  4120  			Confirmations: confs,
  4121  			ScriptPubKey:  hex.EncodeToString(tP2PKHScript),
  4122  			Spendable:     true,
  4123  		}
  4124  		unspents = append(unspents, utxo)
  4125  		node.unspent = unspents
  4126  		// update balance
  4127  		balanceResult.Balances[0].Spendable += utxo.Amount
  4128  		vout++
  4129  	}
  4130  
  4131  	tx := wire.NewMsgTx()
  4132  	payScriptVer, payScript := tPKHAddr.PaymentScript()
  4133  	tx.AddTxOut(newTxOut(int64(unspentVal), payScriptVer, payScript))
  4134  
  4135  	// bSize is the base size for a single tx input.
  4136  	bSize := dexdcr.TxInOverhead + uint32(wire.VarIntSerializeSize(uint64(dexdcr.P2PKHSigScriptSize))) + dexdcr.P2PKHSigScriptSize
  4137  
  4138  	txSize := uint32(tx.SerializeSize()) + bSize
  4139  	estFee := uint64(txSize) * optimalFeeRate
  4140  	changeFee := dexdcr.P2PKHOutputSize * optimalFeeRate
  4141  	estFeeWithChange := changeFee + estFee
  4142  
  4143  	// This should return fee estimate for one output.
  4144  	addUtxo(unspentVal, 1, false)
  4145  	estimate, _, err := wallet.EstimateSendTxFee(addr, unspentVal, optimalFeeRate, true, false)
  4146  	if err != nil {
  4147  		t.Fatal(err)
  4148  	}
  4149  	if estimate != estFee {
  4150  		t.Fatalf("expected estimate to be %v, got %v)", estFee, estimate)
  4151  	}
  4152  
  4153  	// This should return fee estimate for two output.
  4154  	estimate, _, err = wallet.EstimateSendTxFee(addr, unspentVal/2, optimalFeeRate, true, false)
  4155  	if err != nil {
  4156  		t.Fatal(err)
  4157  	}
  4158  	if estimate != estFeeWithChange {
  4159  		t.Fatalf("expected estimate to be %v, got %v)", estFeeWithChange, estimate)
  4160  	}
  4161  
  4162  	// This should return an error, not enough funds to cover fees.
  4163  	_, _, err = wallet.EstimateSendTxFee(addr, unspentVal, optimalFeeRate, false, false)
  4164  	if err == nil {
  4165  		t.Fatal("Expected error not enough to cover funds required")
  4166  	}
  4167  
  4168  	dust := uint64(100)
  4169  	addUtxo(dust, 0, true)
  4170  	// This should return fee estimate for one output with dust added to fee.
  4171  	estFeeWithDust := estFee + 100
  4172  	estimate, _, err = wallet.EstimateSendTxFee(addr, unspentVal, optimalFeeRate, true, false)
  4173  	if err != nil {
  4174  		t.Fatal(err)
  4175  	}
  4176  	if estimate != estFeeWithDust {
  4177  		t.Fatalf("expected estimate to be %v, got %v)", estFeeWithDust, estimate)
  4178  	}
  4179  
  4180  	// Invalid address
  4181  	_, valid, _ := wallet.EstimateSendTxFee("invalidsendaddress", unspentVal, optimalFeeRate, true, false)
  4182  	if valid {
  4183  		t.Fatal("Expected false for an invalid address")
  4184  	}
  4185  
  4186  	// Successful estimate for empty address
  4187  	_, _, err = wallet.EstimateSendTxFee("", unspentVal, optimalFeeRate, true, false)
  4188  	if err != nil {
  4189  		t.Fatalf("Error for empty address: %v", err)
  4190  	}
  4191  
  4192  	// Zero send amount
  4193  	_, _, err = wallet.EstimateSendTxFee(addr, 0, optimalFeeRate, true, false)
  4194  	if err == nil {
  4195  		t.Fatal("Expected error, send amount is zero")
  4196  	}
  4197  }
  4198  
  4199  func TestConfirmRedemption(t *testing.T) {
  4200  	wallet, node, shutdown := tNewWallet()
  4201  	defer shutdown()
  4202  
  4203  	swapVal := toAtoms(5)
  4204  	secret := randBytes(32)
  4205  	secretHash := sha256.Sum256(secret)
  4206  	lockTime := time.Now().Add(time.Hour * 12)
  4207  	addr := tPKHAddr.String()
  4208  
  4209  	contract, err := dexdcr.MakeContract(addr, addr, secretHash[:], lockTime.Unix(), tChainParams)
  4210  	if err != nil {
  4211  		t.Fatalf("error making swap contract: %v", err)
  4212  	}
  4213  
  4214  	privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330")
  4215  
  4216  	node.changeAddr = tPKHAddr
  4217  	node.privWIF, err = dcrutil.NewWIF(privBytes, tChainParams.PrivateKeyID, dcrec.STEcdsaSecp256k1)
  4218  	if err != nil {
  4219  		t.Fatalf("NewWIF error: %v", err)
  4220  	}
  4221  
  4222  	contractAddr, _ := stdaddr.NewAddressScriptHashV0(contract, tChainParams)
  4223  	_, contractP2SHScript := contractAddr.PaymentScript()
  4224  
  4225  	redemptionScript, _ := dexdcr.RedeemP2SHContract(contract, randBytes(73), randBytes(33), secret)
  4226  
  4227  	spentTx := makeRawTx(nil, []dex.Bytes{contractP2SHScript})
  4228  	txHash := spentTx.TxHash()
  4229  	node.blockchain.addRawTx(1, spentTx)
  4230  	inputs := []*wire.TxIn{makeRPCVin(&txHash, 0, redemptionScript)}
  4231  	spenderTx := makeRawTx(inputs, nil)
  4232  	node.blockchain.addRawTx(2, spenderTx)
  4233  
  4234  	tip, _ := wallet.getBestBlock(wallet.ctx)
  4235  	wallet.currentTip.Store(tip)
  4236  
  4237  	txFn := func(doErr []bool) func() (*walletjson.GetTransactionResult, error) {
  4238  		var i int
  4239  		return func() (*walletjson.GetTransactionResult, error) {
  4240  			defer func() { i++ }()
  4241  			if doErr[i] {
  4242  				return nil, asset.CoinNotFoundError
  4243  			}
  4244  			b, err := spenderTx.Bytes() // spender is redeem, searched first
  4245  			if err != nil {
  4246  				t.Fatal(err)
  4247  			}
  4248  			if i > 0 {
  4249  				b, err = spentTx.Bytes() // spent is swap, searched if the fist call was a forced error
  4250  				if err != nil {
  4251  					t.Fatal(err)
  4252  				}
  4253  			}
  4254  			h := hex.EncodeToString(b)
  4255  			return &walletjson.GetTransactionResult{
  4256  				BlockHash:     hex.EncodeToString(randBytes(32)),
  4257  				Hex:           h,
  4258  				Confirmations: int64(i), // 0 for redeem and 1 for swap
  4259  			}, nil
  4260  		}
  4261  	}
  4262  
  4263  	coin := newOutput(&txHash, 0, swapVal, wire.TxTreeRegular)
  4264  
  4265  	ci := &asset.AuditInfo{
  4266  		Coin:       coin,
  4267  		Contract:   contract,
  4268  		Recipient:  tPKHAddr.String(),
  4269  		Expiration: lockTime,
  4270  		SecretHash: secretHash[:],
  4271  	}
  4272  
  4273  	redemption := &asset.Redemption{
  4274  		Spends: ci,
  4275  		Secret: secret,
  4276  	}
  4277  
  4278  	coinID := coin.ID()
  4279  	// Inverting the first byte.
  4280  	badCoinID := append(append(make([]byte, 0, len(coinID)), ^coinID[0]), coinID[1:]...)
  4281  
  4282  	tests := []struct {
  4283  		name             string
  4284  		redemption       *asset.Redemption
  4285  		coinID           []byte
  4286  		wantErr          bool
  4287  		bestBlockErr     error
  4288  		txRes            func() (*walletjson.GetTransactionResult, error)
  4289  		wantConfs        uint64
  4290  		mempoolRedeems   map[[32]byte]*mempoolRedeem
  4291  		txOutRes         map[outPoint]*chainjson.GetTxOutResult
  4292  		unspentOutputErr error
  4293  	}{{
  4294  		name:       "ok tx never seen before now",
  4295  		coinID:     coinID,
  4296  		redemption: redemption,
  4297  		txRes:      txFn([]bool{false}),
  4298  	}, {
  4299  		name:           "ok tx in map",
  4300  		coinID:         coinID,
  4301  		redemption:     redemption,
  4302  		txRes:          txFn([]bool{false}),
  4303  		mempoolRedeems: map[[32]byte]*mempoolRedeem{secretHash: {txHash: txHash, firstSeen: time.Now()}},
  4304  	}, {
  4305  		name:           "tx in map has different hash than coin id",
  4306  		coinID:         badCoinID,
  4307  		redemption:     redemption,
  4308  		txRes:          txFn([]bool{false}),
  4309  		mempoolRedeems: map[[32]byte]*mempoolRedeem{secretHash: {txHash: txHash, firstSeen: time.Now()}},
  4310  		wantErr:        true,
  4311  	}, {
  4312  		name:       "ok tx not found spent new tx",
  4313  		coinID:     coinID,
  4314  		redemption: redemption,
  4315  		txRes:      txFn([]bool{false}),
  4316  		txOutRes:   map[outPoint]*chainjson.GetTxOutResult{newOutPoint(&txHash, 0): makeGetTxOutRes(0, 5, nil)},
  4317  	}, {
  4318  		name:           "ok old tx should maybe be abandoned",
  4319  		coinID:         coinID,
  4320  		redemption:     redemption,
  4321  		txRes:          txFn([]bool{false}),
  4322  		mempoolRedeems: map[[32]byte]*mempoolRedeem{secretHash: {txHash: txHash, firstSeen: time.Now().Add(-maxRedeemMempoolAge - time.Second)}},
  4323  	}, {
  4324  		name:       "ok and spent",
  4325  		coinID:     coinID,
  4326  		txRes:      txFn([]bool{true, false}),
  4327  		redemption: redemption,
  4328  		wantConfs:  1, // one confirm because this tx is in the best block
  4329  	}, {
  4330  		name:   "ok and spent but we dont know who spent it",
  4331  		coinID: coinID,
  4332  		txRes:  txFn([]bool{true, false}),
  4333  		redemption: func() *asset.Redemption {
  4334  			ci := &asset.AuditInfo{
  4335  				Coin:       coin,
  4336  				Contract:   contract,
  4337  				Recipient:  tPKHAddr.String(),
  4338  				Expiration: lockTime,
  4339  				SecretHash: make([]byte, 32), // fake secret hash
  4340  			}
  4341  			return &asset.Redemption{
  4342  				Spends: ci,
  4343  				Secret: secret,
  4344  			}
  4345  		}(),
  4346  		wantConfs: requiredRedeemConfirms,
  4347  	}, {
  4348  		name:       "get transaction error",
  4349  		coinID:     coinID,
  4350  		redemption: redemption,
  4351  		txRes:      txFn([]bool{true, true}),
  4352  		wantErr:    true,
  4353  	}, {
  4354  		name:       "decode coin error",
  4355  		coinID:     nil,
  4356  		redemption: redemption,
  4357  		txRes:      txFn([]bool{true, false}),
  4358  		wantErr:    true,
  4359  	}, {
  4360  		name:     "redeem error",
  4361  		coinID:   coinID,
  4362  		txOutRes: map[outPoint]*chainjson.GetTxOutResult{newOutPoint(&txHash, 0): makeGetTxOutRes(0, 5, nil)},
  4363  		txRes:    txFn([]bool{true, false}),
  4364  		redemption: func() *asset.Redemption {
  4365  			ci := &asset.AuditInfo{
  4366  				Coin: coin,
  4367  				// Contract:   contract,
  4368  				Recipient:  tPKHAddr.String(),
  4369  				Expiration: lockTime,
  4370  				SecretHash: secretHash[:],
  4371  			}
  4372  			return &asset.Redemption{
  4373  				Spends: ci,
  4374  				Secret: secret,
  4375  			}
  4376  		}(),
  4377  		wantErr: true,
  4378  	}}
  4379  	for _, test := range tests {
  4380  		node.walletTxFn = test.txRes
  4381  		node.bestBlockErr = test.bestBlockErr
  4382  		wallet.mempoolRedeems = test.mempoolRedeems
  4383  		if wallet.mempoolRedeems == nil {
  4384  			wallet.mempoolRedeems = make(map[[32]byte]*mempoolRedeem)
  4385  		}
  4386  		node.txOutRes = test.txOutRes
  4387  		if node.txOutRes == nil {
  4388  			node.txOutRes = make(map[outPoint]*chainjson.GetTxOutResult)
  4389  		}
  4390  		status, err := wallet.ConfirmRedemption(test.coinID, test.redemption, 0)
  4391  		if test.wantErr {
  4392  			if err == nil {
  4393  				t.Fatalf("%q: expected error", test.name)
  4394  			}
  4395  			continue
  4396  		}
  4397  		if err != nil {
  4398  			t.Fatalf("%q: unexpected error: %v", test.name, err)
  4399  		}
  4400  		if status.Confs != test.wantConfs {
  4401  			t.Fatalf("%q: wanted %d confs but got %d", test.name, test.wantConfs, status.Confs)
  4402  		}
  4403  	}
  4404  }
  4405  
  4406  func TestPurchaseTickets(t *testing.T) {
  4407  	const feeSuggestion = 100
  4408  	const sdiff = 1
  4409  
  4410  	wallet, cl, shutdown := tNewWalletMonitorBlocks(false)
  4411  	defer shutdown()
  4412  	wallet.connected.Store(true)
  4413  	cl.stakeInfo.Difficulty = dcrutil.Amount(sdiff).ToCoin()
  4414  	cl.balanceResult = &walletjson.GetBalanceResult{Balances: []walletjson.GetAccountBalanceResult{{AccountName: tAcctName}}}
  4415  	setBalance := func(n, reserves uint64) {
  4416  		ticketCost := n * (sdiff + feeSuggestion*minVSPTicketPurchaseSize)
  4417  		cl.balanceResult.Balances[0].Spendable = dcrutil.Amount(ticketCost).ToCoin()
  4418  		wallet.bondReserves.Store(reserves)
  4419  	}
  4420  
  4421  	var blocksToConfirm atomic.Int64
  4422  	cl.walletTxFn = func() (*walletjson.GetTransactionResult, error) {
  4423  		txHex, _ := makeTxHex(nil, []dex.Bytes{randBytes(25)})
  4424  		var confs int64 = 1
  4425  		if blocksToConfirm.Load() > 0 {
  4426  			confs = 0
  4427  		}
  4428  		return &walletjson.GetTransactionResult{Hex: txHex, Confirmations: confs}, nil
  4429  	}
  4430  
  4431  	var remains []uint32
  4432  	checkRemains := func(exp ...uint32) {
  4433  		t.Helper()
  4434  		if len(remains) != len(exp) {
  4435  			t.Fatalf("wrong number of remains, wanted %d, got %+v", len(exp), remains)
  4436  		}
  4437  		for i := 0; i < len(remains); i++ {
  4438  			if remains[i] != exp[i] {
  4439  				t.Fatalf("wrong remains updates: wanted %+v, got %+v", exp, remains)
  4440  			}
  4441  		}
  4442  	}
  4443  
  4444  	waitForTicketLoopToExit := func() {
  4445  		// Ensure the loop closes
  4446  		timeout := time.After(time.Second)
  4447  		for {
  4448  			if !wallet.ticketBuyer.running.Load() {
  4449  				break
  4450  			}
  4451  			select {
  4452  			case <-time.After(time.Millisecond):
  4453  				return
  4454  			case <-timeout:
  4455  				t.Fatalf("ticket loop didn't exit")
  4456  			}
  4457  		}
  4458  	}
  4459  
  4460  	buyTickets := func(n int, wantErr bool) {
  4461  		defer waitForTicketLoopToExit()
  4462  		remains = make([]uint32, 0)
  4463  		if err := wallet.PurchaseTickets(n, feeSuggestion); err != nil {
  4464  			t.Fatalf("initial PurchaseTickets error: %v", err)
  4465  		}
  4466  
  4467  		var emitted int
  4468  		timeout := time.After(time.Second)
  4469  	out:
  4470  		for {
  4471  			var ni asset.WalletNotification
  4472  			select {
  4473  			case ni = <-cl.emitC:
  4474  			case <-timeout:
  4475  				t.Fatalf("timed out looking for ticket updates")
  4476  			default:
  4477  				blocksToConfirm.Add(-1)
  4478  				wallet.runTicketBuyer()
  4479  				continue
  4480  			}
  4481  			switch nt := ni.(type) {
  4482  			case *asset.CustomWalletNote:
  4483  				switch n := nt.Payload.(type) {
  4484  				case *TicketPurchaseUpdate:
  4485  					remains = append(remains, n.Remaining)
  4486  					if n.Err != "" {
  4487  						if wantErr {
  4488  							return
  4489  						}
  4490  						t.Fatalf("Error received in TicketPurchaseUpdate: %s", n.Err)
  4491  					}
  4492  					if n.Remaining == 0 {
  4493  						break out
  4494  					}
  4495  					emitted++
  4496  				}
  4497  
  4498  			}
  4499  		}
  4500  	}
  4501  
  4502  	tixHashes := func(n int) []*chainhash.Hash {
  4503  		hs := make([]*chainhash.Hash, n)
  4504  		for i := 0; i < n; i++ {
  4505  			var ticketHash chainhash.Hash
  4506  			copy(ticketHash[:], randBytes(32))
  4507  			hs[i] = &ticketHash
  4508  		}
  4509  		return hs
  4510  	}
  4511  
  4512  	// Single ticket purchased right away.
  4513  	cl.purchasedTickets = [][]*chainhash.Hash{tixHashes(1)}
  4514  	setBalance(1, 0)
  4515  	buyTickets(1, false)
  4516  	checkRemains(1, 0)
  4517  
  4518  	// Multiple tickets purchased right away.
  4519  	cl.purchasedTickets = [][]*chainhash.Hash{tixHashes(2)}
  4520  	setBalance(2, 0)
  4521  	buyTickets(2, false)
  4522  	checkRemains(2, 0)
  4523  
  4524  	// Two tickets, purchased in two tries, skipping some tries for unconfirmed
  4525  	// tickets.
  4526  	blocksToConfirm.Store(3)
  4527  	cl.purchasedTickets = [][]*chainhash.Hash{tixHashes(1), tixHashes(1)}
  4528  	buyTickets(2, false)
  4529  	checkRemains(2, 1, 0)
  4530  
  4531  	// (Wallet).PurchaseTickets error
  4532  	cl.purchasedTickets = [][]*chainhash.Hash{tixHashes(4)}
  4533  	cl.purchaseTicketsErr = errors.New("test error")
  4534  	setBalance(4, 0)
  4535  	buyTickets(4, true)
  4536  	checkRemains(4, 0)
  4537  
  4538  	// Low-balance error
  4539  	cl.purchasedTickets = [][]*chainhash.Hash{tixHashes(1)}
  4540  	setBalance(1, 1) // reserves make our available balance 0
  4541  	buyTickets(1, true)
  4542  	checkRemains(1, 0)
  4543  }
  4544  
  4545  func TestFindBond(t *testing.T) {
  4546  	wallet, node, shutdown := tNewWallet()
  4547  	defer shutdown()
  4548  
  4549  	privBytes, _ := hex.DecodeString("b07209eec1a8fb6cfe5cb6ace36567406971a75c330db7101fb21bc679bc5330")
  4550  	bondKey := secp256k1.PrivKeyFromBytes(privBytes)
  4551  
  4552  	amt := uint64(50_000)
  4553  	acctID := [32]byte{}
  4554  	lockTime := time.Now().Add(time.Hour * 12)
  4555  	utxo := walletjson.ListUnspentResult{
  4556  		TxID:          tTxID,
  4557  		Address:       tPKHAddr.String(),
  4558  		Account:       tAcctName,
  4559  		Amount:        1.0,
  4560  		Confirmations: 1,
  4561  		ScriptPubKey:  hex.EncodeToString(tP2PKHScript),
  4562  		Spendable:     true,
  4563  	}
  4564  	node.unspent = []walletjson.ListUnspentResult{utxo}
  4565  	node.newAddr = tPKHAddr
  4566  	node.changeAddr = tPKHAddr
  4567  
  4568  	bond, _, err := wallet.MakeBondTx(0, amt, 200, lockTime, bondKey, acctID[:])
  4569  	if err != nil {
  4570  		t.Fatal(err)
  4571  	}
  4572  
  4573  	txFn := func(err error, tx []byte) func() (*walletjson.GetTransactionResult, error) {
  4574  		return func() (*walletjson.GetTransactionResult, error) {
  4575  			if err != nil {
  4576  				return nil, err
  4577  			}
  4578  			h := hex.EncodeToString(tx)
  4579  			return &walletjson.GetTransactionResult{
  4580  				BlockHash: hex.EncodeToString(randBytes(32)),
  4581  				Hex:       h,
  4582  			}, nil
  4583  		}
  4584  	}
  4585  
  4586  	newBondTx := func() *wire.MsgTx {
  4587  		msgTx := wire.NewMsgTx()
  4588  		if err := msgTx.FromBytes(bond.SignedTx); err != nil {
  4589  			t.Fatal(err)
  4590  		}
  4591  		return msgTx
  4592  	}
  4593  	tooFewOutputs := newBondTx()
  4594  	tooFewOutputs.TxOut = tooFewOutputs.TxOut[2:]
  4595  	tooFewOutputsBytes, err := tooFewOutputs.Bytes()
  4596  	if err != nil {
  4597  		t.Fatal(err)
  4598  	}
  4599  
  4600  	badBondScript := newBondTx()
  4601  	badBondScript.TxOut[1].PkScript = badBondScript.TxOut[1].PkScript[1:]
  4602  	badBondScriptBytes, err := badBondScript.Bytes()
  4603  	if err != nil {
  4604  		t.Fatal(err)
  4605  	}
  4606  
  4607  	noBondMatch := newBondTx()
  4608  	noBondMatch.TxOut[0].PkScript = noBondMatch.TxOut[0].PkScript[1:]
  4609  	noBondMatchBytes, err := noBondMatch.Bytes()
  4610  	if err != nil {
  4611  		t.Fatal(err)
  4612  	}
  4613  
  4614  	node.blockchain.addRawTx(1, newBondTx())
  4615  	verboseBlocks := node.blockchain.verboseBlocks
  4616  
  4617  	tests := []struct {
  4618  		name          string
  4619  		coinID        []byte
  4620  		txRes         func() (*walletjson.GetTransactionResult, error)
  4621  		bestBlockErr  error
  4622  		verboseBlocks map[chainhash.Hash]*wire.MsgBlock
  4623  		searchUntil   time.Time
  4624  		wantErr       bool
  4625  	}{{
  4626  		name:   "ok",
  4627  		coinID: bond.CoinID,
  4628  		txRes:  txFn(nil, bond.SignedTx),
  4629  	}, {
  4630  		name:   "ok with find blocks",
  4631  		coinID: bond.CoinID,
  4632  		txRes:  txFn(asset.CoinNotFoundError, nil),
  4633  	}, {
  4634  		name:    "bad coin id",
  4635  		coinID:  make([]byte, 0),
  4636  		txRes:   txFn(nil, bond.SignedTx),
  4637  		wantErr: true,
  4638  	}, {
  4639  		name:    "missing an output",
  4640  		coinID:  bond.CoinID,
  4641  		txRes:   txFn(nil, tooFewOutputsBytes),
  4642  		wantErr: true,
  4643  	}, {
  4644  		name:    "bad bond commitment script",
  4645  		coinID:  bond.CoinID,
  4646  		txRes:   txFn(nil, badBondScriptBytes),
  4647  		wantErr: true,
  4648  	}, {
  4649  		name:    "bond script does not match commitment",
  4650  		coinID:  bond.CoinID,
  4651  		txRes:   txFn(nil, noBondMatchBytes),
  4652  		wantErr: true,
  4653  	}, {
  4654  		name:    "bad msgtx",
  4655  		coinID:  bond.CoinID,
  4656  		txRes:   txFn(nil, bond.SignedTx[100:]),
  4657  		wantErr: true,
  4658  	}, {
  4659  		name:         "get best block error",
  4660  		coinID:       bond.CoinID,
  4661  		txRes:        txFn(asset.CoinNotFoundError, nil),
  4662  		bestBlockErr: errors.New("some error"),
  4663  		wantErr:      true,
  4664  	}, {
  4665  		name:          "block not found",
  4666  		coinID:        bond.CoinID,
  4667  		txRes:         txFn(asset.CoinNotFoundError, nil),
  4668  		verboseBlocks: map[chainhash.Hash]*wire.MsgBlock{},
  4669  		wantErr:       true,
  4670  	}}
  4671  
  4672  	for _, test := range tests {
  4673  		t.Run(test.name, func(t *testing.T) {
  4674  			node.walletTxFn = test.txRes
  4675  			node.bestBlockErr = test.bestBlockErr
  4676  			node.blockchain.verboseBlocks = verboseBlocks
  4677  			if test.verboseBlocks != nil {
  4678  				node.blockchain.verboseBlocks = test.verboseBlocks
  4679  			}
  4680  			bd, err := wallet.FindBond(tCtx, test.coinID, test.searchUntil)
  4681  			if test.wantErr {
  4682  				if err == nil {
  4683  					t.Fatal("expected error")
  4684  				}
  4685  				return
  4686  			}
  4687  			if err != nil {
  4688  				t.Fatalf("unexpected error: %v", err)
  4689  			}
  4690  			if !bd.CheckPrivKey(bondKey) {
  4691  				t.Fatal("pkh not equal")
  4692  			}
  4693  		})
  4694  	}
  4695  }
  4696  
  4697  func makeSwapContract(lockTimeOffset time.Duration) (pkScriptVer uint16, pkScript []byte) {
  4698  	secret := randBytes(32)
  4699  	secretHash := sha256.Sum256(secret)
  4700  
  4701  	lockTime := time.Now().Add(lockTimeOffset)
  4702  	var err error
  4703  	contract, err := dexdcr.MakeContract(tPKHAddr.String(), tPKHAddr.String(), secretHash[:], lockTime.Unix(), chaincfg.MainNetParams())
  4704  	if err != nil {
  4705  		panic("error making swap contract:" + err.Error())
  4706  	}
  4707  
  4708  	scriptAddr, err := stdaddr.NewAddressScriptHashV0(contract, chaincfg.MainNetParams())
  4709  	if err != nil {
  4710  		panic("error making script address:" + err.Error())
  4711  	}
  4712  
  4713  	return scriptAddr.PaymentScript()
  4714  }
  4715  
  4716  func TestIDUnknownTx(t *testing.T) {
  4717  	// Swap Tx - any tx with p2sh outputs that is not a bond.
  4718  	_, swapPKScript := makeSwapContract(time.Hour * 12)
  4719  	swapTx := &wire.MsgTx{
  4720  		TxIn:  []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)},
  4721  		TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(1)), swapPKScript)},
  4722  	}
  4723  
  4724  	// Redeem Tx
  4725  	swapContract, _ := dexdcr.MakeContract(tPKHAddr.String(), tPKHAddr.String(), randBytes(32), time.Now().Unix(), chaincfg.MainNetParams())
  4726  	txIn := wire.NewTxIn(&wire.OutPoint{}, 0, nil)
  4727  	txIn.SignatureScript, _ = dexdcr.RedeemP2SHContract(swapContract, randBytes(73), randBytes(33), randBytes(32))
  4728  	redeemFee := 0.0000143
  4729  	_, tP2PKH := tPKHAddr.PaymentScript()
  4730  	redemptionTx := &wire.MsgTx{
  4731  		TxIn:  []*wire.TxIn{txIn},
  4732  		TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(5-redeemFee)), tP2PKH)},
  4733  	}
  4734  
  4735  	h2b := func(h string) []byte {
  4736  		b, _ := hex.DecodeString(h)
  4737  		return b
  4738  	}
  4739  
  4740  	// Create Bond Tx
  4741  	bondLockTime := 1711637410
  4742  	bondID := h2b("0e39bbb09592fd00b7d770cc832ddf4d625ae3a0")
  4743  	accountID := h2b("a0836b39b5ceb84f422b8a8cd5940117087a8522457c6d81d200557652fbe6ea")
  4744  	bondContract, _ := dexdcr.MakeBondScript(0, uint32(bondLockTime), bondID)
  4745  	contractAddr, err := stdaddr.NewAddressScriptHashV0(bondContract, chaincfg.MainNetParams())
  4746  	if err != nil {
  4747  		t.Fatal("error making script address:" + err.Error())
  4748  	}
  4749  	_, bondPkScript := contractAddr.PaymentScript()
  4750  
  4751  	bondOutput := wire.NewTxOut(int64(toAtoms(2)), bondPkScript)
  4752  	bondCommitPkScript, _ := bondPushDataScript(0, accountID, int64(bondLockTime), bondID)
  4753  	bondCommitmentOutput := wire.NewTxOut(0, bondCommitPkScript)
  4754  	createBondTx := &wire.MsgTx{
  4755  		TxIn:  []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)},
  4756  		TxOut: []*wire.TxOut{bondOutput, bondCommitmentOutput},
  4757  	}
  4758  
  4759  	// Redeem Bond Tx
  4760  	txIn = wire.NewTxIn(&wire.OutPoint{}, 0, nil)
  4761  	txIn.SignatureScript, _ = dexdcr.RefundBondScript(bondContract, randBytes(73), randBytes(33))
  4762  	redeemBondTx := &wire.MsgTx{
  4763  		TxIn:  []*wire.TxIn{txIn},
  4764  		TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(5)), tP2PKH)},
  4765  	}
  4766  
  4767  	// Split Tx
  4768  	splitTx := &wire.MsgTx{
  4769  		TxIn:  []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)},
  4770  		TxOut: []*wire.TxOut{wire.NewTxOut(0, tP2PKH), wire.NewTxOut(0, tP2PKH)},
  4771  	}
  4772  
  4773  	// Send Tx
  4774  	cpAddr, _ := stdaddr.DecodeAddress("Dsedb5o6Tw225Loq5J56BZ9jS4ehnEnmQ16", tChainParams)
  4775  	_, cpPkScript := cpAddr.PaymentScript()
  4776  	sendTx := &wire.MsgTx{
  4777  		TxIn:  []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)},
  4778  		TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(0.001)), tP2PKH), wire.NewTxOut(int64(toAtoms(4)), cpPkScript)},
  4779  	}
  4780  
  4781  	// Receive Tx
  4782  	receiveTx := &wire.MsgTx{
  4783  		TxIn:  []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)},
  4784  		TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(0.001)), cpPkScript), wire.NewTxOut(int64(toAtoms(4)), tP2PKH)},
  4785  	}
  4786  	type test struct {
  4787  		name            string
  4788  		ltr             *ListTransactionsResult
  4789  		tx              *wire.MsgTx
  4790  		validateAddress map[string]*walletjson.ValidateAddressResult
  4791  		exp             *asset.WalletTransaction
  4792  	}
  4793  
  4794  	// Ticket Tx
  4795  	ticketTx := &wire.MsgTx{
  4796  		TxIn:  []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, nil)},
  4797  		TxOut: []*wire.TxOut{wire.NewTxOut(int64(toAtoms(1)), cpPkScript)},
  4798  	}
  4799  
  4800  	float64Ptr := func(f float64) *float64 {
  4801  		return &f
  4802  	}
  4803  
  4804  	stringPtr := func(s string) *string {
  4805  		return &s
  4806  	}
  4807  
  4808  	regularTx := walletjson.LTTTRegular
  4809  	ticketPurchaseTx := walletjson.LTTTTicket
  4810  	ticketRevocationTx := walletjson.LTTTRevocation
  4811  	ticketVote := walletjson.LTTTVote
  4812  
  4813  	tests := []*test{
  4814  		{
  4815  			name: "swap",
  4816  			ltr: &ListTransactionsResult{
  4817  				TxType: &regularTx,
  4818  				Fee:    float64Ptr(0.0000321),
  4819  				TxID:   swapTx.TxHash().String(),
  4820  			},
  4821  			tx: swapTx,
  4822  			exp: &asset.WalletTransaction{
  4823  				Type:   asset.SwapOrSend,
  4824  				ID:     swapTx.TxHash().String(),
  4825  				Amount: toAtoms(1),
  4826  				Fees:   toAtoms(0.0000321),
  4827  			},
  4828  		},
  4829  		{
  4830  			name: "redeem",
  4831  			ltr: &ListTransactionsResult{
  4832  				TxType: &regularTx,
  4833  				TxID:   redemptionTx.TxHash().String(),
  4834  			},
  4835  			tx: redemptionTx,
  4836  			exp: &asset.WalletTransaction{
  4837  				Type:   asset.Redeem,
  4838  				ID:     redemptionTx.TxHash().String(),
  4839  				Amount: toAtoms(5 - redeemFee),
  4840  				Fees:   0,
  4841  			},
  4842  		},
  4843  		{
  4844  			name: "create bond",
  4845  			ltr: &ListTransactionsResult{
  4846  				TxType: &regularTx,
  4847  				Fee:    float64Ptr(0.0000222),
  4848  				TxID:   createBondTx.TxHash().String(),
  4849  			},
  4850  			tx: createBondTx,
  4851  			exp: &asset.WalletTransaction{
  4852  				Type:   asset.CreateBond,
  4853  				ID:     createBondTx.TxHash().String(),
  4854  				Amount: toAtoms(2),
  4855  				Fees:   toAtoms(0.0000222),
  4856  				BondInfo: &asset.BondTxInfo{
  4857  					AccountID: accountID,
  4858  					BondID:    bondID,
  4859  					LockTime:  uint64(bondLockTime),
  4860  				},
  4861  			},
  4862  		},
  4863  		{
  4864  			name: "redeem bond",
  4865  			ltr: &ListTransactionsResult{
  4866  				TxType: &regularTx,
  4867  				TxID:   redeemBondTx.TxHash().String(),
  4868  			},
  4869  			tx: redeemBondTx,
  4870  			exp: &asset.WalletTransaction{
  4871  				Type:   asset.RedeemBond,
  4872  				ID:     redeemBondTx.TxHash().String(),
  4873  				Amount: toAtoms(5),
  4874  				BondInfo: &asset.BondTxInfo{
  4875  					AccountID: []byte{},
  4876  					BondID:    bondID,
  4877  					LockTime:  uint64(bondLockTime),
  4878  				},
  4879  			},
  4880  		},
  4881  		{
  4882  			name: "split",
  4883  			ltr: &ListTransactionsResult{
  4884  				TxType: &regularTx,
  4885  				Fee:    float64Ptr(-0.0000251),
  4886  				Send:   true,
  4887  				TxID:   splitTx.TxHash().String(),
  4888  			},
  4889  			tx: splitTx,
  4890  			exp: &asset.WalletTransaction{
  4891  				Type: asset.Split,
  4892  				ID:   splitTx.TxHash().String(),
  4893  				Fees: toAtoms(0.0000251),
  4894  			},
  4895  		},
  4896  		{
  4897  			name: "send",
  4898  			ltr: &ListTransactionsResult{
  4899  				TxType: &regularTx,
  4900  				Send:   true,
  4901  				Fee:    float64Ptr(0.0000504),
  4902  				TxID:   sendTx.TxHash().String(),
  4903  			},
  4904  			tx: sendTx,
  4905  			exp: &asset.WalletTransaction{
  4906  				Type:      asset.Send,
  4907  				ID:        sendTx.TxHash().String(),
  4908  				Amount:    toAtoms(4),
  4909  				Recipient: stringPtr(cpAddr.String()),
  4910  				Fees:      toAtoms(0.0000504),
  4911  			},
  4912  			validateAddress: map[string]*walletjson.ValidateAddressResult{
  4913  				tPKHAddr.String(): {
  4914  					IsMine:  true,
  4915  					Account: tAcctName,
  4916  				},
  4917  			},
  4918  		},
  4919  		{
  4920  			name: "receive",
  4921  			ltr: &ListTransactionsResult{
  4922  				TxType: &regularTx,
  4923  				TxID:   receiveTx.TxHash().String(),
  4924  			},
  4925  			tx: receiveTx,
  4926  			exp: &asset.WalletTransaction{
  4927  				Type:      asset.Receive,
  4928  				ID:        receiveTx.TxHash().String(),
  4929  				Amount:    toAtoms(4),
  4930  				Recipient: stringPtr(tPKHAddr.String()),
  4931  			},
  4932  			validateAddress: map[string]*walletjson.ValidateAddressResult{
  4933  				tPKHAddr.String(): {
  4934  					IsMine:  true,
  4935  					Account: tAcctName,
  4936  				},
  4937  			},
  4938  		},
  4939  		{
  4940  			name: "ticket purchase",
  4941  			ltr: &ListTransactionsResult{
  4942  				TxType: &ticketPurchaseTx,
  4943  				TxID:   ticketTx.TxHash().String(),
  4944  			},
  4945  			tx: ticketTx,
  4946  			exp: &asset.WalletTransaction{
  4947  				Type:   asset.TicketPurchase,
  4948  				ID:     ticketTx.TxHash().String(),
  4949  				Amount: toAtoms(1),
  4950  			},
  4951  		},
  4952  		{
  4953  			name: "ticket vote",
  4954  			ltr: &ListTransactionsResult{
  4955  				TxType: &ticketVote,
  4956  				TxID:   ticketTx.TxHash().String(),
  4957  			},
  4958  			tx: ticketTx,
  4959  			exp: &asset.WalletTransaction{
  4960  				Type:   asset.TicketVote,
  4961  				ID:     ticketTx.TxHash().String(),
  4962  				Amount: toAtoms(1),
  4963  			},
  4964  		},
  4965  		{
  4966  			name: "ticket revocation",
  4967  			ltr: &ListTransactionsResult{
  4968  				TxType: &ticketRevocationTx,
  4969  				TxID:   ticketTx.TxHash().String(),
  4970  			},
  4971  			tx: ticketTx,
  4972  			exp: &asset.WalletTransaction{
  4973  				Type:   asset.TicketRevocation,
  4974  				ID:     ticketTx.TxHash().String(),
  4975  				Amount: toAtoms(1),
  4976  			},
  4977  		},
  4978  	}
  4979  
  4980  	runTest := func(tt *test) {
  4981  		t.Run(tt.name, func(t *testing.T) {
  4982  			wallet, node, shutdown := tNewWallet()
  4983  			defer shutdown()
  4984  			node.validateAddress = tt.validateAddress
  4985  			node.blockchain.rawTxs[tt.tx.TxHash()] = &wireTxWithHeight{
  4986  				tx: tt.tx,
  4987  			}
  4988  			wt, err := wallet.idUnknownTx(context.Background(), tt.ltr)
  4989  			if err != nil {
  4990  				t.Fatalf("%s: unexpected error: %v", tt.name, err)
  4991  			}
  4992  			if !reflect.DeepEqual(wt, tt.exp) {
  4993  				t.Fatalf("%s: expected %+v, got %+v", tt.name, tt.exp, wt)
  4994  			}
  4995  		})
  4996  	}
  4997  
  4998  	for _, tt := range tests {
  4999  		runTest(tt)
  5000  	}
  5001  }
  5002  
  5003  func TestRescanSync(t *testing.T) {
  5004  	wallet, node, shutdown := tNewWalletMonitorBlocks(false)
  5005  	defer shutdown()
  5006  
  5007  	const tip = 1000
  5008  	wallet.currentTip.Store(&block{height: tip})
  5009  
  5010  	node.rawRes[methodSyncStatus], node.rawErr[methodSyncStatus] = json.Marshal(&walletjson.SyncStatusResult{
  5011  		Synced:               true,
  5012  		InitialBlockDownload: false,
  5013  		HeadersFetchProgress: 1,
  5014  	})
  5015  
  5016  	node.blockchain.mainchain[tip] = &chainhash.Hash{}
  5017  
  5018  	checkProgress := func(expSynced bool, expProgress float32) {
  5019  		t.Helper()
  5020  		ss, err := wallet.SyncStatus()
  5021  		if err != nil {
  5022  			t.Fatalf("Unexpected error: %v", err)
  5023  		}
  5024  		if ss.Synced != expSynced {
  5025  			t.Fatalf("expected synced = %t, bot %t", expSynced, ss.Synced)
  5026  		}
  5027  		if !ss.Synced {
  5028  			txProgress := float32(*ss.Transactions) / float32(ss.TargetHeight)
  5029  			if math.Abs(float64(expProgress/txProgress)-1) > 0.001 {
  5030  				t.Fatalf("expected progress %f, got %f", expProgress, txProgress)
  5031  			}
  5032  		}
  5033  	}
  5034  
  5035  	// No rescan in progress.
  5036  	checkProgress(true, 1)
  5037  
  5038  	// Rescan running. No progress.
  5039  	wallet.rescan.progress = &rescanProgress{}
  5040  	checkProgress(false, 0)
  5041  
  5042  	// Halfway done.
  5043  	wallet.rescan.progress = &rescanProgress{scannedThrough: tip / 2}
  5044  	checkProgress(false, 0.5)
  5045  
  5046  	// Not synced until progress is nil.
  5047  	wallet.rescan.progress = &rescanProgress{scannedThrough: tip}
  5048  	checkProgress(false, 1)
  5049  
  5050  	// Scanned > tip OK
  5051  	wallet.rescan.progress = &rescanProgress{scannedThrough: tip * 2}
  5052  	checkProgress(false, 1)
  5053  
  5054  	// Rescan complete.
  5055  	wallet.rescan.progress = nil
  5056  	checkProgress(true, 1)
  5057  
  5058  }