decred.org/dcrdex@v1.0.5/server/asset/eth/eth_test.go (about)

     1  //go:build !harness
     2  
     3  // These tests will not be run if the harness build tag is set.
     4  
     5  package eth
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"crypto/sha256"
    11  	"encoding/binary"
    12  	"encoding/hex"
    13  	"errors"
    14  	"math/big"
    15  	"net/url"
    16  	"os"
    17  	"strings"
    18  	"testing"
    19  	"time"
    20  
    21  	"decred.org/dcrdex/dex"
    22  	"decred.org/dcrdex/dex/calc"
    23  	"decred.org/dcrdex/dex/encode"
    24  	dexeth "decred.org/dcrdex/dex/networks/eth"
    25  	"decred.org/dcrdex/server/asset"
    26  	"github.com/ethereum/go-ethereum"
    27  	"github.com/ethereum/go-ethereum/common"
    28  	"github.com/ethereum/go-ethereum/core/types"
    29  )
    30  
    31  const initLocktime = 1632112916
    32  
    33  var (
    34  	_            ethFetcher = (*testNode)(nil)
    35  	tLogger                 = dex.StdOutLogger("ETHTEST", dex.LevelTrace)
    36  	tCtx         context.Context
    37  	initCalldata = mustParseHex("a8793f94000000000000000000000000000" +
    38  		"0000000000000000000000000000000000020000000000000000000000000000000000" +
    39  		"0000000000000000000000000000002000000000000000000000000000000000000000" +
    40  		"00000000000000000614811148b3e4acc53b664f9cf6fcac0adcd328e95d62ba1f4379" +
    41  		"650ae3e1460a0f9d1a1000000000000000000000000345853e21b1d475582e71cc2691" +
    42  		"24ed5e2dd342200000000000000000000000000000000000000000000000022b1c8c12" +
    43  		"27a0000000000000000000000000000000000000000000000000000000000006148111" +
    44  		"4ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c56100000" +
    45  		"0000000000000000000345853e21b1d475582e71cc269124ed5e2dd342200000000000" +
    46  		"000000000000000000000000000000000000022b1c8c1227a0000")
    47  	/* initCallData parses to:
    48  	[ETHSwapInitiation {
    49  			RefundTimestamp: 1632112916
    50  			SecretHash: 8b3e4acc53b664f9cf6fcac0adcd328e95d62ba1f4379650ae3e1460a0f9d1a1
    51  			Value: 5e9 gwei
    52  			Participant: 0x345853e21b1d475582e71cc269124ed5e2dd3422
    53  		},
    54  	ETHSwapInitiation {
    55  			RefundTimestamp: 1632112916
    56  			SecretHash: ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561
    57  			Value: 5e9 gwei
    58  			Participant: 0x345853e21b1d475582e71cc269124ed5e2dd3422
    59  		}]
    60  	*/
    61  	initSecretHashA     = mustParseHex("8b3e4acc53b664f9cf6fcac0adcd328e95d62ba1f4379650ae3e1460a0f9d1a1")
    62  	initSecretHashB     = mustParseHex("ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561")
    63  	initParticipantAddr = common.HexToAddress("345853e21b1d475582E71cC269124eD5e2dD3422")
    64  	redeemCalldata      = mustParseHex("f4fd17f90000000000000000000000000000000000000" +
    65  		"000000000000000000000000020000000000000000000000000000000000000000000000000" +
    66  		"00000000000000022c0a304c9321402dc11cbb5898b9f2af3029ce1c76ec6702c4cd5bb965f" +
    67  		"d3e7399d971975c09331eb00f5e0dc1eaeca9bf4ee2d086d3fe1de489f920007d654687eac0" +
    68  		"9638c0c38b4e735b79f053cb869167ee770640ac5df5c4ab030813122aebdc4c31b88d0c8f4" +
    69  		"d644591a8e00e92b607f920ad8050deb7c7469767d9c561")
    70  	/*
    71  		redeemCallData parses to:
    72  		[ETHSwapRedemption {
    73  			SecretHash: 99d971975c09331eb00f5e0dc1eaeca9bf4ee2d086d3fe1de489f920007d6546
    74  			Secret: 2c0a304c9321402dc11cbb5898b9f2af3029ce1c76ec6702c4cd5bb965fd3e73
    75  		}
    76  		ETHSwapRedemption {
    77  			SecretHash: ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561
    78  			Secret: 87eac09638c0c38b4e735b79f053cb869167ee770640ac5df5c4ab030813122a
    79  		}]
    80  	*/
    81  	redeemSecretHashA = mustParseHex("99d971975c09331eb00f5e0dc1eaeca9bf4ee2d086d3fe1de489f920007d6546")
    82  	redeemSecretHashB = mustParseHex("ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561")
    83  	redeemSecretB     = mustParseHex("87eac09638c0c38b4e735b79f053cb869167ee770640ac5df5c4ab030813122a")
    84  )
    85  
    86  func mustParseHex(s string) []byte {
    87  	b, err := hex.DecodeString(s)
    88  	if err != nil {
    89  		panic(err)
    90  	}
    91  	return b
    92  }
    93  
    94  type testNode struct {
    95  	connectErr       error
    96  	bestHdr          *types.Header
    97  	bestHdrErr       error
    98  	hdrByHeight      *types.Header
    99  	hdrByHeightErr   error
   100  	blkNum           uint64
   101  	blkNumErr        error
   102  	syncProg         *ethereum.SyncProgress
   103  	syncProgErr      error
   104  	suggGasTipCap    *big.Int
   105  	suggGasTipCapErr error
   106  	swp              *dexeth.SwapState
   107  	swpErr           error
   108  	tx               *types.Transaction
   109  	txIsMempool      bool
   110  	txErr            error
   111  	acctBal          *big.Int
   112  	acctBalErr       error
   113  }
   114  
   115  func (n *testNode) connect(ctx context.Context) error {
   116  	return n.connectErr
   117  }
   118  
   119  func (n *testNode) shutdown() {}
   120  
   121  func (n *testNode) loadToken(context.Context, uint32, *VersionedToken) error {
   122  	return nil
   123  }
   124  
   125  func (n *testNode) bestHeader(ctx context.Context) (*types.Header, error) {
   126  	return n.bestHdr, n.bestHdrErr
   127  }
   128  
   129  func (n *testNode) headerByHeight(ctx context.Context, height uint64) (*types.Header, error) {
   130  	return n.hdrByHeight, n.hdrByHeightErr
   131  }
   132  
   133  func (n *testNode) blockNumber(ctx context.Context) (uint64, error) {
   134  	return n.blkNum, n.blkNumErr
   135  }
   136  
   137  func (n *testNode) syncProgress(ctx context.Context) (*ethereum.SyncProgress, error) {
   138  	return n.syncProg, n.syncProgErr
   139  }
   140  
   141  func (n *testNode) suggestGasTipCap(ctx context.Context) (*big.Int, error) {
   142  	return n.suggGasTipCap, n.suggGasTipCapErr
   143  }
   144  
   145  func (n *testNode) swap(ctx context.Context, assetID uint32, secretHash [32]byte) (*dexeth.SwapState, error) {
   146  	return n.swp, n.swpErr
   147  }
   148  
   149  func (n *testNode) transaction(ctx context.Context, hash common.Hash) (tx *types.Transaction, isMempool bool, err error) {
   150  	return n.tx, n.txIsMempool, n.txErr
   151  }
   152  
   153  func (n *testNode) accountBalance(ctx context.Context, assetID uint32, addr common.Address) (*big.Int, error) {
   154  	return n.acctBal, n.acctBalErr
   155  }
   156  
   157  func tSwap(bn, locktime int64, value uint64, secret [32]byte, state dexeth.SwapStep, participantAddr *common.Address) *dexeth.SwapState {
   158  	return &dexeth.SwapState{
   159  		Secret:      secret,
   160  		BlockHeight: uint64(bn),
   161  		LockTime:    time.Unix(locktime, 0),
   162  		Participant: *participantAddr,
   163  		State:       state,
   164  		Value:       dexeth.GweiToWei(value),
   165  	}
   166  }
   167  
   168  func tNewBackend(assetID uint32) (*AssetBackend, *testNode) {
   169  	node := &testNode{}
   170  	return &AssetBackend{
   171  		baseBackend: &baseBackend{
   172  			net:        dex.Simnet,
   173  			node:       node,
   174  			baseLogger: tLogger,
   175  		},
   176  		log:        tLogger.SubLogger(strings.ToUpper(dex.BipIDSymbol(assetID))),
   177  		assetID:    assetID,
   178  		blockChans: make(map[chan *asset.BlockUpdate]struct{}),
   179  		atomize:    dexeth.WeiToGwei,
   180  	}, node
   181  }
   182  
   183  func TestMain(m *testing.M) {
   184  	tLogger = dex.StdOutLogger("TEST", dex.LevelTrace)
   185  	var shutdown func()
   186  	tCtx, shutdown = context.WithCancel(context.Background())
   187  	doIt := func() int {
   188  		defer shutdown()
   189  		dexeth.Tokens[usdcID].NetTokens[dex.Simnet].SwapContracts[0].Address = common.BytesToAddress(encode.RandomBytes(20))
   190  		return m.Run()
   191  	}
   192  	os.Exit(doIt())
   193  }
   194  
   195  func TestDecodeCoinID(t *testing.T) {
   196  	drv := &Driver{}
   197  	txid := "0x1b86600b740d58ecc06eda8eba1c941c7ba3d285c78be89b56678da146ed53d1"
   198  	txHashB := mustParseHex("1b86600b740d58ecc06eda8eba1c941c7ba3d285c78be89b56678da146ed53d1")
   199  
   200  	type test struct {
   201  		name    string
   202  		input   []byte
   203  		wantErr bool
   204  		expRes  string
   205  	}
   206  
   207  	tests := []test{{
   208  		name:   "ok",
   209  		input:  txHashB,
   210  		expRes: txid,
   211  	}, {
   212  		name:    "too short",
   213  		input:   txHashB[:len(txHashB)/2],
   214  		wantErr: true,
   215  	}, {
   216  		name:    "too long",
   217  		input:   append(txHashB, txHashB...),
   218  		wantErr: true,
   219  	}}
   220  
   221  	for _, tt := range tests {
   222  		res, err := drv.DecodeCoinID(tt.input)
   223  		if err != nil {
   224  			if !tt.wantErr {
   225  				t.Fatalf("%s: error: %v", tt.name, err)
   226  			}
   227  			continue
   228  		}
   229  
   230  		if tt.wantErr {
   231  			t.Fatalf("%s: no error", tt.name)
   232  		}
   233  		if res != tt.expRes {
   234  			t.Fatalf("%s: wrong result. wanted %s, got %s", tt.name, tt.expRes, res)
   235  		}
   236  	}
   237  }
   238  
   239  func TestRun(t *testing.T) {
   240  	ctx, cancel := context.WithCancel(context.Background())
   241  	backend, err := unconnectedETH(BipID, dexeth.ContractAddresses[0][dex.Simnet], registeredTokens, tLogger, dex.Simnet)
   242  	if err != nil {
   243  		t.Fatalf("unconnectedETH error: %v", err)
   244  	}
   245  	backend.node = &testNode{
   246  		blkNum: backend.bestHeight + 1,
   247  	}
   248  	ch := backend.BlockChannel(1)
   249  	go func() {
   250  		select {
   251  		case <-ch:
   252  			cancel()
   253  		case <-time.After(time.Second * 2):
   254  		}
   255  	}()
   256  	backend.run(ctx)
   257  	// Ok if ctx was canceled above. Linters complain about calling t.Fatal
   258  	// in the goroutine above.
   259  	select {
   260  	case <-ctx.Done():
   261  		return
   262  	default:
   263  		t.Fatal("test timeout")
   264  	}
   265  }
   266  
   267  func TestFeeRate(t *testing.T) {
   268  	maxInt := ^uint64(0)
   269  	maxWei := new(big.Int).SetUint64(maxInt)
   270  	gweiFactorBig := big.NewInt(dexeth.GweiFactor)
   271  	maxWei.Mul(maxWei, gweiFactorBig)
   272  	overMaxWei := new(big.Int).Set(maxWei)
   273  	overMaxWei.Add(overMaxWei, gweiFactorBig)
   274  	tests := []struct {
   275  		name             string
   276  		hdrBaseFee       *big.Int
   277  		hdrErr           error
   278  		suggGasTipCap    *big.Int
   279  		suggGasTipCapErr error
   280  		wantFee          uint64
   281  		wantErr          bool
   282  	}{{
   283  		name:          "ok zero",
   284  		hdrBaseFee:    new(big.Int),
   285  		suggGasTipCap: new(big.Int),
   286  		wantFee:       0,
   287  	}, {
   288  		name:          "ok rounded up",
   289  		hdrBaseFee:    big.NewInt(dexeth.GweiFactor - 1),
   290  		suggGasTipCap: new(big.Int),
   291  		wantFee:       2,
   292  	}, {
   293  		name:          "ok 100, 2",
   294  		hdrBaseFee:    big.NewInt(dexeth.GweiFactor * 100),
   295  		suggGasTipCap: big.NewInt(dexeth.GweiFactor * 2),
   296  		wantFee:       202,
   297  	}, {
   298  		name:          "over max int",
   299  		hdrBaseFee:    overMaxWei,
   300  		suggGasTipCap: big.NewInt(dexeth.GweiFactor * 2),
   301  		wantErr:       true,
   302  	}, {
   303  		name:          "node header err",
   304  		hdrBaseFee:    new(big.Int),
   305  		hdrErr:        errors.New(""),
   306  		suggGasTipCap: new(big.Int),
   307  		wantErr:       true,
   308  	}, {
   309  		name:          "nil base fee error",
   310  		hdrBaseFee:    nil,
   311  		suggGasTipCap: new(big.Int),
   312  		wantErr:       true,
   313  	}, {
   314  		name:             "node suggest gas tip cap err",
   315  		hdrBaseFee:       new(big.Int),
   316  		suggGasTipCapErr: errors.New(""),
   317  		wantErr:          true,
   318  	}}
   319  
   320  	for _, test := range tests {
   321  		eth, node := tNewBackend(BipID)
   322  		node.bestHdr = &types.Header{
   323  			BaseFee: test.hdrBaseFee,
   324  		}
   325  		node.bestHdrErr = test.hdrErr
   326  		node.suggGasTipCap = test.suggGasTipCap
   327  		node.suggGasTipCapErr = test.suggGasTipCapErr
   328  
   329  		fee, err := eth.FeeRate(tCtx)
   330  		if test.wantErr {
   331  			if err == nil {
   332  				t.Fatalf("expected error for test %q", test.name)
   333  			}
   334  			continue
   335  		}
   336  		if err != nil {
   337  			t.Fatalf("unexpected error for test %q: %v", test.name, err)
   338  		}
   339  		if fee != test.wantFee {
   340  			t.Fatalf("want fee %v got %v for test %q", test.wantFee, fee, test.name)
   341  		}
   342  	}
   343  }
   344  
   345  func TestSynced(t *testing.T) {
   346  	tests := []struct {
   347  		name                    string
   348  		syncProg                *ethereum.SyncProgress
   349  		subSecs                 uint64
   350  		bestHdrErr, syncProgErr error
   351  		wantErr, wantSynced     bool
   352  	}{{
   353  		name:       "ok synced",
   354  		subSecs:    dexeth.MaxBlockInterval - 1,
   355  		wantSynced: true,
   356  	}, {
   357  		name:    "ok header too old",
   358  		subSecs: dexeth.MaxBlockInterval,
   359  	}, {
   360  		name:       "best header error",
   361  		bestHdrErr: errors.New(""),
   362  		wantErr:    true,
   363  	}}
   364  
   365  	for _, test := range tests {
   366  		nowInSecs := uint64(time.Now().Unix())
   367  		eth, node := tNewBackend(BipID)
   368  		node.syncProg = test.syncProg
   369  		node.syncProgErr = test.syncProgErr
   370  		node.bestHdr = &types.Header{Time: nowInSecs - test.subSecs}
   371  		node.bestHdrErr = test.bestHdrErr
   372  
   373  		synced, err := eth.Synced()
   374  		if test.wantErr {
   375  			if err == nil {
   376  				t.Fatalf("expected error for test %q", test.name)
   377  			}
   378  			continue
   379  		}
   380  		if err != nil {
   381  			t.Fatalf("unexpected error for test %q: %v", test.name, err)
   382  		}
   383  		if synced != test.wantSynced {
   384  			t.Fatalf("want synced %v got %v for test %q", test.wantSynced, synced, test.name)
   385  		}
   386  	}
   387  }
   388  
   389  // TestRequiredOrderFunds ensures that a fee calculation in the calc package
   390  // will come up with the correct required funds.
   391  func TestRequiredOrderFunds(t *testing.T) {
   392  
   393  	initTxSize := dexeth.InitGas(1, ethContractVersion)
   394  	swapVal := uint64(1000000000) // gwei
   395  	numSwaps := uint64(17)        // swaps
   396  	feeRate := uint64(30)         // gwei / gas
   397  
   398  	// We want the fee calculation to simply be the cost of the gas used
   399  	// for each swap plus the initial value.
   400  	want := swapVal + (numSwaps * initTxSize * feeRate)
   401  	// Second argument called inputsSize same as another initSize.
   402  	got := calc.RequiredOrderFunds(swapVal, 0, numSwaps, initTxSize, initTxSize, feeRate)
   403  	if got != want {
   404  		t.Fatalf("want %v got %v for fees", want, got)
   405  	}
   406  }
   407  
   408  func tTx(gasFeeCap, gasTipCap, value uint64, to *common.Address, data []byte) *types.Transaction {
   409  	return types.NewTx(&types.DynamicFeeTx{
   410  		GasFeeCap: dexeth.GweiToWei(gasFeeCap),
   411  		GasTipCap: dexeth.GweiToWei(gasTipCap),
   412  		To:        to,
   413  		Value:     dexeth.GweiToWei(value),
   414  		Data:      data,
   415  	})
   416  }
   417  
   418  func TestContract(t *testing.T) {
   419  	receiverAddr, contractAddr := new(common.Address), new(common.Address)
   420  	copy(receiverAddr[:], encode.RandomBytes(20))
   421  	copy(contractAddr[:], encode.RandomBytes(20))
   422  	var txHash [32]byte
   423  	copy(txHash[:], encode.RandomBytes(32))
   424  	const gasPrice = 30
   425  	const gasTipCap = 2
   426  	const swapVal = 25e8
   427  	const txVal = 5e9
   428  	var secret, secretHash [32]byte
   429  	copy(secret[:], redeemSecretB)
   430  	copy(secretHash[:], redeemSecretHashB)
   431  	tests := []struct {
   432  		name           string
   433  		coinID         []byte
   434  		contract       []byte
   435  		tx             *types.Transaction
   436  		swap           *dexeth.SwapState
   437  		swapErr, txErr error
   438  		wantErr        bool
   439  	}{{
   440  		name:     "ok",
   441  		tx:       tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata),
   442  		contract: dexeth.EncodeContractData(0, secretHash),
   443  		swap:     tSwap(97, initLocktime, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr),
   444  		coinID:   txHash[:],
   445  	}, {
   446  		name:     "new coiner error, wrong tx type",
   447  		tx:       tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata),
   448  		contract: dexeth.EncodeContractData(0, secretHash),
   449  		swap:     tSwap(97, initLocktime, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr),
   450  		coinID:   txHash[1:],
   451  		wantErr:  true,
   452  	}, {
   453  		name:     "confirmations error, swap error",
   454  		tx:       tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata),
   455  		contract: dexeth.EncodeContractData(0, secretHash),
   456  		coinID:   txHash[:],
   457  		swapErr:  errors.New(""),
   458  		wantErr:  true,
   459  	}}
   460  	for _, test := range tests {
   461  		eth, node := tNewBackend(BipID)
   462  		node.tx = test.tx
   463  		node.txErr = test.txErr
   464  		node.swp = test.swap
   465  		node.swpErr = test.swapErr
   466  		eth.contractAddr = *contractAddr
   467  
   468  		contractData := dexeth.EncodeContractData(0, secretHash) // matches initCalldata
   469  		contract, err := eth.Contract(test.coinID, contractData)
   470  		if test.wantErr {
   471  			if err == nil {
   472  				t.Fatalf("expected error for test %q", test.name)
   473  			}
   474  			continue
   475  		}
   476  		if err != nil {
   477  			t.Fatalf("unexpected error for test %q: %v", test.name, err)
   478  		}
   479  		if contract.SwapAddress != initParticipantAddr.String() ||
   480  			contract.LockTime.Unix() != initLocktime {
   481  			t.Fatalf("returns do not match expected for test %q", test.name)
   482  		}
   483  	}
   484  }
   485  
   486  func TestValidateFeeRate(t *testing.T) {
   487  	swapCoin := swapCoin{
   488  		baseCoin: &baseCoin{
   489  			backend:   &AssetBackend{log: tLogger},
   490  			gasFeeCap: dexeth.GweiToWei(100),
   491  			gasTipCap: dexeth.GweiToWei(2),
   492  		},
   493  	}
   494  
   495  	contract := &asset.Contract{
   496  		Coin: &swapCoin,
   497  	}
   498  
   499  	eth, _ := tNewBackend(BipID)
   500  
   501  	if !eth.ValidateFeeRate(contract.Coin, 100) {
   502  		t.Fatalf("expected valid fee rate, but was not valid")
   503  	}
   504  
   505  	if eth.ValidateFeeRate(contract.Coin, 101) {
   506  		t.Fatalf("expected invalid fee rate, but was valid")
   507  	}
   508  
   509  	swapCoin.gasTipCap = dexeth.GweiToWei(dexeth.MinGasTipCap - 1)
   510  	if eth.ValidateFeeRate(contract.Coin, 100) {
   511  		t.Fatalf("expected invalid fee rate, but was valid")
   512  	}
   513  }
   514  
   515  func TestValidateSecret(t *testing.T) {
   516  	secret, blankHash := [32]byte{}, [32]byte{}
   517  	copy(secret[:], encode.RandomBytes(32))
   518  	secretHash := sha256.Sum256(secret[:])
   519  	tests := []struct {
   520  		name         string
   521  		contractData []byte
   522  		want         bool
   523  	}{{
   524  		name:         "ok",
   525  		contractData: dexeth.EncodeContractData(0, secretHash),
   526  		want:         true,
   527  	}, {
   528  		name:         "not the right hash",
   529  		contractData: dexeth.EncodeContractData(0, blankHash),
   530  	}, {
   531  		name: "bad contract data",
   532  	}}
   533  	for _, test := range tests {
   534  		eth, _ := tNewBackend(BipID)
   535  		got := eth.ValidateSecret(secret[:], test.contractData)
   536  		if test.want != got {
   537  			t.Fatalf("expected %v but got %v for test %q", test.want, got, test.name)
   538  		}
   539  	}
   540  }
   541  
   542  func TestRedemption(t *testing.T) {
   543  	receiverAddr, contractAddr := new(common.Address), new(common.Address)
   544  	copy(receiverAddr[:], encode.RandomBytes(20))
   545  	copy(contractAddr[:], encode.RandomBytes(20))
   546  	var secret, secretHash, txHash [32]byte
   547  	copy(secret[:], redeemSecretB)
   548  	copy(secretHash[:], redeemSecretHashB)
   549  	copy(txHash[:], encode.RandomBytes(32))
   550  	const gasPrice = 30
   551  	const gasTipCap = 2
   552  	tests := []struct {
   553  		name               string
   554  		coinID, contractID []byte
   555  		swp                *dexeth.SwapState
   556  		tx                 *types.Transaction
   557  		txIsMempool        bool
   558  		swpErr, txErr      error
   559  		wantErr            bool
   560  	}{{
   561  		name:       "ok",
   562  		tx:         tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata),
   563  		contractID: dexeth.EncodeContractData(0, secretHash),
   564  		coinID:     txHash[:],
   565  		swp:        tSwap(0, 0, 0, secret, dexeth.SSRedeemed, receiverAddr),
   566  	}, {
   567  		name:       "new coiner error, wrong tx type",
   568  		tx:         tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata),
   569  		contractID: dexeth.EncodeContractData(0, secretHash),
   570  		coinID:     txHash[1:],
   571  		wantErr:    true,
   572  	}, {
   573  		name:       "confirmations error, swap wrong state",
   574  		tx:         tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata),
   575  		contractID: dexeth.EncodeContractData(0, secretHash),
   576  		swp:        tSwap(0, 0, 0, secret, dexeth.SSRefunded, receiverAddr),
   577  		coinID:     txHash[:],
   578  		wantErr:    true,
   579  	}, {
   580  		name:       "validate redeem error",
   581  		tx:         tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata),
   582  		contractID: secretHash[:31],
   583  		coinID:     txHash[:],
   584  		swp:        tSwap(0, 0, 0, secret, dexeth.SSRedeemed, receiverAddr),
   585  		wantErr:    true,
   586  	}}
   587  	for _, test := range tests {
   588  		eth, node := tNewBackend(BipID)
   589  		node.tx = test.tx
   590  		node.txIsMempool = test.txIsMempool
   591  		node.txErr = test.txErr
   592  		node.swp = test.swp
   593  		node.swpErr = test.swpErr
   594  		eth.contractAddr = *contractAddr
   595  
   596  		_, err := eth.Redemption(test.coinID, nil, test.contractID)
   597  		if test.wantErr {
   598  			if err == nil {
   599  				t.Fatalf("expected error for test %q", test.name)
   600  			}
   601  			continue
   602  		}
   603  		if err != nil {
   604  			t.Fatalf("unexpected error for test %q: %v", test.name, err)
   605  		}
   606  	}
   607  }
   608  
   609  func TestTxData(t *testing.T) {
   610  	eth, node := tNewBackend(BipID)
   611  
   612  	const gasPrice = 30
   613  	const gasTipCap = 2
   614  	const value = 5e9
   615  	addr := randomAddress()
   616  	data := encode.RandomBytes(5)
   617  	tx := tTx(gasPrice, gasTipCap, value, addr, data)
   618  	goodCoinID, _ := hex.DecodeString("09c3bed75b35c6cf0549b0636c9511161b18765c019ef371e2a9f01e4b4a1487")
   619  	node.tx = tx
   620  
   621  	// initial success
   622  	txData, err := eth.TxData(goodCoinID)
   623  	if err != nil {
   624  		t.Fatalf("TxData error: %v", err)
   625  	}
   626  	checkB, _ := tx.MarshalBinary()
   627  	if !bytes.Equal(txData, checkB) {
   628  		t.Fatalf("tx data not transmitted")
   629  	}
   630  
   631  	// bad coin ID
   632  	coinID := encode.RandomBytes(2)
   633  	_, err = eth.TxData(coinID)
   634  	if err == nil {
   635  		t.Fatalf("no error for bad coin ID")
   636  	}
   637  
   638  	// Wrong type of coin ID
   639  	_, err = eth.TxData(goodCoinID[2:])
   640  	if err == nil {
   641  		t.Fatalf("no error for wrong coin type")
   642  	}
   643  
   644  	// No transaction
   645  	node.tx = nil
   646  	_, err = eth.TxData(goodCoinID)
   647  	if err == nil {
   648  		t.Fatalf("no error for missing tx")
   649  	}
   650  
   651  	// Success again
   652  	node.tx = tx
   653  	_, err = eth.TxData(goodCoinID)
   654  	if err != nil {
   655  		t.Fatalf("TxData error: %v", err)
   656  	}
   657  }
   658  
   659  func TestValidateContract(t *testing.T) {
   660  	t.Run("eth", func(t *testing.T) { testValidateContract(t, BipID) })
   661  	t.Run("token", func(t *testing.T) { testValidateContract(t, usdcID) })
   662  }
   663  
   664  func testValidateContract(t *testing.T, assetID uint32) {
   665  	tests := []struct {
   666  		name       string
   667  		ver        uint32
   668  		secretHash []byte
   669  		wantErr    bool
   670  	}{{
   671  		name:       "ok",
   672  		secretHash: make([]byte, dexeth.SecretHashSize),
   673  	}, {
   674  		name:       "wrong size",
   675  		secretHash: make([]byte, dexeth.SecretHashSize-1),
   676  		wantErr:    true,
   677  	}, {
   678  		name:       "wrong version",
   679  		ver:        1,
   680  		secretHash: make([]byte, dexeth.SecretHashSize),
   681  		wantErr:    true,
   682  	}}
   683  
   684  	type contractValidator interface {
   685  		ValidateContract([]byte) error
   686  	}
   687  
   688  	for _, test := range tests {
   689  		eth, _ := tNewBackend(assetID)
   690  		var cv contractValidator
   691  		if assetID == BipID {
   692  			cv = &ETHBackend{eth}
   693  		} else {
   694  			cv = &TokenBackend{
   695  				AssetBackend: eth,
   696  				VersionedToken: &VersionedToken{
   697  					Token: dexeth.Tokens[usdcID],
   698  					Ver:   0,
   699  				},
   700  			}
   701  		}
   702  
   703  		swapData := make([]byte, 4+len(test.secretHash))
   704  		binary.BigEndian.PutUint32(swapData[:4], test.ver)
   705  		copy(swapData[4:], test.secretHash)
   706  
   707  		err := cv.ValidateContract(swapData)
   708  		if test.wantErr {
   709  			if err == nil {
   710  				t.Fatalf("expected error for test %q", test.name)
   711  			}
   712  			continue
   713  		}
   714  		if err != nil {
   715  			t.Fatalf("unexpected error for test %q: %v", test.name, err)
   716  		}
   717  	}
   718  }
   719  
   720  func TestAccountBalance(t *testing.T) {
   721  	eth, node := tNewBackend(BipID)
   722  
   723  	const gweiBal = 1e9
   724  	bigBal := big.NewInt(gweiBal)
   725  	node.acctBal = bigBal.Mul(bigBal, big.NewInt(dexeth.GweiFactor))
   726  
   727  	// Initial success
   728  	bal, err := eth.AccountBalance("")
   729  	if err != nil {
   730  		t.Fatalf("AccountBalance error: %v", err)
   731  	}
   732  
   733  	if bal != gweiBal {
   734  		t.Fatalf("wrong balance. expected %f, got %d", gweiBal, bal)
   735  	}
   736  
   737  	// Only error path.
   738  	node.acctBalErr = errors.New("test error")
   739  	_, err = eth.AccountBalance("")
   740  	if err == nil {
   741  		t.Fatalf("no AccountBalance error when expected")
   742  	}
   743  	node.acctBalErr = nil
   744  
   745  	// Success again
   746  	_, err = eth.AccountBalance("")
   747  	if err != nil {
   748  		t.Fatalf("AccountBalance error: %v", err)
   749  	}
   750  }
   751  
   752  func TestPoll(t *testing.T) {
   753  	tests := []struct {
   754  		name        string
   755  		addBlock    bool
   756  		blockNumErr error
   757  	}{{
   758  		name: "ok nothing to do",
   759  	}, {
   760  		name:     "ok new",
   761  		addBlock: true,
   762  	}, {
   763  		name:        "blockNumber error",
   764  		blockNumErr: errors.New(""),
   765  	}}
   766  
   767  	for _, test := range tests {
   768  		be, node := tNewBackend(BipID)
   769  		eth := &ETHBackend{be}
   770  		node.blkNumErr = test.blockNumErr
   771  		if test.addBlock {
   772  			node.blkNum = be.bestHeight + 1
   773  		} else {
   774  			node.blkNum = be.bestHeight
   775  		}
   776  		ch := make(chan *asset.BlockUpdate, 1)
   777  		eth.blockChans[ch] = struct{}{}
   778  		bu := new(asset.BlockUpdate)
   779  		wait := make(chan struct{})
   780  		go func() {
   781  			select {
   782  			case bu = <-ch:
   783  			case <-time.After(time.Second * 2):
   784  			}
   785  			close(wait)
   786  		}()
   787  		eth.poll(nil)
   788  		<-wait
   789  		if test.blockNumErr != nil {
   790  			if bu.Err == nil {
   791  				t.Fatalf("expected error for test %q", test.name)
   792  			}
   793  			continue
   794  		}
   795  		if bu.Err != nil {
   796  			t.Fatalf("unexpected error for test %q: %v", test.name, bu.Err)
   797  		}
   798  	}
   799  }
   800  
   801  func TestValidateSignature(t *testing.T) {
   802  	// "ok" values used are the same as tests in client/assets/eth.
   803  	pkBytes := mustParseHex("04b911d1f39f7792e165767e35aa134083e2f70ac7de6945d7641a3015d09a54561b71112b8d60f63831f0e62c23c6921ec627820afedf8236155b9e9bd82b6523")
   804  	msg := []byte("msg")
   805  	sigBytes := mustParseHex("ffd26911d3fdaf11ac44801744f2df015a16539b6e688aff4cabc092b747466e7bc8036a03d1479a1570dd11bf042120301c34a65b237267720ef8a9e56f2eb1")
   806  	max32Bytes := mustParseHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
   807  	addr := "0x2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27"
   808  	eth := new(AssetBackend)
   809  
   810  	tests := []struct {
   811  		name                   string
   812  		wantErr                bool
   813  		pkBytes, sigBytes, msg []byte
   814  		addr                   string
   815  	}{{
   816  		name:     "ok",
   817  		pkBytes:  pkBytes,
   818  		msg:      msg,
   819  		addr:     addr,
   820  		sigBytes: sigBytes,
   821  	}, {
   822  		name:    "sig wrong size",
   823  		pkBytes: pkBytes,
   824  		msg:     msg,
   825  		addr:    addr,
   826  		wantErr: true,
   827  	}, {
   828  		name:     "pubkey doesn't match address",
   829  		pkBytes:  pkBytes,
   830  		msg:      msg,
   831  		addr:     addr[:21] + "a",
   832  		sigBytes: sigBytes,
   833  		wantErr:  true,
   834  	}, {
   835  		name:     "bad pubkey",
   836  		pkBytes:  pkBytes[1:],
   837  		msg:      msg,
   838  		sigBytes: sigBytes,
   839  		addr:     addr,
   840  		wantErr:  true,
   841  	}, {
   842  		name:     "r too big",
   843  		pkBytes:  pkBytes,
   844  		msg:      msg,
   845  		sigBytes: append(append([]byte{}, max32Bytes...), sigBytes[32:]...),
   846  		addr:     addr,
   847  		wantErr:  true,
   848  	}, {
   849  		name:     "s too big",
   850  		pkBytes:  pkBytes,
   851  		msg:      msg,
   852  		sigBytes: append(append(append([]byte{}, sigBytes[:32]...), max32Bytes...), byte(1)),
   853  		addr:     addr,
   854  		wantErr:  true,
   855  	}, {
   856  		name:     "cannot verify signature, bad msg",
   857  		pkBytes:  pkBytes,
   858  		sigBytes: sigBytes,
   859  		addr:     addr,
   860  		wantErr:  true,
   861  	}}
   862  
   863  	for _, test := range tests {
   864  		err := eth.ValidateSignature(test.addr, test.pkBytes, test.msg, test.sigBytes)
   865  		if test.wantErr {
   866  			if err == nil {
   867  				t.Fatalf("expected error for test %q", test.name)
   868  			}
   869  			continue
   870  		}
   871  		if err != nil {
   872  			t.Fatalf("unexpected error for test %q: %v", test.name, err)
   873  		}
   874  	}
   875  }
   876  
   877  func TestIsRemoteURL(t *testing.T) {
   878  	for _, tt := range []struct {
   879  		url        string
   880  		wantRemote bool
   881  	}{
   882  		{"http://localhost:1234", false},
   883  		{"http://127.0.0.1", false},
   884  		{"http://127.0.0.1:1234", false},
   885  		{"https://127.0.0.1:1234", false},
   886  		{"https://decred.org", true},
   887  		{"https://241.45.173.171", true},
   888  		{"http://[::1]:8080", false},
   889  		{"https://[2001:db8::1]:8080", true},
   890  		{"https://241.45.173.171:1234", true},
   891  	} {
   892  		uri, _ := url.Parse(tt.url)
   893  		if is := isRemoteURL(uri); is != tt.wantRemote {
   894  			t.Fatalf("%s: wanted %t, got %t", tt.url, tt.wantRemote, is)
   895  		}
   896  	}
   897  }
   898  
   899  func TestParseEndpoints(t *testing.T) {
   900  	type test struct {
   901  		name              string
   902  		fileContents      string
   903  		relayAddr         string
   904  		expectedEndpoints []string
   905  		wantErr           bool
   906  	}
   907  
   908  	url1 := "http://127.0.0.1:1234"
   909  	url2 := "https://example.com"
   910  	relayAddr := "123.111.4.8:1111"
   911  	relayURL := "http://" + relayAddr
   912  
   913  	tests := []*test{
   914  		{
   915  			name:              "single localhost in file",
   916  			fileContents:      url1,
   917  			expectedEndpoints: []string{"http://127.0.0.1:1234"},
   918  		},
   919  		{
   920  			name:    "no path provided error",
   921  			wantErr: true,
   922  		},
   923  		{
   924  			name:              "two from file and a noderelay",
   925  			fileContents:      url1 + "\n" + url2,
   926  			relayAddr:         relayAddr,
   927  			expectedEndpoints: []string{relayURL, url1, url2},
   928  		},
   929  		{
   930  			name:              "just a relay adddress",
   931  			relayAddr:         relayAddr,
   932  			expectedEndpoints: []string{relayURL},
   933  		},
   934  	}
   935  
   936  	runTest := func(t *testing.T, tt *test) {
   937  		var configPath string
   938  		if tt.fileContents != "" {
   939  			f, err := os.CreateTemp("", "")
   940  			if err != nil {
   941  				t.Fatalf("error getting temporary file")
   942  			}
   943  			configPath = f.Name()
   944  			defer os.Remove(configPath)
   945  			defer f.Close()
   946  			f.WriteString(tt.fileContents)
   947  		}
   948  		endpoints, err := parseEndpoints(&asset.BackendConfig{
   949  			ConfigPath: configPath,
   950  			RelayAddr:  tt.relayAddr,
   951  		})
   952  		if err != nil {
   953  			if tt.wantErr {
   954  				return
   955  			}
   956  			t.Fatalf("parseEndpoints error: %v", err)
   957  		}
   958  		if len(endpoints) != len(tt.expectedEndpoints) {
   959  			t.Fatalf("wrong number of endpoints. wanted %d, got %d", len(tt.expectedEndpoints), len(endpoints))
   960  		}
   961  		for i, pt := range endpoints {
   962  			if expURL := tt.expectedEndpoints[i]; pt.url != expURL {
   963  				t.Fatalf("wrong endpoint at index %d: wanted %s, got %s", i, expURL, pt.url)
   964  			}
   965  		}
   966  	}
   967  	for _, tt := range tests {
   968  		t.Run(tt.name, func(t *testing.T) {
   969  			runTest(t, tt)
   970  		})
   971  	}
   972  }