decred.org/dcrdex@v1.0.5/client/asset/eth/eth_test.go (about) 1 //go:build !harness && !rpclive 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/ecdsa" 11 "crypto/sha256" 12 "encoding/hex" 13 "errors" 14 "fmt" 15 "math/big" 16 "math/rand" 17 "sort" 18 "strings" 19 "sync" 20 "testing" 21 "time" 22 23 "decred.org/dcrdex/client/asset" 24 "decred.org/dcrdex/dex" 25 "decred.org/dcrdex/dex/config" 26 "decred.org/dcrdex/dex/encode" 27 dexeth "decred.org/dcrdex/dex/networks/eth" 28 swapv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0" 29 "github.com/ethereum/go-ethereum" 30 "github.com/ethereum/go-ethereum/accounts" 31 "github.com/ethereum/go-ethereum/accounts/abi/bind" 32 "github.com/ethereum/go-ethereum/common" 33 "github.com/ethereum/go-ethereum/core/types" 34 "github.com/ethereum/go-ethereum/crypto" 35 "github.com/ethereum/go-ethereum/params" 36 ) 37 38 const ( 39 txConfsNeededToConfirm = 3 40 ) 41 42 var ( 43 _ ethFetcher = (*testNode)(nil) 44 tLogger = dex.StdOutLogger("ETHTEST", dex.LevelTrace) 45 46 testAddressA = common.HexToAddress("dd93b447f7eBCA361805eBe056259853F3912E04") 47 testAddressB = common.HexToAddress("8d83B207674bfd53B418a6E47DA148F5bFeCc652") 48 testAddressC = common.HexToAddress("2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27") 49 50 ethGases = dexeth.VersionedGases[0] 51 tokenGases = dexeth.Tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas 52 53 tETH = &dex.Asset{ 54 // Version meaning? 55 ID: 60, 56 Symbol: "ETH", 57 MaxFeeRate: 100, 58 SwapConf: 1, 59 } 60 61 tBTC = &dex.Asset{ 62 ID: 0, 63 Symbol: "btc", 64 Version: 0, // match btc.version 65 MaxFeeRate: 10, 66 SwapConf: 1, 67 } 68 69 tToken = &dex.Asset{ 70 ID: usdcTokenID, 71 Symbol: "usdc.eth", 72 Version: 0, 73 MaxFeeRate: 20, 74 SwapConf: 1, 75 } 76 77 signer = types.LatestSigner(params.AllEthashProtocolChanges) 78 79 // simBackend = backends.NewSimulatedBackend(core.GenesisAlloc{ 80 // testAddressA: core.GenesisAccount{Balance: dexeth.GweiToWei(5e10)}, 81 // }, 1e9) 82 ) 83 84 type tGetTxRes struct { 85 tx *types.Transaction 86 height int64 87 } 88 89 type testNode struct { 90 acct *accounts.Account 91 addr common.Address 92 connectErr error 93 bestHdr *types.Header 94 bestHdrErr error 95 syncProg ethereum.SyncProgress 96 syncProgT uint64 97 syncProgErr error 98 bal *big.Int 99 balErr error 100 signDataErr error 101 privKey *ecdsa.PrivateKey 102 swapVers map[uint32]struct{} // For SwapConfirmations -> swap. TODO for other contractor methods 103 swapMap map[[32]byte]*dexeth.SwapState 104 refundable bool 105 baseFee *big.Int 106 tip *big.Int 107 netFeeStateErr error 108 confNonce uint64 109 confNonceErr error 110 getTxRes *types.Transaction 111 getTxResMap map[common.Hash]*tGetTxRes 112 getTxHeight int64 113 getTxErr error 114 receipt *types.Receipt 115 receiptTx *types.Transaction 116 receiptErr error 117 receipts map[common.Hash]*types.Receipt 118 receiptTxs map[common.Hash]*types.Transaction 119 receiptErrs map[common.Hash]error 120 hdrByHash *types.Header 121 lastSignedTx *types.Transaction 122 sentTxs int 123 sendTxTx *types.Transaction 124 sendTxErr error 125 simBackend bind.ContractBackend 126 maxFeeRate *big.Int 127 tContractor *tContractor 128 tokenContractor *tTokenContractor 129 contractor contractor 130 tokenParent *assetWallet // only set for tokens 131 txConfirmations map[common.Hash]uint32 132 txConfsErr map[common.Hash]error 133 } 134 135 func newBalance(current, in, out uint64) *Balance { 136 return &Balance{ 137 Current: dexeth.GweiToWei(current), 138 PendingIn: dexeth.GweiToWei(in), 139 PendingOut: dexeth.GweiToWei(out), 140 } 141 } 142 143 func (n *testNode) newTransaction(nonce uint64, value *big.Int) *types.Transaction { 144 to := common.BytesToAddress(encode.RandomBytes(20)) 145 tx, err := types.SignTx(types.NewTx(&types.DynamicFeeTx{ 146 Nonce: nonce, 147 Value: value, 148 Gas: 50_000, 149 To: &to, 150 ChainID: n.chainConfig().ChainID, 151 }), signer, n.privKey) 152 if err != nil { 153 panic("tx signing error") 154 } 155 return tx 156 } 157 158 func (n *testNode) address() common.Address { 159 return n.addr 160 } 161 162 func (n *testNode) connect(ctx context.Context) error { 163 return n.connectErr 164 } 165 166 func (n *testNode) contractBackend() bind.ContractBackend { 167 return n.simBackend 168 } 169 170 func (n *testNode) chainConfig() *params.ChainConfig { 171 return params.AllEthashProtocolChanges 172 } 173 174 func (n *testNode) getConfirmedNonce(context.Context) (uint64, error) { 175 return n.confNonce, n.confNonceErr 176 } 177 178 func (n *testNode) getTransaction(ctx context.Context, hash common.Hash) (*types.Transaction, int64, error) { 179 if n.getTxErr != nil { 180 return nil, 0, n.getTxErr 181 } 182 183 if n.getTxResMap != nil { 184 if tx, ok := n.getTxResMap[hash]; ok { 185 return tx.tx, tx.height, nil 186 } else { 187 return nil, 0, asset.CoinNotFoundError 188 } 189 } 190 191 return n.getTxRes, n.getTxHeight, n.getTxErr 192 } 193 194 func (n *testNode) txOpts(ctx context.Context, val, maxGas uint64, maxFeeRate, tipRate, nonce *big.Int) (*bind.TransactOpts, error) { 195 if maxFeeRate == nil { 196 maxFeeRate = n.maxFeeRate 197 } 198 txOpts := newTxOpts(ctx, n.addr, val, maxGas, maxFeeRate, dexeth.GweiToWei(2)) 199 txOpts.Nonce = big.NewInt(1) 200 return txOpts, nil 201 } 202 203 func (n *testNode) currentFees(ctx context.Context) (baseFees, tipCap *big.Int, err error) { 204 return n.baseFee, n.tip, n.netFeeStateErr 205 } 206 207 func (n *testNode) shutdown() {} 208 209 func (n *testNode) bestHeader(ctx context.Context) (*types.Header, error) { 210 return n.bestHdr, n.bestHdrErr 211 } 212 func (n *testNode) addressBalance(ctx context.Context, addr common.Address) (*big.Int, error) { 213 return n.bal, n.balErr 214 } 215 func (n *testNode) unlock(pw string) error { 216 return nil 217 } 218 func (n *testNode) lock() error { 219 return nil 220 } 221 func (n *testNode) locked() bool { 222 return false 223 } 224 func (n *testNode) syncProgress(context.Context) (prog *ethereum.SyncProgress, bestBlockUNIXTime uint64, err error) { 225 return &n.syncProg, n.syncProgT, n.syncProgErr 226 } 227 func (n *testNode) peerCount() uint32 { 228 return 1 229 } 230 231 func (n *testNode) signData(data []byte) (sig, pubKey []byte, err error) { 232 if n.signDataErr != nil { 233 return nil, nil, n.signDataErr 234 } 235 236 if n.privKey == nil { 237 return nil, nil, nil 238 } 239 240 sig, err = crypto.Sign(crypto.Keccak256(data), n.privKey) 241 if err != nil { 242 return nil, nil, err 243 } 244 245 return sig, crypto.FromECDSAPub(&n.privKey.PublicKey), nil 246 } 247 func (n *testNode) sendTransaction(ctx context.Context, txOpts *bind.TransactOpts, to common.Address, data []byte, filts ...acceptabilityFilter) (*types.Transaction, error) { 248 n.sentTxs++ 249 return n.sendTxTx, n.sendTxErr 250 } 251 func (n *testNode) sendSignedTransaction(ctx context.Context, tx *types.Transaction, filts ...acceptabilityFilter) error { 252 n.lastSignedTx = tx 253 return nil 254 } 255 256 func tTx(gasFeeCap, gasTipCap, value uint64, to *common.Address, data []byte, gasLimit uint64) *types.Transaction { 257 return types.NewTx(&types.DynamicFeeTx{ 258 GasFeeCap: dexeth.GweiToWei(gasFeeCap), 259 GasTipCap: dexeth.GweiToWei(gasTipCap), 260 To: to, 261 Value: dexeth.GweiToWei(value), 262 Data: data, 263 Gas: gasLimit, 264 }) 265 } 266 267 func (n *testNode) transactionConfirmations(_ context.Context, txHash common.Hash) (uint32, error) { 268 if n.txConfirmations == nil { 269 return 0, nil 270 } 271 return n.txConfirmations[txHash], n.txConfsErr[txHash] 272 } 273 274 func (n *testNode) headerByHash(_ context.Context, txHash common.Hash) (*types.Header, error) { 275 return n.hdrByHash, nil 276 } 277 278 func (n *testNode) transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { 279 if n.receiptErr != nil { 280 return nil, n.receiptErr 281 } 282 if n.receipt != nil { 283 return n.receipt, nil 284 } 285 286 return n.receipts[txHash], n.receiptErrs[txHash] 287 } 288 289 func (n *testNode) transactionAndReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, *types.Transaction, error) { 290 if n.receiptErr != nil { 291 return nil, nil, n.receiptErr 292 } 293 if n.receipt != nil { 294 return n.receipt, n.receiptTx, nil 295 } 296 297 return n.receipts[txHash], n.receiptTxs[txHash], n.receiptErrs[txHash] 298 } 299 300 func (n *testNode) nonce(ctx context.Context) (*big.Int, *big.Int, error) { 301 return big.NewInt(0), big.NewInt(1), nil 302 } 303 304 func (n *testNode) setBalanceError(w *assetWallet, err error) { 305 n.balErr = err 306 n.tokenContractor.balErr = err 307 w.balances.m = nil 308 } 309 310 type tMempoolNode struct { 311 *testNode 312 pendingTxs []*types.Transaction 313 } 314 315 func (n *tMempoolNode) pendingTransactions() ([]*types.Transaction, error) { 316 return n.pendingTxs, nil 317 } 318 319 type tContractor struct { 320 gasEstimates *dexeth.Gases 321 swapMap map[[32]byte]*dexeth.SwapState 322 swapErr error 323 initTx *types.Transaction 324 initErr error 325 redeemTx *types.Transaction 326 redeemErr error 327 refundTx *types.Transaction 328 refundErr error 329 initGasErr error 330 redeemGasErr error 331 refundGasErr error 332 redeemGasOverride *uint64 333 valueIn map[common.Hash]uint64 334 valueOut map[common.Hash]uint64 335 valueErr error 336 refundable bool 337 refundableErr error 338 lastRedeemOpts *bind.TransactOpts 339 lastRedeems []*asset.Redemption 340 lastRefund struct { 341 // tx *types.Transaction 342 secretHash [32]byte 343 maxFeeRate *big.Int 344 // contractVer uint32 345 } 346 } 347 348 func (c *tContractor) swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) { 349 if c.swapErr != nil { 350 return nil, c.swapErr 351 } 352 swap, ok := c.swapMap[secretHash] 353 if !ok { 354 return nil, errors.New("swap not in map") 355 } 356 return swap, nil 357 } 358 359 func (c *tContractor) initiate(*bind.TransactOpts, []*asset.Contract) (*types.Transaction, error) { 360 return c.initTx, c.initErr 361 } 362 363 func (c *tContractor) redeem(txOpts *bind.TransactOpts, redeems []*asset.Redemption) (*types.Transaction, error) { 364 c.lastRedeemOpts = txOpts 365 c.lastRedeems = redeems 366 return c.redeemTx, c.redeemErr 367 } 368 369 func (c *tContractor) refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { 370 c.lastRefund.secretHash = secretHash 371 c.lastRefund.maxFeeRate = opts.GasFeeCap 372 return c.refundTx, c.refundErr 373 } 374 375 func (c *tContractor) estimateInitGas(ctx context.Context, n int) (uint64, error) { 376 return c.gasEstimates.SwapN(n), c.initGasErr 377 } 378 379 func (c *tContractor) estimateRedeemGas(ctx context.Context, secrets [][32]byte) (uint64, error) { 380 if c.redeemGasOverride != nil { 381 return *c.redeemGasOverride, nil 382 } 383 return c.gasEstimates.RedeemN(len(secrets)), c.redeemGasErr 384 } 385 386 func (c *tContractor) estimateRefundGas(ctx context.Context, secretHash [32]byte) (uint64, error) { 387 return c.gasEstimates.Refund, c.refundGasErr 388 } 389 390 func (c *tContractor) value(_ context.Context, tx *types.Transaction) (incoming, outgoing uint64, err error) { 391 return c.valueIn[tx.Hash()], c.valueOut[tx.Hash()], c.valueErr 392 } 393 394 func (c *tContractor) isRefundable(secretHash [32]byte) (bool, error) { 395 return c.refundable, c.refundableErr 396 } 397 398 func (c *tContractor) voidUnusedNonce() {} 399 400 type tTokenContractor struct { 401 *tContractor 402 tokenAddr common.Address 403 bal *big.Int 404 balErr error 405 allow *big.Int 406 allowErr error 407 approveTx *types.Transaction 408 approved bool 409 approveErr error 410 approveEstimate uint64 411 approveEstimateErr error 412 transferTx *types.Transaction 413 transferErr error 414 transferEstimate uint64 415 transferEstimateErr error 416 } 417 418 var _ tokenContractor = (*tTokenContractor)(nil) 419 420 func (c *tTokenContractor) tokenAddress() common.Address { 421 return c.tokenAddr 422 } 423 424 func (c *tTokenContractor) balance(context.Context) (*big.Int, error) { 425 return c.bal, c.balErr 426 } 427 428 func (c *tTokenContractor) allowance(context.Context) (*big.Int, error) { 429 return c.allow, c.allowErr 430 } 431 432 func (c *tTokenContractor) approve(*bind.TransactOpts, *big.Int) (*types.Transaction, error) { 433 if c.approveErr == nil { 434 c.approved = true 435 } 436 return c.approveTx, c.approveErr 437 } 438 439 func (c *tTokenContractor) estimateApproveGas(context.Context, *big.Int) (uint64, error) { 440 return c.approveEstimate, c.approveEstimateErr 441 } 442 443 func (c *tTokenContractor) transfer(*bind.TransactOpts, common.Address, *big.Int) (*types.Transaction, error) { 444 return c.transferTx, c.transferErr 445 } 446 447 func (c *tTokenContractor) parseTransfer(*types.Receipt) (uint64, error) { 448 return 0, nil 449 } 450 451 func (c *tTokenContractor) estimateTransferGas(context.Context, *big.Int) (uint64, error) { 452 return c.transferEstimate, c.transferEstimateErr 453 } 454 455 type tTxDB struct { 456 storeTxCalled bool 457 storeTxErr error 458 removeTxCalled bool 459 removeTxErr error 460 txToGet *extendedWalletTx 461 getTxErr error 462 } 463 464 var _ txDB = (*tTxDB)(nil) 465 466 func (db *tTxDB) Connect(ctx context.Context) (*sync.WaitGroup, error) { 467 return &sync.WaitGroup{}, nil 468 } 469 func (db *tTxDB) storeTx(wt *extendedWalletTx) error { 470 db.storeTxCalled = true 471 return db.storeTxErr 472 } 473 func (db *tTxDB) removeTx(_ /* id */ string) error { 474 db.removeTxCalled = true 475 return db.removeTxErr 476 } 477 func (db *tTxDB) getTxs(n int, refID *common.Hash, past bool, tokenID *uint32) ([]*asset.WalletTransaction, error) { 478 return nil, nil 479 } 480 481 // getTx gets a single transaction. It is not an error if the tx is not known. 482 // In that case, a nil tx is returned. 483 func (db *tTxDB) getTx(txHash common.Hash) (tx *extendedWalletTx, _ error) { 484 return db.txToGet, db.getTxErr 485 } 486 func (db *tTxDB) getPendingTxs() ([]*extendedWalletTx, error) { 487 return nil, nil 488 } 489 func (db *tTxDB) close() error { 490 return nil 491 } 492 493 // func TestCheckUnconfirmedTxs(t *testing.T) { 494 // const tipHeight = 50 495 // const baseFeeGwei = 100 496 // const gasTipCapGwei = 2 497 498 // type tExtendedWalletTx struct { 499 // wt *extendedWalletTx 500 // confs uint32 501 // gasUsed uint64 502 // txReceiptErr error 503 // } 504 505 // newExtendedWalletTx := func(assetID uint32, nonce int64, maxFees uint64, currBlockNumber uint64, txReceiptConfs uint32, 506 // txReceiptGasUsed uint64, txReceiptErr error, timeStamp int64, savedToDB bool) *tExtendedWalletTx { 507 // var tokenID *uint32 508 // if assetID != BipID { 509 // tokenID = &assetID 510 // } 511 512 // return &tExtendedWalletTx{ 513 // wt: &extendedWalletTx{ 514 // WalletTransaction: &asset.WalletTransaction{ 515 // BlockNumber: currBlockNumber, 516 // TokenID: tokenID, 517 // Fees: maxFees, 518 // }, 519 // SubmissionTime: uint64(timeStamp), 520 // nonce: big.NewInt(nonce), 521 // savedToDB: savedToDB, 522 // }, 523 // confs: txReceiptConfs, 524 // gasUsed: txReceiptGasUsed, 525 // txReceiptErr: txReceiptErr, 526 // } 527 // } 528 529 // gasFee := func(gasUsed uint64) uint64 { 530 // return gasUsed * (baseFeeGwei + gasTipCapGwei) 531 // } 532 533 // now := time.Now().Unix() 534 535 // tests := []struct { 536 // name string 537 // assetID uint32 538 // unconfirmedTxs []*tExtendedWalletTx 539 // confirmedNonce uint64 540 541 // expTxsAfter []*extendedWalletTx 542 // expStoreTxCalled bool 543 // expRemoveTxCalled bool 544 // storeTxErr error 545 // removeTxErr error 546 // }{ 547 // { 548 // name: "coin not found", 549 // assetID: BipID, 550 // unconfirmedTxs: []*tExtendedWalletTx{ 551 // newExtendedWalletTx(BipID, 5, 1e7, 0, 0, 0, asset.CoinNotFoundError, now, true), 552 // }, 553 // expTxsAfter: []*extendedWalletTx{ 554 // newExtendedWalletTx(BipID, 5, 1e7, 0, 0, 0, asset.CoinNotFoundError, now, true).wt, 555 // }, 556 // }, 557 // { 558 // name: "still in mempool", 559 // assetID: BipID, 560 // unconfirmedTxs: []*tExtendedWalletTx{ 561 // newExtendedWalletTx(BipID, 5, 1e7, 0, 0, 0, nil, now, true), 562 // }, 563 // expTxsAfter: []*extendedWalletTx{ 564 // newExtendedWalletTx(BipID, 5, 1e7, 0, 0, 0, nil, now, true).wt, 565 // }, 566 // }, 567 // { 568 // name: "1 confirmation", 569 // assetID: BipID, 570 // unconfirmedTxs: []*tExtendedWalletTx{ 571 // newExtendedWalletTx(BipID, 5, 1e7, 0, 1, 6e5, nil, now, true), 572 // }, 573 // expTxsAfter: []*extendedWalletTx{ 574 // newExtendedWalletTx(BipID, 5, gasFee(6e5), tipHeight, 0, 0, nil, now, true).wt, 575 // }, 576 // expStoreTxCalled: true, 577 // }, 578 // { 579 // name: "3 confirmations", 580 // assetID: BipID, 581 // unconfirmedTxs: []*tExtendedWalletTx{ 582 // newExtendedWalletTx(BipID, 1, gasFee(6e5), tipHeight, 3, 6e5, nil, now, true), 583 // }, 584 // expTxsAfter: []*extendedWalletTx{}, 585 // expStoreTxCalled: true, 586 // }, 587 // { 588 // name: "3 confirmations, leave in unconfirmed txs if txDB.storeTx fails", 589 // assetID: BipID, 590 // unconfirmedTxs: []*tExtendedWalletTx{ 591 // newExtendedWalletTx(BipID, 5, gasFee(6e5), tipHeight-2, 3, 6e5, nil, now, true), 592 // }, 593 // expTxsAfter: []*extendedWalletTx{ 594 // newExtendedWalletTx(BipID, 5, gasFee(6e5), tipHeight-2, 3, 6e5, nil, now, true).wt, 595 // }, 596 // expStoreTxCalled: true, 597 // storeTxErr: errors.New("test error"), 598 // }, 599 // { 600 // name: "was confirmed but now not found", 601 // assetID: BipID, 602 // unconfirmedTxs: []*tExtendedWalletTx{ 603 // newExtendedWalletTx(BipID, 5, 1e7, tipHeight-1, 0, 0, asset.CoinNotFoundError, now, true), 604 // }, 605 // expTxsAfter: []*extendedWalletTx{ 606 // newExtendedWalletTx(BipID, 5, 1e7, 0, 0, 0, asset.CoinNotFoundError, now, true).wt, 607 // }, 608 // expStoreTxCalled: true, 609 // }, 610 // } 611 612 // for _, tt := range tests { 613 // t.Run(tt.name, func(t *testing.T) { 614 // _, eth, node, shutdown := tassetWallet(tt.assetID) 615 // defer shutdown() 616 617 // node.tokenContractor.bal = unlimitedAllowance 618 // node.receipts = make(map[common.Hash]*types.Receipt) 619 // node.receiptTxs = make(map[common.Hash]*types.Transaction) 620 // node.receiptErrs = make(map[common.Hash]error) 621 // node.hdrByHash = &types.Header{ 622 // BaseFee: dexeth.GweiToWei(baseFeeGwei), 623 // } 624 // node.confNonce = tt.confirmedNonce 625 // eth.connected.Store(true) 626 // eth.tipMtx.Lock() 627 // eth.currentTip = &types.Header{Number: new(big.Int).SetUint64(tipHeight)} 628 // eth.tipMtx.Unlock() 629 630 // txDB := &tTxDB{ 631 // storeTxErr: tt.storeTxErr, 632 // removeTxErr: tt.removeTxErr, 633 // } 634 // eth.txDB = txDB 635 636 // for _, pt := range tt.unconfirmedTxs { 637 // txHash := common.BytesToHash(encode.RandomBytes(32)) 638 // pt.wt.ID = txHash.String() 639 // pt.wt.txHash = txHash 640 // eth.pendingTxs = append(eth.pendingTxs, pt.wt) 641 // var blockNumber *big.Int 642 // if pt.confs > 0 { 643 // blockNumber = big.NewInt(int64(tipHeight - pt.confs + 1)) 644 // } 645 // node.receipts[txHash] = &types.Receipt{BlockNumber: blockNumber, GasUsed: pt.gasUsed} 646 647 // node.receiptTxs[txHash] = types.NewTx(&types.DynamicFeeTx{ 648 // GasTipCap: dexeth.GweiToWei(gasTipCapGwei), 649 // GasFeeCap: dexeth.GweiToWei(2 * baseFeeGwei), 650 // }) 651 // node.receiptErrs[txHash] = pt.txReceiptErr 652 // } 653 654 // eth.checkPendingTxs() 655 656 // if len(eth.pendingTxs) != len(tt.expTxsAfter) { 657 // t.Fatalf("expected %d unconfirmed txs, got %d", len(tt.expTxsAfter), len(eth.pendingTxs)) 658 // } 659 // for i, expTx := range tt.expTxsAfter { 660 // tx := eth.pendingTxs[i] 661 // if expTx.nonce.Cmp(tx.nonce) != 0 { 662 // t.Fatalf("expected tx index %d to have nonce %d, not %d", i, expTx.nonce, tx.nonce) 663 // } 664 // } 665 666 // if txDB.storeTxCalled != tt.expStoreTxCalled { 667 // t.Fatalf("expected storeTx called %v, got %v", tt.expStoreTxCalled, txDB.storeTxCalled) 668 // } 669 // if txDB.removeTxCalled != tt.expRemoveTxCalled { 670 // t.Fatalf("expected removeTx called %v, got %v", tt.expRemoveTxCalled, txDB.removeTxCalled) 671 // } 672 // }) 673 // } 674 // } 675 676 func TestCheckPendingTxs(t *testing.T) { 677 _, eth, node, shutdown := tassetWallet(BipID) 678 defer shutdown() 679 680 const tip = 12552 681 const finalized = tip - txConfsNeededToConfirm + 1 682 now := uint64(time.Now().Unix()) 683 finalizedStamp := now - txConfsNeededToConfirm*10 684 rebroadcastable := now - 300 685 mature := now - 600 // able to send actions 686 agedOut := now - uint64(txAgeOut.Seconds()) - 1 687 688 val := dexeth.GweiToWei(1) 689 extendedTx := func(nonce, blockNum, blockStamp, submissionStamp uint64) *extendedWalletTx { 690 pendingTx := eth.extendedTx(node.newTransaction(nonce, val), asset.Send, 1, nil) 691 pendingTx.BlockNumber = blockNum 692 pendingTx.Confirmed = blockNum > 0 && blockNum <= finalized 693 pendingTx.Timestamp = blockStamp 694 pendingTx.SubmissionTime = submissionStamp 695 pendingTx.lastBroadcast = time.Unix(int64(submissionStamp), 0) 696 pendingTx.lastFeeCheck = time.Unix(int64(submissionStamp), 0) 697 return pendingTx 698 } 699 700 newReceipt := func(confs uint64) *types.Receipt { 701 r := &types.Receipt{ 702 EffectiveGasPrice: big.NewInt(1), 703 } 704 if confs > 0 { 705 r.BlockNumber = big.NewInt(int64(tip - confs + 1)) 706 } 707 return r 708 } 709 710 emitChan := make(chan asset.WalletNotification, 128) 711 eth.emit = asset.NewWalletEmitter(emitChan, BipID, eth.log) 712 713 getAction := func(t *testing.T) string { 714 for { 715 select { 716 case ni := <-emitChan: 717 if n, ok := ni.(*asset.ActionRequiredNote); ok { 718 return n.ActionID 719 } 720 default: 721 t.Fatalf("no ActionRequiredNote found") 722 } 723 } 724 } 725 726 for _, tt := range []*struct { 727 name string 728 dbErr error 729 pendingTxs []*extendedWalletTx 730 receipts []*types.Receipt 731 receiptErrs []error 732 txs []bool 733 noncesAfter []uint64 734 actionID string 735 recast bool 736 }{ 737 { 738 name: "first of two is confirmed", 739 pendingTxs: []*extendedWalletTx{ 740 extendedTx(0, finalized, finalizedStamp, finalizedStamp), 741 extendedTx(1, finalized+1, finalizedStamp, finalizedStamp), // won't even be checked. 742 }, 743 noncesAfter: []uint64{1}, 744 }, 745 { 746 name: "second one is confirmed first", 747 pendingTxs: []*extendedWalletTx{ 748 extendedTx(2, 0, 0, rebroadcastable), 749 extendedTx(3, finalized, finalizedStamp, finalizedStamp), 750 }, 751 noncesAfter: []uint64{2, 3}, 752 receipts: []*types.Receipt{nil, nil}, 753 txs: []bool{false, true}, 754 receiptErrs: []error{asset.CoinNotFoundError, nil}, 755 actionID: actionTypeLostNonce, 756 }, 757 { 758 name: "confirm one with receipt", 759 pendingTxs: []*extendedWalletTx{ 760 extendedTx(4, 0, 0, finalizedStamp), 761 }, 762 receipts: []*types.Receipt{newReceipt(txConfsNeededToConfirm)}, 763 }, 764 { 765 name: "old and unindexed", 766 pendingTxs: []*extendedWalletTx{ 767 extendedTx(5, 0, 0, agedOut), 768 }, 769 noncesAfter: []uint64{5}, 770 receipts: []*types.Receipt{nil}, 771 receiptErrs: []error{asset.CoinNotFoundError}, 772 txs: []bool{false}, 773 actionID: actionTypeLostNonce, 774 }, 775 { 776 name: "mature and indexed, low fees", 777 pendingTxs: []*extendedWalletTx{ 778 extendedTx(6, 0, 0, mature), 779 }, 780 noncesAfter: []uint64{6}, 781 receipts: []*types.Receipt{newReceipt(0)}, 782 actionID: actionTypeTooCheap, 783 }, { 784 name: "missing nonces", 785 pendingTxs: []*extendedWalletTx{ 786 extendedTx(8, finalized, finalizedStamp, finalizedStamp), 787 extendedTx(11, finalized+1, finalizedStamp, finalizedStamp), 788 }, 789 noncesAfter: []uint64{11}, 790 actionID: actionTypeMissingNonces, 791 }, 792 } { 793 t.Run(tt.name, func(t *testing.T) { 794 eth.confirmedNonceAt = tt.pendingTxs[0].Nonce 795 eth.pendingNonceAt = new(big.Int).Add(tt.pendingTxs[len(tt.pendingTxs)-1].Nonce, big.NewInt(1)) 796 797 node.lastSignedTx = nil 798 eth.currentTip = &types.Header{Number: new(big.Int).SetUint64(tip)} 799 eth.pendingTxs = tt.pendingTxs 800 for i, r := range tt.receipts { 801 pendingTx := tt.pendingTxs[i] 802 if tt.receiptErrs != nil { 803 node.receiptErrs[pendingTx.txHash] = tt.receiptErrs[i] 804 } 805 if r == nil { 806 continue 807 } 808 node.receipts[pendingTx.txHash] = r 809 if len(tt.txs) < i+1 || tt.txs[i] { 810 node.receiptTxs[pendingTx.txHash], _ = pendingTx.tx() 811 } 812 if pendingTx.Timestamp == 0 && r.BlockNumber != nil && r.BlockNumber.Uint64() != 0 { 813 node.hdrByHash = &types.Header{ 814 Number: r.BlockNumber, 815 Time: now, 816 } 817 } 818 } 819 eth.checkPendingTxs() 820 if len(eth.pendingTxs) != len(tt.noncesAfter) { 821 t.Fatalf("wrong number of pending txs. expected %d got %d", len(tt.noncesAfter), len(eth.pendingTxs)) 822 } 823 for i, pendingTx := range eth.pendingTxs { 824 if pendingTx.Nonce.Uint64() != tt.noncesAfter[i] { 825 t.Fatalf("Expected nonce %d at index %d, but got nonce %s", tt.noncesAfter[i], i, pendingTx.Nonce) 826 } 827 } 828 if tt.actionID != "" { 829 if actionID := getAction(t); actionID != tt.actionID { 830 t.Fatalf("expected action %s, got %s", tt.actionID, actionID) 831 } 832 } 833 if tt.recast != (node.lastSignedTx != nil) { 834 t.Fatalf("wrong recast result recast = %t, lastSignedTx = %t", tt.recast, node.lastSignedTx != nil) 835 } 836 }) 837 } 838 } 839 840 func TestTakeAction(t *testing.T) { 841 _, eth, node, shutdown := tassetWallet(BipID) 842 defer shutdown() 843 844 aGwei := dexeth.GweiToWei(1) 845 846 pendingTx := eth.extendedTx(node.newTransaction(0, aGwei), asset.Send, 1, nil) 847 eth.pendingTxs = []*extendedWalletTx{pendingTx} 848 849 feeCap := new(big.Int).Mul(aGwei, big.NewInt(5)) 850 tipCap := new(big.Int).Mul(aGwei, big.NewInt(2)) 851 replacementTx, _ := types.SignTx(types.NewTx(&types.DynamicFeeTx{ 852 Nonce: 1, 853 GasTipCap: tipCap, 854 GasFeeCap: feeCap, 855 Gas: 50_000, 856 ChainID: node.chainConfig().ChainID, 857 }), signer, node.privKey) 858 node.sendTxTx = replacementTx 859 860 tooCheapAction := []byte(fmt.Sprintf(`{"txID":"%s","bump":true}`, pendingTx.ID)) 861 if err := eth.TakeAction(actionTypeTooCheap, tooCheapAction); err != nil { 862 t.Fatalf("TakeAction error: %v", err) 863 } 864 865 newPendingTx := eth.pendingTxs[0] 866 if pendingTx.txHash == newPendingTx.txHash { 867 t.Fatal("tx wasn't replaced") 868 } 869 tx, _ := newPendingTx.tx() 870 if tx.GasFeeCap().Cmp(feeCap) != 0 { 871 t.Fatalf("wrong fee cap. wanted %s, got %s", feeCap, tx.GasFeeCap()) 872 } 873 if tx.GasTipCap().Cmp(tipCap) != 0 { 874 t.Fatalf("wrong tip cap. wanted %s, got %s", tipCap, tx.GasTipCap()) 875 } 876 if !newPendingTx.savedToDB { 877 t.Fatal("didn't save to DB") 878 } 879 880 pendingTx = eth.extendedTx(node.newTransaction(1, aGwei), asset.Send, 1, nil) 881 eth.pendingTxs = []*extendedWalletTx{pendingTx} 882 pendingTx.SubmissionTime = 0 883 // Neglecting to bump should reset submission time. 884 tooCheapAction = []byte(fmt.Sprintf(`{"txID":"%s","bump":false}`, pendingTx.ID)) 885 if err := eth.TakeAction(actionTypeTooCheap, tooCheapAction); err != nil { 886 t.Fatalf("TakeAction bump=false error: %v", err) 887 } 888 tx, _ = pendingTx.tx() 889 if tx.GasTipCap().Uint64() != 0 { 890 t.Fatal("The fee was bumped. The fee shouldn't have been bumped.") 891 } 892 if pendingTx.actionIgnored.IsZero() { 893 t.Fatalf("The ignore time wasn't reset") 894 } 895 if len(eth.pendingTxs) != 1 { 896 t.Fatalf("Tx was removed") 897 } 898 899 // Nonce-replaced tx 900 eth.pendingTxs = []*extendedWalletTx{pendingTx} 901 lostNonceAction := []byte(fmt.Sprintf(`{"txID":"%s","abandon":true}`, pendingTx.ID)) 902 if err := eth.TakeAction(actionTypeLostNonce, lostNonceAction); err != nil { 903 t.Fatalf("TakeAction replacment=false, abandon=true error: %v", err) 904 } 905 if len(eth.pendingTxs) != 0 { 906 t.Fatalf("Tx wasn't abandoned") 907 } 908 eth.pendingTxs = []*extendedWalletTx{pendingTx} 909 node.getTxRes = replacementTx 910 lostNonceAction = []byte(fmt.Sprintf(`{"txID":"%s","abandon":false,"replacementID":"%s"}`, pendingTx.ID, replacementTx.Hash())) 911 if err := eth.TakeAction(actionTypeLostNonce, lostNonceAction); err != nil { 912 t.Fatalf("TakeAction replacment=true, error: %v", err) 913 } 914 newPendingTx = eth.pendingTxs[0] 915 if newPendingTx.txHash != replacementTx.Hash() { 916 t.Fatalf("replacement tx wasn't accepted") 917 } 918 // wrong nonce is an error though 919 pendingTx = eth.extendedTx(node.newTransaction(5050, aGwei), asset.Send, 1, nil) 920 eth.pendingTxs = []*extendedWalletTx{pendingTx} 921 lostNonceAction = []byte(fmt.Sprintf(`{"txID":"%s","abandon":false,"replacementID":"%s"}`, pendingTx.ID, replacementTx.Hash())) 922 if err := eth.TakeAction(actionTypeLostNonce, lostNonceAction); err == nil { 923 t.Fatalf("no error for wrong nonce") 924 } 925 926 // Missing nonces 927 tx5 := eth.extendedTx(node.newTransaction(5, aGwei), asset.Send, 1, nil) 928 eth.pendingTxs = []*extendedWalletTx{tx5} 929 eth.confirmedNonceAt = big.NewInt(2) 930 eth.pendingNonceAt = big.NewInt(6) 931 nonceRecoveryAction := []byte(`{"recover":true}`) 932 node.sentTxs = 0 933 if err := eth.TakeAction(actionTypeMissingNonces, nonceRecoveryAction); err != nil { 934 t.Fatalf("error for nonce recover: %v", err) 935 } 936 if node.sentTxs != 3 { 937 t.Fatalf("expected 2 new txs. saw %d", node.sentTxs) 938 } 939 940 } 941 942 func TestCheckForNewBlocks(t *testing.T) { 943 header0 := &types.Header{Number: new(big.Int)} 944 header1 := &types.Header{Number: big.NewInt(1)} 945 tests := []struct { 946 name string 947 hashErr, blockErr error 948 bestHeader *types.Header 949 hasTipChange bool 950 }{{ 951 name: "ok", 952 bestHeader: header1, 953 hasTipChange: true, 954 }, { 955 name: "ok same hash", 956 bestHeader: header0, 957 }} 958 959 for _, test := range tests { 960 ctx, cancel := context.WithCancel(context.Background()) 961 node := &testNode{} 962 notes := make(chan asset.WalletNotification, 1) 963 emit := asset.NewWalletEmitter(notes, BipID, tLogger) 964 965 node.bestHdr = test.bestHeader 966 node.bestHdrErr = test.blockErr 967 w := ÐWallet{ 968 assetWallet: &assetWallet{ 969 baseWallet: &baseWallet{ 970 node: node, 971 addr: node.address(), 972 ctx: ctx, 973 log: tLogger, 974 currentTip: header0, 975 confirmedNonceAt: new(big.Int), 976 pendingNonceAt: new(big.Int), 977 txDB: &tTxDB{}, 978 finalizeConfs: txConfsNeededToConfirm, 979 }, 980 log: tLogger.SubLogger("ETH"), 981 emit: emit, 982 assetID: BipID, 983 }, 984 } 985 w.wallets = map[uint32]*assetWallet{BipID: w.assetWallet} 986 w.assetWallet.connected.Store(true) 987 w.checkForNewBlocks(ctx) 988 989 if test.hasTipChange { 990 out: 991 for { 992 select { 993 case ni := <-notes: 994 switch n := ni.(type) { 995 case *asset.TipChangeNote: 996 if n.Tip != test.bestHeader.Number.Uint64() { 997 t.Fatalf("expected tip change but didn't get it") 998 } 999 break out 1000 } 1001 case <-time.After(time.Second * 5): 1002 t.Fatal("timed out waiting for tip change") 1003 } 1004 } 1005 } else { 1006 if w.currentTip.Number.Cmp(test.bestHeader.Number) != 0 { 1007 t.Fatalf("tip was changed. wasn't supposed to be changed") 1008 } 1009 } 1010 cancel() 1011 1012 } 1013 } 1014 1015 func TestSyncStatus(t *testing.T) { 1016 tests := []struct { 1017 name string 1018 syncProg ethereum.SyncProgress 1019 syncProgErr error 1020 subSecs uint64 1021 wantErr, wantSynced bool 1022 wantRatio float32 1023 }{{ 1024 name: "ok synced", 1025 syncProg: ethereum.SyncProgress{ 1026 CurrentBlock: 25, 1027 HighestBlock: 25, 1028 }, 1029 wantRatio: 1, 1030 wantSynced: true, 1031 }, { 1032 name: "ok header too old", 1033 syncProg: ethereum.SyncProgress{ 1034 CurrentBlock: 25, 1035 HighestBlock: 25, 1036 }, 1037 subSecs: dexeth.MaxBlockInterval + 1, 1038 wantRatio: 0.999, 1039 wantSynced: false, 1040 }, { 1041 name: "sync progress error", 1042 syncProg: ethereum.SyncProgress{ 1043 CurrentBlock: 25, 1044 HighestBlock: 0, 1045 }, 1046 syncProgErr: errors.New("test error"), 1047 wantErr: true, 1048 }} 1049 1050 for _, test := range tests { 1051 nowInSecs := uint64(time.Now().Unix()) 1052 ctx, cancel := context.WithCancel(context.Background()) 1053 node := &testNode{ 1054 syncProg: test.syncProg, 1055 syncProgT: nowInSecs - test.subSecs, 1056 syncProgErr: test.syncProgErr, 1057 } 1058 eth := &baseWallet{ 1059 node: node, 1060 addr: node.address(), 1061 ctx: ctx, 1062 log: tLogger, 1063 finalizeConfs: txConfsNeededToConfirm, 1064 } 1065 ss, err := eth.SyncStatus() 1066 cancel() 1067 if test.wantErr { 1068 if err == nil { 1069 t.Fatalf("expected error for test %q", test.name) 1070 } 1071 continue 1072 } 1073 if err != nil { 1074 t.Fatalf("unexpected error for test %q: %v", test.name, err) 1075 } 1076 if ss.Synced != test.wantSynced { 1077 t.Fatalf("want synced %v got %v for test %q", test.wantSynced, ss.Synced, test.name) 1078 } 1079 if ss.BlockProgress() != test.wantRatio { 1080 t.Fatalf("want ratio %v got %v for test %q", test.wantRatio, ss.BlockProgress(), test.name) 1081 } 1082 } 1083 } 1084 1085 func newTestNode(assetID uint32) *tMempoolNode { 1086 privKey, _ := crypto.HexToECDSA("9447129055a25c8496fca9e5ee1b9463e47e6043ff0c288d07169e8284860e34") 1087 addr := common.HexToAddress("2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27") 1088 acct := &accounts.Account{ 1089 Address: addr, 1090 } 1091 1092 tc := &tContractor{ 1093 gasEstimates: ethGases, 1094 swapMap: make(map[[32]byte]*dexeth.SwapState), 1095 valueIn: make(map[common.Hash]uint64), 1096 valueOut: make(map[common.Hash]uint64), 1097 } 1098 1099 var c contractor = tc 1100 1101 ttc := &tTokenContractor{ 1102 tContractor: tc, 1103 allow: new(big.Int), 1104 } 1105 if assetID != BipID { 1106 ttc.tContractor.gasEstimates = &tokenGases 1107 c = ttc 1108 } 1109 1110 return &tMempoolNode{ 1111 testNode: &testNode{ 1112 acct: acct, 1113 addr: acct.Address, 1114 maxFeeRate: dexeth.GweiToWei(100), 1115 baseFee: dexeth.GweiToWei(100), 1116 tip: dexeth.GweiToWei(2), 1117 privKey: privKey, 1118 contractor: c, 1119 tContractor: tc, 1120 tokenContractor: ttc, 1121 txConfirmations: make(map[common.Hash]uint32), 1122 txConfsErr: make(map[common.Hash]error), 1123 receipts: make(map[common.Hash]*types.Receipt), 1124 receiptErrs: make(map[common.Hash]error), 1125 receiptTxs: make(map[common.Hash]*types.Transaction), 1126 }, 1127 } 1128 } 1129 1130 func tassetWallet(assetID uint32) (asset.Wallet, *assetWallet, *tMempoolNode, context.CancelFunc) { 1131 node := newTestNode(assetID) 1132 ctx, cancel := context.WithCancel(context.Background()) 1133 var c contractor = node.tContractor 1134 if assetID != BipID { 1135 c = node.tokenContractor 1136 } 1137 1138 versionedGases := make(map[uint32]*dexeth.Gases) 1139 if assetID == BipID { // just make a copy 1140 for ver, g := range dexeth.VersionedGases { 1141 versionedGases[ver] = g 1142 } 1143 } else { 1144 netToken := dexeth.Tokens[assetID].NetTokens[dex.Simnet] 1145 for ver, c := range netToken.SwapContracts { 1146 versionedGases[ver] = &c.Gas 1147 } 1148 } 1149 1150 emitChan := make(chan asset.WalletNotification, 128) 1151 go func() { 1152 for { 1153 select { 1154 case <-ctx.Done(): 1155 return 1156 case <-emitChan: 1157 } 1158 } 1159 }() 1160 1161 aw := &assetWallet{ 1162 baseWallet: &baseWallet{ 1163 baseChainID: BipID, 1164 chainID: dexeth.ChainIDs[dex.Simnet], 1165 tokens: dexeth.Tokens, 1166 addr: node.addr, 1167 net: dex.Simnet, 1168 node: node, 1169 ctx: ctx, 1170 log: tLogger, 1171 gasFeeLimitV: defaultGasFeeLimit, 1172 pendingNonceAt: new(big.Int), 1173 confirmedNonceAt: new(big.Int), 1174 pendingTxs: make([]*extendedWalletTx, 0), 1175 txDB: &tTxDB{}, 1176 currentTip: &types.Header{Number: new(big.Int)}, 1177 finalizeConfs: txConfsNeededToConfirm, 1178 }, 1179 versionedGases: versionedGases, 1180 maxSwapGas: versionedGases[0].Swap, 1181 maxRedeemGas: versionedGases[0].Redeem, 1182 log: tLogger.SubLogger(strings.ToUpper(dex.BipIDSymbol(assetID))), 1183 assetID: assetID, 1184 contractors: map[uint32]contractor{0: c}, 1185 findRedemptionReqs: make(map[[32]byte]*findRedemptionRequest), 1186 evmify: dexeth.GweiToWei, 1187 atomize: dexeth.WeiToGwei, 1188 pendingTxCheckBal: new(big.Int), 1189 pendingApprovals: make(map[uint32]*pendingApproval), 1190 approvalCache: make(map[uint32]bool), 1191 // move up after review 1192 wi: WalletInfo, 1193 emit: asset.NewWalletEmitter(emitChan, BipID, tLogger), 1194 } 1195 aw.wallets = map[uint32]*assetWallet{ 1196 BipID: aw, 1197 } 1198 1199 var w asset.Wallet 1200 if assetID == BipID { 1201 w = ÐWallet{assetWallet: aw} 1202 aw.wallets = map[uint32]*assetWallet{ 1203 BipID: aw, 1204 } 1205 } else { 1206 node.tokenParent = &assetWallet{ 1207 baseWallet: aw.baseWallet, 1208 log: tLogger.SubLogger("ETH"), 1209 contractors: map[uint32]contractor{0: node.tContractor}, 1210 assetID: BipID, 1211 atomize: dexeth.WeiToGwei, 1212 pendingApprovals: make(map[uint32]*pendingApproval), 1213 approvalCache: make(map[uint32]bool), 1214 emit: asset.NewWalletEmitter(emitChan, BipID, tLogger), 1215 } 1216 w = &TokenWallet{ 1217 assetWallet: aw, 1218 cfg: &tokenWalletConfig{}, 1219 parent: node.tokenParent, 1220 token: dexeth.Tokens[usdcTokenID], 1221 netToken: dexeth.Tokens[usdcTokenID].NetTokens[dex.Simnet], 1222 } 1223 aw.wallets = map[uint32]*assetWallet{ 1224 usdcTokenID: aw, 1225 BipID: node.tokenParent, 1226 } 1227 } 1228 1229 return w, aw, node, cancel 1230 } 1231 1232 func TestBalanceWithMempool(t *testing.T) { 1233 tinyBal := newBalance(0, 0, 0) 1234 tinyBal.Current = big.NewInt(dexeth.GweiFactor - 1) 1235 1236 tests := []struct { 1237 name string 1238 bal *Balance 1239 balErr error 1240 wantErr bool 1241 wantBal uint64 1242 wantImmature uint64 1243 wantLocked uint64 1244 token bool 1245 }{{ 1246 name: "ok zero", 1247 bal: newBalance(0, 0, 0), 1248 wantBal: 0, 1249 wantImmature: 0, 1250 }, { 1251 name: "ok rounded down", 1252 bal: tinyBal, 1253 wantBal: 0, 1254 }, { 1255 name: "ok one", 1256 bal: newBalance(1, 0, 0), 1257 wantBal: 1, 1258 }, { 1259 name: "ok one token", 1260 bal: newBalance(1, 0, 0), 1261 wantBal: 1, 1262 token: true, 1263 }, { 1264 name: "ok pending out", 1265 bal: newBalance(4e8, 0, 1.4e8), 1266 wantBal: 2.6e8, 1267 wantLocked: 0, 1268 }, { 1269 name: "ok pending out token", 1270 bal: newBalance(4e8, 0, 1.4e8), 1271 wantBal: 2.6e8, 1272 wantLocked: 0, 1273 token: true, 1274 }, { 1275 name: "ok pending in", 1276 bal: newBalance(1e8, 3e8, 0), 1277 wantBal: 1e8, 1278 wantImmature: 3e8, 1279 }, { 1280 name: "ok pending in token", 1281 bal: newBalance(1e8, 3e8, 0), 1282 wantBal: 1e8, 1283 wantImmature: 3e8, 1284 token: true, 1285 }, { 1286 name: "ok pending out and in", 1287 bal: newBalance(4e8, 2e8, 1e8), 1288 wantBal: 3e8, 1289 wantLocked: 0, 1290 wantImmature: 2e8, 1291 }, { 1292 name: "ok pending out and in token", 1293 bal: newBalance(4e8, 2e8, 1e8), 1294 wantBal: 3e8, 1295 wantLocked: 0, 1296 wantImmature: 2e8, 1297 token: true, 1298 }, { 1299 // name: "ok max int", 1300 // bal: maxWei, 1301 // pendingTxs: []*types.Transaction{}, 1302 // wantBal: maxInt, 1303 // wantImmature: 0, 1304 // }, { 1305 // name: "over max int", 1306 // bal: overMaxWei, 1307 // pendingTxs: []*types.Transaction{}, 1308 // wantErr: true, 1309 // }, { 1310 name: "node balance error", 1311 bal: newBalance(0, 0, 0), 1312 balErr: errors.New("test error"), 1313 wantErr: true, 1314 }} 1315 1316 for _, test := range tests { 1317 var assetID uint32 = BipID 1318 if test.token { 1319 assetID = usdcTokenID 1320 } 1321 1322 _, eth, node, shutdown := tassetWallet(assetID) 1323 defer shutdown() 1324 1325 if test.token { 1326 node.tokenContractor.bal = test.bal.Current 1327 node.tokenContractor.balErr = test.balErr 1328 } else { 1329 node.bal = test.bal.Current 1330 node.balErr = test.balErr 1331 } 1332 1333 var nonce uint64 1334 newTx := func(value *big.Int) *types.Transaction { 1335 nonce++ 1336 return node.newTransaction(nonce, value) 1337 } 1338 1339 if test.bal.PendingIn.Cmp(new(big.Int)) > 0 { 1340 tx := newTx(new(big.Int)) 1341 node.pendingTxs = append(node.pendingTxs, tx) 1342 node.tContractor.valueIn[tx.Hash()] = dexeth.WeiToGwei(test.bal.PendingIn) 1343 } 1344 if test.bal.PendingOut.Cmp(new(big.Int)) > 0 { 1345 if test.token { 1346 tx := newTx(nil) 1347 node.pendingTxs = append(node.pendingTxs, tx) 1348 node.tContractor.valueOut[tx.Hash()] = dexeth.WeiToGwei(test.bal.PendingOut) 1349 } else { 1350 node.pendingTxs = append(node.pendingTxs, newTx(test.bal.PendingOut)) 1351 } 1352 } 1353 1354 bal, err := eth.Balance() 1355 if test.wantErr { 1356 if err == nil { 1357 t.Fatalf("expected error for test %q", test.name) 1358 } 1359 continue 1360 } 1361 if err != nil { 1362 t.Fatalf("unexpected error for test %q: %v", test.name, err) 1363 } 1364 if bal.Available != test.wantBal { 1365 t.Fatalf("want available balance %v got %v for test %q", test.wantBal, bal.Available, test.name) 1366 } 1367 if bal.Immature != test.wantImmature { 1368 t.Fatalf("want immature balance %v got %v for test %q", test.wantImmature, bal.Immature, test.name) 1369 } 1370 if bal.Locked != test.wantLocked { 1371 t.Fatalf("want locked balance %v got %v for test %q", test.wantLocked, bal.Locked, test.name) 1372 } 1373 } 1374 } 1375 1376 func TestBalanceNoMempool(t *testing.T) { 1377 var nonceCounter int64 1378 1379 newExtendedWalletTx := func(assetID uint32, amt, maxFees, blockNum uint64, txType asset.TransactionType) *extendedWalletTx { 1380 var tokenID *uint32 1381 if assetID != BipID { 1382 tokenID = &assetID 1383 } 1384 txHash := common.BytesToHash(encode.RandomBytes(32)) 1385 1386 et := &extendedWalletTx{ 1387 WalletTransaction: &asset.WalletTransaction{ 1388 ID: txHash.String(), 1389 Type: txType, 1390 Amount: amt, 1391 BlockNumber: blockNum, 1392 TokenID: tokenID, 1393 Fees: maxFees, 1394 }, 1395 Nonce: big.NewInt(nonceCounter), 1396 txHash: txHash, 1397 } 1398 nonceCounter++ 1399 1400 return et 1401 } 1402 1403 tests := []struct { 1404 name string 1405 assetID uint32 1406 unconfirmedTxs []*extendedWalletTx 1407 expPendingIn uint64 1408 expPendingOut uint64 1409 }{ 1410 { 1411 name: "single eth tx", 1412 assetID: BipID, 1413 unconfirmedTxs: []*extendedWalletTx{ 1414 newExtendedWalletTx(BipID, 1, 2, 0, asset.Send), 1415 }, 1416 expPendingOut: 3, 1417 }, 1418 { 1419 name: "eth with token fees", 1420 assetID: BipID, 1421 unconfirmedTxs: []*extendedWalletTx{ 1422 newExtendedWalletTx(usdcTokenID, 4, 5, 0, asset.Send), 1423 }, 1424 expPendingOut: 5, 1425 }, 1426 { 1427 name: "token with 1 tx and other ignored assets", 1428 assetID: usdcTokenID, 1429 unconfirmedTxs: []*extendedWalletTx{ 1430 newExtendedWalletTx(usdcTokenID, 4, 5, 0, asset.Send), 1431 newExtendedWalletTx(usdcTokenID+1, 8, 9, 0, asset.Send), 1432 }, 1433 expPendingOut: 4, 1434 }, 1435 { 1436 name: "token with 1 tx incoming", 1437 assetID: usdcTokenID, 1438 unconfirmedTxs: []*extendedWalletTx{ 1439 newExtendedWalletTx(usdcTokenID, 15, 5, 0, asset.Redeem), 1440 }, 1441 expPendingIn: 15, 1442 }, 1443 { 1444 name: "eth mixed txs", 1445 assetID: BipID, 1446 unconfirmedTxs: []*extendedWalletTx{ 1447 newExtendedWalletTx(BipID, 1, 2, 0, asset.Swap), // 3 eth out 1448 newExtendedWalletTx(usdcTokenID, 3, 4, 1, asset.Send), // confirmed 1449 newExtendedWalletTx(usdcTokenID, 5, 6, 0, asset.Swap), // 6 eth out 1450 newExtendedWalletTx(BipID, 7, 1, 0, asset.Refund), // 1 eth out, 7 eth in 1451 }, 1452 expPendingOut: 10, 1453 expPendingIn: 7, 1454 }, 1455 { 1456 name: "already confirmed, but still waiting for txConfsNeededToConfirm", 1457 assetID: usdcTokenID, 1458 unconfirmedTxs: []*extendedWalletTx{ 1459 newExtendedWalletTx(usdcTokenID, 15, 5, 1, asset.Redeem), 1460 }, 1461 }, 1462 } 1463 1464 for _, tt := range tests { 1465 t.Run(tt.name, func(t *testing.T) { 1466 _, eth, node, shutdown := tassetWallet(tt.assetID) 1467 defer shutdown() 1468 eth.node = node.testNode // no mempool 1469 node.bal = unlimitedAllowance 1470 node.tokenContractor.bal = unlimitedAllowance 1471 eth.connected.Store(true) 1472 1473 eth.pendingTxs = tt.unconfirmedTxs 1474 1475 bal, err := eth.balanceWithTxPool() 1476 if err != nil { 1477 t.Fatalf("balanceWithTxPool error: %v", err) 1478 } 1479 1480 if in := dexeth.WeiToGwei(bal.PendingIn); in != tt.expPendingIn { 1481 t.Fatalf("wrong PendingIn. wanted %d, got %d", tt.expPendingIn, in) 1482 } 1483 1484 if out := dexeth.WeiToGwei(bal.PendingOut); out != tt.expPendingOut { 1485 t.Fatalf("wrong PendingOut. wanted %d, got %d", tt.expPendingOut, out) 1486 } 1487 }) 1488 } 1489 } 1490 1491 func TestFeeRate(t *testing.T) { 1492 ctx, cancel := context.WithCancel(context.Background()) 1493 defer cancel() 1494 node := &testNode{} 1495 eth := &baseWallet{ 1496 node: node, 1497 ctx: ctx, 1498 log: tLogger, 1499 finalizeConfs: txConfsNeededToConfirm, 1500 currentTip: &types.Header{Number: big.NewInt(100)}, 1501 } 1502 1503 maxInt := ^int64(0) 1504 tests := []struct { 1505 name string 1506 baseFee *big.Int 1507 tip *big.Int 1508 netFeeStateErr error 1509 wantFeeRate uint64 1510 }{ 1511 { 1512 name: "ok", 1513 baseFee: big.NewInt(100e9), 1514 tip: big.NewInt(2e9), 1515 wantFeeRate: 202, 1516 }, 1517 { 1518 name: "net fee state error", 1519 netFeeStateErr: errors.New("test error"), 1520 }, 1521 { 1522 name: "overflow error", 1523 baseFee: big.NewInt(maxInt), 1524 tip: big.NewInt(1), 1525 }, 1526 } 1527 1528 for _, test := range tests { 1529 eth.currentFees.blockNum = 0 1530 node.baseFee = test.baseFee 1531 node.tip = test.tip 1532 node.netFeeStateErr = test.netFeeStateErr 1533 feeRate := eth.FeeRate() 1534 if feeRate != test.wantFeeRate { 1535 t.Fatalf("%v: expected fee rate %d but got %d", test.name, test.wantFeeRate, feeRate) 1536 } 1537 } 1538 } 1539 1540 func TestRefund(t *testing.T) { 1541 t.Run("eth", func(t *testing.T) { testRefund(t, BipID) }) 1542 t.Run("token", func(t *testing.T) { testRefund(t, usdcTokenID) }) 1543 } 1544 1545 func testRefund(t *testing.T, assetID uint32) { 1546 _, eth, node, shutdown := tassetWallet(assetID) 1547 defer shutdown() 1548 1549 const feeSuggestion = 100 1550 const gweiBal = 1e9 1551 const ogRefundReserves = 1e8 1552 1553 v1Contractor := &tContractor{ 1554 swapMap: make(map[[32]byte]*dexeth.SwapState, 1), 1555 gasEstimates: ethGases, 1556 redeemTx: types.NewTx(&types.DynamicFeeTx{}), 1557 } 1558 var v1c contractor = v1Contractor 1559 1560 gasesV1 := &dexeth.Gases{Refund: 1e5} 1561 if assetID == BipID { 1562 eth.versionedGases[1] = gasesV1 1563 } else { 1564 eth.versionedGases[1] = &dexeth.Tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas 1565 v1c = &tTokenContractor{tContractor: v1Contractor} 1566 } 1567 1568 eth.contractors[1] = v1c 1569 1570 var secretHash [32]byte 1571 copy(secretHash[:], encode.RandomBytes(32)) 1572 v0Contract := dexeth.EncodeContractData(0, secretHash) 1573 ss := &dexeth.SwapState{Value: dexeth.GweiToWei(1)} 1574 v0Contractor := node.tContractor 1575 1576 v0Contractor.swapMap[secretHash] = ss 1577 v1Contractor.swapMap[secretHash] = ss 1578 1579 tests := []struct { 1580 name string 1581 badContract bool 1582 isRefundable bool 1583 isRefundableErr error 1584 refundErr error 1585 wantLocked uint64 1586 wantErr bool 1587 wantZeroHash bool 1588 swapStep dexeth.SwapStep 1589 swapErr error 1590 useV1Gases bool 1591 }{ 1592 { 1593 name: "ok", 1594 swapStep: dexeth.SSInitiated, 1595 isRefundable: true, 1596 wantLocked: ogRefundReserves - feeSuggestion*dexeth.RefundGas(0), 1597 }, 1598 { 1599 name: "ok v1", 1600 swapStep: dexeth.SSInitiated, 1601 isRefundable: true, 1602 wantLocked: ogRefundReserves - feeSuggestion*dexeth.RefundGas(1), 1603 useV1Gases: true, 1604 }, 1605 { 1606 name: "ok refunded", 1607 swapStep: dexeth.SSRefunded, 1608 isRefundable: true, 1609 wantLocked: ogRefundReserves - feeSuggestion*dexeth.RefundGas(0), 1610 wantZeroHash: true, 1611 }, 1612 { 1613 name: "swap error", 1614 swapStep: dexeth.SSInitiated, 1615 swapErr: errors.New("test error"), 1616 wantErr: true, 1617 }, 1618 { 1619 name: "is refundable error", 1620 isRefundable: true, 1621 isRefundableErr: errors.New("test error"), 1622 wantErr: true, 1623 }, 1624 { 1625 name: "is refundable false", 1626 isRefundable: false, 1627 wantErr: true, 1628 }, 1629 { 1630 name: "refund error", 1631 isRefundable: true, 1632 refundErr: errors.New("test error"), 1633 wantErr: true, 1634 }, 1635 { 1636 name: "cannot decode contract", 1637 badContract: true, 1638 isRefundable: true, 1639 wantErr: true, 1640 }, 1641 } 1642 1643 for _, test := range tests { 1644 contract := v0Contract 1645 c := v0Contractor 1646 if test.useV1Gases { 1647 contract = dexeth.EncodeContractData(1, secretHash) 1648 c = v1Contractor 1649 } else if test.badContract { 1650 contract = []byte{} 1651 } 1652 1653 c.refundable = test.isRefundable 1654 c.refundableErr = test.isRefundableErr 1655 c.refundErr = test.refundErr 1656 node.bal = dexeth.GweiToWei(gweiBal) 1657 c.swapErr = test.swapErr 1658 ss.State = test.swapStep 1659 eth.lockedFunds.refundReserves = ogRefundReserves 1660 1661 var txHash common.Hash 1662 if test.isRefundable { 1663 tx := types.NewTx(&types.DynamicFeeTx{}) 1664 txHash = tx.Hash() 1665 c.refundTx = tx 1666 } 1667 1668 refundID, err := eth.Refund(nil, contract, feeSuggestion) 1669 1670 if test.wantErr { 1671 if err == nil { 1672 t.Fatalf(`%v: expected error but did not get: %v`, test.name, err) 1673 } 1674 continue 1675 } 1676 if err != nil { 1677 t.Fatalf(`%v: unexpected error: %v`, test.name, err) 1678 } 1679 1680 if test.wantZeroHash { 1681 // No on chain refund expected if status was already refunded. 1682 zeroHash := common.Hash{} 1683 if !bytes.Equal(refundID, zeroHash[:]) { 1684 t.Fatalf(`%v: expected refund tx hash: %x = returned id: %s`, test.name, zeroHash, refundID) 1685 } 1686 } else { 1687 if !bytes.Equal(refundID, txHash[:]) { 1688 t.Fatalf(`%v: expected refund tx hash: %x = returned id: %s`, test.name, txHash, refundID) 1689 } 1690 1691 if secretHash != c.lastRefund.secretHash { 1692 t.Fatalf(`%v: secret hash in contract %x != used to call refund %x`, 1693 test.name, secretHash, c.lastRefund.secretHash) 1694 } 1695 1696 if dexeth.GweiToWei(feeSuggestion).Cmp(c.lastRefund.maxFeeRate) != 0 { 1697 t.Fatalf(`%v: fee suggestion %v != used to call refund %v`, 1698 test.name, dexeth.GweiToWei(feeSuggestion), c.lastRefund.maxFeeRate) 1699 } 1700 } 1701 } 1702 } 1703 1704 // badCoin fulfills the asset.Coin interface, but the ID does not match the ID expected in the 1705 // ETH wallet code. 1706 type badCoin uint64 1707 1708 func (*badCoin) ID() dex.Bytes { 1709 return []byte{123} 1710 } 1711 func (*badCoin) String() string { 1712 return "abc" 1713 } 1714 func (*badCoin) TxID() string { 1715 return "abc" 1716 } 1717 func (b *badCoin) Value() uint64 { 1718 return uint64(*b) 1719 } 1720 1721 func TestFundOrderReturnCoinsFundingCoins(t *testing.T) { 1722 t.Run("eth", func(t *testing.T) { testFundOrderReturnCoinsFundingCoins(t, BipID) }) 1723 t.Run("token", func(t *testing.T) { testFundOrderReturnCoinsFundingCoins(t, usdcTokenID) }) 1724 } 1725 1726 func testFundOrderReturnCoinsFundingCoins(t *testing.T, assetID uint32) { 1727 w, eth, node, shutdown := tassetWallet(assetID) 1728 defer shutdown() 1729 walletBalanceGwei := uint64(dexeth.GweiFactor) 1730 fromAsset := tETH 1731 if assetID == BipID { 1732 node.bal = dexeth.GweiToWei(walletBalanceGwei) 1733 } else { 1734 fromAsset = tToken 1735 node.tokenContractor.bal = dexeth.GweiToWei(walletBalanceGwei) 1736 node.tokenContractor.allow = unlimitedAllowance 1737 node.tokenParent.node.(*tMempoolNode).bal = dexeth.GweiToWei(walletBalanceGwei) 1738 } 1739 1740 checkBalance := func(wallet *assetWallet, expectedAvailable, expectedLocked uint64, testName string) { 1741 t.Helper() 1742 balance, err := wallet.Balance() 1743 if err != nil { 1744 t.Fatalf("%v: unexpected error %v", testName, err) 1745 } 1746 if balance.Available != expectedAvailable { 1747 t.Fatalf("%v: expected %v funds to be available but got %v", testName, expectedAvailable, balance.Available) 1748 } 1749 if balance.Locked != expectedLocked { 1750 t.Fatalf("%v: expected %v funds to be locked but got %v", testName, expectedLocked, balance.Locked) 1751 } 1752 } 1753 1754 type fundOrderTest struct { 1755 testName string 1756 wantErr bool 1757 coinValue uint64 1758 coinAddress string 1759 } 1760 checkFundOrderResult := func(coins asset.Coins, redeemScripts []dex.Bytes, err error, test fundOrderTest) { 1761 t.Helper() 1762 if test.wantErr && err == nil { 1763 t.Fatalf("%v: expected error but didn't get", test.testName) 1764 } 1765 if test.wantErr { 1766 return 1767 } 1768 if err != nil { 1769 t.Fatalf("%s: unexpected error: %v", test.testName, err) 1770 } 1771 if len(coins) != 1 { 1772 t.Fatalf("%s: expected 1 coins but got %v", test.testName, len(coins)) 1773 } 1774 if len(redeemScripts) != 1 { 1775 t.Fatalf("%s: expected 1 redeem script but got %v", test.testName, len(redeemScripts)) 1776 } 1777 rc, is := coins[0].(asset.RecoveryCoin) 1778 if !is { 1779 t.Fatalf("%s: funding coin is not a RecoveryCoin", test.testName) 1780 } 1781 1782 if assetID == BipID { 1783 _, err = decodeFundingCoin(rc.RecoveryID()) 1784 } else { 1785 _, err = decodeTokenFundingCoin(rc.RecoveryID()) 1786 } 1787 if err != nil { 1788 t.Fatalf("%s: unexpected error: %v", test.testName, err) 1789 } 1790 if coins[0].Value() != test.coinValue { 1791 t.Fatalf("%s: wrong value. expected %v but got %v", test.testName, test.coinValue, coins[0].Value()) 1792 } 1793 } 1794 1795 order := asset.Order{ 1796 Version: fromAsset.Version, 1797 Value: walletBalanceGwei / 2, 1798 MaxSwapCount: 2, 1799 MaxFeeRate: fromAsset.MaxFeeRate, 1800 RedeemVersion: tBTC.Version, // not important if not a token 1801 RedeemAssetID: tBTC.ID, 1802 } 1803 1804 // Test fund order with less than available funds 1805 coins1, redeemScripts1, _, err := w.FundOrder(&order) 1806 expectedOrderFees := eth.gases(fromAsset.Version).Swap * order.MaxFeeRate * order.MaxSwapCount 1807 expectedFees := expectedOrderFees 1808 expectedCoinValue := order.Value 1809 if assetID == BipID { 1810 expectedCoinValue += expectedOrderFees 1811 } 1812 1813 checkFundOrderResult(coins1, redeemScripts1, err, fundOrderTest{ 1814 testName: "more than enough", 1815 coinValue: expectedCoinValue, 1816 coinAddress: node.addr.String(), 1817 }) 1818 checkBalance(eth, walletBalanceGwei-expectedCoinValue, expectedCoinValue, "more than enough") 1819 1820 // Test fund order with 1 more than available funds 1821 order.Value = walletBalanceGwei - expectedCoinValue + 1 1822 if assetID == BipID { 1823 order.Value -= expectedOrderFees 1824 } 1825 coins, redeemScripts, _, err := w.FundOrder(&order) 1826 checkFundOrderResult(coins, redeemScripts, err, fundOrderTest{ 1827 testName: "not enough", 1828 wantErr: true, 1829 }) 1830 checkBalance(eth, walletBalanceGwei-expectedCoinValue, expectedCoinValue, "not enough") 1831 1832 // Test fund order with funds equal to available 1833 order.Value = order.Value - 1 1834 expVal := order.Value 1835 if assetID == BipID { 1836 expVal += expectedFees 1837 } 1838 coins2, redeemScripts2, _, err := w.FundOrder(&order) 1839 checkFundOrderResult(coins2, redeemScripts2, err, fundOrderTest{ 1840 testName: "just enough", 1841 coinValue: expVal, 1842 coinAddress: node.addr.String(), 1843 }) 1844 checkBalance(eth, 0, walletBalanceGwei, "just enough") 1845 1846 // Test returning funds > locked returns locked funds to 0 1847 err = w.ReturnCoins([]asset.Coin{coins1[0], coins1[0]}) 1848 if err != nil { 1849 t.Fatalf("unexpected error: %v", err) 1850 } 1851 checkBalance(eth, walletBalanceGwei, 0, "after return too much") 1852 1853 // Fund order with funds equal to available 1854 order.Value = walletBalanceGwei 1855 if assetID == BipID { 1856 order.Value -= expectedOrderFees 1857 } 1858 _, _, _, err = w.FundOrder(&order) 1859 if err != nil { 1860 t.Fatalf("unexpected error: %v", err) 1861 } 1862 checkBalance(eth, 0, walletBalanceGwei, "just enough 2") 1863 1864 // Test returning coin with invalid ID 1865 var badCoin badCoin 1866 err = w.ReturnCoins([]asset.Coin{&badCoin}) 1867 if err == nil { 1868 t.Fatalf("expected error but did not get") 1869 } 1870 1871 // Test returning correct coins returns all funds 1872 err = w.ReturnCoins([]asset.Coin{coins1[0], coins2[0]}) 1873 if err != nil { 1874 t.Fatalf("unexpected error: %v", err) 1875 } 1876 checkBalance(eth, walletBalanceGwei, 0, "returned correct amount") 1877 1878 node.setBalanceError(eth, errors.New("test error")) 1879 _, _, _, err = w.FundOrder(&order) 1880 if err == nil { 1881 t.Fatalf("balance error should cause error but did not") 1882 } 1883 node.setBalanceError(eth, nil) 1884 1885 // Test that funding without allowance causes error 1886 if assetID != BipID { 1887 eth.approvalCache = make(map[uint32]bool) 1888 node.tokenContractor.allow = big.NewInt(0) 1889 _, _, _, err = w.FundOrder(&order) 1890 if err == nil { 1891 t.Fatalf("no allowance should cause error but did not") 1892 } 1893 node.tokenContractor.allow = unlimitedAllowance 1894 } 1895 1896 // Test eth wallet gas fee limit > server MaxFeeRate causes error 1897 tmpGasFeeLimit := eth.gasFeeLimit() 1898 eth.gasFeeLimitV = order.MaxFeeRate - 1 1899 _, _, _, err = w.FundOrder(&order) 1900 if err == nil { 1901 t.Fatalf("eth wallet gas fee limit > server MaxFeeRate should cause error") 1902 } 1903 eth.gasFeeLimitV = tmpGasFeeLimit 1904 1905 w2, eth2, _, shutdown2 := tassetWallet(assetID) 1906 defer shutdown2() 1907 eth2.node = node 1908 eth2.contractors[0] = node.tokenContractor 1909 node.tokenContractor.bal = dexeth.GweiToWei(walletBalanceGwei) 1910 1911 // Test reloading coins from first order 1912 coinVal := coins1[0].Value() 1913 coins, err = w2.FundingCoins([]dex.Bytes{parseRecoveryID(coins1[0])}) 1914 if err != nil { 1915 t.Fatalf("unexpected error: %v", err) 1916 } 1917 if len(coins) != 1 { 1918 t.Fatalf("expected 1 coins but got %v", len(coins)) 1919 } 1920 if coins[0].Value() != coinVal { 1921 t.Fatalf("funding coin value %v != expected %v", coins[0].Value(), coins[1].Value()) 1922 } 1923 checkBalance(eth2, walletBalanceGwei-coinVal, coinVal, "funding1") 1924 1925 // Test reloading more coins than are available in balance 1926 rid := parseRecoveryID(coins1[0]) 1927 if assetID != BipID { 1928 rid = createTokenFundingCoin(node.addr, coinVal+1, 1).RecoveryID() 1929 } 1930 _, err = w2.FundingCoins([]dex.Bytes{rid}) 1931 if err == nil { 1932 t.Fatalf("expected error but didn't get one") 1933 } 1934 checkBalance(eth2, walletBalanceGwei-coinVal, coinVal, "after funding error 1") 1935 1936 // Test funding coins with bad coin ID 1937 1938 _, err = w2.FundingCoins([]dex.Bytes{append(parseRecoveryID(coins1[0]), 0x0a)}) 1939 if err == nil { 1940 t.Fatalf("expected error but did not get") 1941 } 1942 checkBalance(eth2, walletBalanceGwei-coinVal, coinVal, "after funding error 3") 1943 1944 // Test funding coins with coin from different address 1945 var differentAddress [20]byte 1946 decodedHex, _ := hex.DecodeString("8d83B207674bfd53B418a6E47DA148F5bFeCc652") 1947 copy(differentAddress[:], decodedHex) 1948 var nonce [8]byte 1949 copy(nonce[:], encode.RandomBytes(8)) 1950 1951 differentKindaCoin := (&coin{ 1952 id: randomHash(), // e.g. tx hash 1953 }) 1954 _, err = w2.FundingCoins([]dex.Bytes{differentKindaCoin.ID()}) 1955 if err == nil { 1956 t.Fatalf("expected error for unknown coin id format, but did not get") 1957 } 1958 1959 differentAddressCoin := createFundingCoin(differentAddress, 100000) 1960 _, err = w2.FundingCoins([]dex.Bytes{differentAddressCoin.ID()}) 1961 if err == nil { 1962 t.Fatalf("expected error for wrong address, but did not get") 1963 } 1964 checkBalance(eth2, walletBalanceGwei-coinVal, coinVal, "after funding error 4") 1965 1966 // Test funding coins with balance error 1967 node.balErr = errors.New("test error") 1968 _, err = w2.FundingCoins([]dex.Bytes{badCoin.ID()}) 1969 if err == nil { 1970 t.Fatalf("expected error but did not get") 1971 } 1972 node.balErr = nil 1973 checkBalance(eth2, walletBalanceGwei-coinVal, coinVal, "after funding error 5") 1974 1975 // Reloading coins from second order 1976 coins, err = w2.FundingCoins([]dex.Bytes{parseRecoveryID(coins2[0])}) 1977 if err != nil { 1978 t.Fatalf("unexpected error: %v", err) 1979 } 1980 if len(coins) != 1 { 1981 t.Fatalf("expected 1 coins but got %v", len(coins)) 1982 } 1983 if coins[0].Value() != coins2[0].Value() { 1984 t.Fatalf("funding coin value %v != expected %v", coins[0].Value(), coins[1].Value()) 1985 } 1986 checkBalance(eth2, 0, walletBalanceGwei, "funding2") 1987 1988 // return coin with wrong kind and incorrect address 1989 err = w2.ReturnCoins([]asset.Coin{differentKindaCoin}) 1990 if err == nil { 1991 t.Fatalf("expected error for unknown coin ID format, but did not get") 1992 } 1993 1994 err = w2.ReturnCoins([]asset.Coin{differentAddressCoin}) 1995 if err == nil { 1996 t.Fatalf("expected error for wrong address, but did not get") 1997 } 1998 1999 // return all coins 2000 err = w2.ReturnCoins([]asset.Coin{coins1[0], coins2[0]}) 2001 if err != nil { 2002 t.Fatalf("unexpected error") 2003 } 2004 checkBalance(eth2, walletBalanceGwei, 0, "return coins after funding") 2005 2006 // Test funding coins with two coins at the same time 2007 _, err = w2.FundingCoins([]dex.Bytes{parseRecoveryID(coins1[0]), parseRecoveryID(coins2[0])}) 2008 if err != nil { 2009 t.Fatalf("unexpected error: %v", err) 2010 } 2011 checkBalance(eth2, 0, walletBalanceGwei, "funding3") 2012 } 2013 2014 func TestFundMultiOrder(t *testing.T) { 2015 t.Run("eth", func(t *testing.T) { testFundMultiOrder(t, BipID) }) 2016 t.Run("token", func(t *testing.T) { testFundMultiOrder(t, usdcTokenID) }) 2017 } 2018 2019 func testFundMultiOrder(t *testing.T, assetID uint32) { 2020 w, eth, node, shutdown := tassetWallet(assetID) 2021 2022 defer shutdown() 2023 2024 fromAsset := tETH 2025 swapGas := dexeth.VersionedGases[fromAsset.Version].Swap 2026 if assetID != BipID { 2027 fromAsset = tToken 2028 node.tokenContractor.allow = unlimitedAllowance 2029 swapGas = dexeth.Tokens[usdcTokenID].NetTokens[dex.Simnet]. 2030 SwapContracts[fromAsset.Version].Gas.Swap 2031 } 2032 2033 type test struct { 2034 name string 2035 multiOrder *asset.MultiOrder 2036 maxLock uint64 2037 bal uint64 2038 tokenBal uint64 2039 parentBal uint64 2040 2041 ethOnly bool 2042 tokenOnly bool 2043 2044 expectErr bool 2045 } 2046 2047 tests := []test{ 2048 { 2049 name: "ok", 2050 bal: uint64(dexeth.GweiFactor) + swapGas*4*fromAsset.MaxFeeRate, 2051 tokenBal: uint64(dexeth.GweiFactor), 2052 parentBal: uint64(dexeth.GweiFactor), 2053 multiOrder: &asset.MultiOrder{ 2054 Version: fromAsset.Version, 2055 MaxFeeRate: fromAsset.MaxFeeRate, 2056 Values: []*asset.MultiOrderValue{ 2057 { 2058 Value: uint64(dexeth.GweiFactor) / 2, 2059 MaxSwapCount: 2, 2060 }, 2061 { 2062 Value: uint64(dexeth.GweiFactor) / 2, 2063 MaxSwapCount: 2, 2064 }, 2065 }, 2066 }, 2067 }, 2068 { 2069 name: "maxLock just enough, eth", 2070 bal: uint64(dexeth.GweiFactor) + swapGas*4*fromAsset.MaxFeeRate, 2071 tokenBal: uint64(dexeth.GweiFactor), 2072 parentBal: uint64(dexeth.GweiFactor), 2073 multiOrder: &asset.MultiOrder{ 2074 Version: fromAsset.Version, 2075 MaxFeeRate: fromAsset.MaxFeeRate, 2076 Values: []*asset.MultiOrderValue{ 2077 { 2078 Value: uint64(dexeth.GweiFactor) / 2, 2079 MaxSwapCount: 2, 2080 }, 2081 { 2082 Value: uint64(dexeth.GweiFactor) / 2, 2083 MaxSwapCount: 2, 2084 }, 2085 }, 2086 }, 2087 maxLock: uint64(dexeth.GweiFactor) + swapGas*4*fromAsset.MaxFeeRate, 2088 }, 2089 { 2090 name: "maxLock not enough, eth", 2091 ethOnly: true, 2092 bal: uint64(dexeth.GweiFactor) + swapGas*4*fromAsset.MaxFeeRate, 2093 tokenBal: uint64(dexeth.GweiFactor), 2094 parentBal: uint64(dexeth.GweiFactor), 2095 multiOrder: &asset.MultiOrder{ 2096 Version: fromAsset.Version, 2097 MaxFeeRate: fromAsset.MaxFeeRate, 2098 Values: []*asset.MultiOrderValue{ 2099 { 2100 Value: uint64(dexeth.GweiFactor) / 2, 2101 MaxSwapCount: 2, 2102 }, 2103 { 2104 Value: uint64(dexeth.GweiFactor) / 2, 2105 MaxSwapCount: 2, 2106 }, 2107 }, 2108 }, 2109 maxLock: uint64(dexeth.GweiFactor) + swapGas*4*fromAsset.MaxFeeRate - 1, 2110 expectErr: true, 2111 }, 2112 { 2113 name: "maxLock just enough, token", 2114 tokenOnly: true, 2115 bal: uint64(dexeth.GweiFactor) + swapGas*4*fromAsset.MaxFeeRate, 2116 tokenBal: uint64(dexeth.GweiFactor), 2117 parentBal: uint64(dexeth.GweiFactor), 2118 multiOrder: &asset.MultiOrder{ 2119 Version: fromAsset.Version, 2120 MaxFeeRate: fromAsset.MaxFeeRate, 2121 Values: []*asset.MultiOrderValue{ 2122 { 2123 Value: uint64(dexeth.GweiFactor) / 2, 2124 MaxSwapCount: 2, 2125 }, 2126 { 2127 Value: uint64(dexeth.GweiFactor) / 2, 2128 MaxSwapCount: 2, 2129 }, 2130 }, 2131 }, 2132 maxLock: uint64(dexeth.GweiFactor), 2133 }, 2134 { 2135 name: "maxLock not enough, eth", 2136 tokenOnly: true, 2137 bal: uint64(dexeth.GweiFactor) + swapGas*4*fromAsset.MaxFeeRate, 2138 tokenBal: uint64(dexeth.GweiFactor), 2139 parentBal: uint64(dexeth.GweiFactor), 2140 multiOrder: &asset.MultiOrder{ 2141 Version: fromAsset.Version, 2142 MaxFeeRate: fromAsset.MaxFeeRate, 2143 Values: []*asset.MultiOrderValue{ 2144 { 2145 Value: uint64(dexeth.GweiFactor) / 2, 2146 MaxSwapCount: 2, 2147 }, 2148 { 2149 Value: uint64(dexeth.GweiFactor) / 2, 2150 MaxSwapCount: 2, 2151 }, 2152 }, 2153 }, 2154 maxLock: uint64(dexeth.GweiFactor) + swapGas*4*fromAsset.MaxFeeRate, 2155 }, 2156 2157 { 2158 name: "insufficient balance", 2159 bal: uint64(dexeth.GweiFactor) + swapGas*4*fromAsset.MaxFeeRate - 1, 2160 tokenBal: uint64(dexeth.GweiFactor) - 1, 2161 parentBal: uint64(dexeth.GweiFactor), 2162 multiOrder: &asset.MultiOrder{ 2163 Version: fromAsset.Version, 2164 MaxFeeRate: fromAsset.MaxFeeRate, 2165 Values: []*asset.MultiOrderValue{ 2166 { 2167 Value: uint64(dexeth.GweiFactor) / 2, 2168 MaxSwapCount: 2, 2169 }, 2170 { 2171 Value: uint64(dexeth.GweiFactor) / 2, 2172 MaxSwapCount: 2, 2173 }, 2174 }, 2175 }, 2176 expectErr: true, 2177 }, 2178 { 2179 name: "parent balance ok", 2180 tokenOnly: true, 2181 tokenBal: uint64(dexeth.GweiFactor), 2182 parentBal: swapGas * 4 * fromAsset.MaxFeeRate, 2183 multiOrder: &asset.MultiOrder{ 2184 Version: fromAsset.Version, 2185 MaxFeeRate: fromAsset.MaxFeeRate, 2186 Values: []*asset.MultiOrderValue{ 2187 { 2188 Value: uint64(dexeth.GweiFactor) / 2, 2189 MaxSwapCount: 2, 2190 }, 2191 { 2192 Value: uint64(dexeth.GweiFactor) / 2, 2193 MaxSwapCount: 2, 2194 }, 2195 }, 2196 }, 2197 }, 2198 { 2199 name: "insufficient parent balance", 2200 tokenOnly: true, 2201 tokenBal: uint64(dexeth.GweiFactor), 2202 parentBal: swapGas*4*fromAsset.MaxFeeRate - 1, 2203 multiOrder: &asset.MultiOrder{ 2204 Version: fromAsset.Version, 2205 MaxFeeRate: fromAsset.MaxFeeRate, 2206 Values: []*asset.MultiOrderValue{ 2207 { 2208 Value: uint64(dexeth.GweiFactor) / 2, 2209 MaxSwapCount: 2, 2210 }, 2211 { 2212 Value: uint64(dexeth.GweiFactor) / 2, 2213 MaxSwapCount: 2, 2214 }, 2215 }, 2216 }, 2217 expectErr: true, 2218 }, 2219 } 2220 2221 for _, test := range tests { 2222 node.setBalanceError(eth, nil) 2223 if assetID == BipID { 2224 if test.tokenOnly { 2225 continue 2226 } 2227 node.bal = dexeth.GweiToWei(test.bal) 2228 } else { 2229 if test.ethOnly { 2230 continue 2231 } 2232 node.tokenContractor.bal = dexeth.GweiToWei(test.tokenBal) 2233 node.tokenParent.node.(*tMempoolNode).bal = dexeth.GweiToWei(test.parentBal) 2234 } 2235 eth.lockedFunds.initiateReserves = 0 2236 eth.baseWallet.wallets[BipID].lockedFunds.initiateReserves = 0 2237 2238 allCoins, redeemScripts, _, err := w.FundMultiOrder(test.multiOrder, test.maxLock) 2239 if test.expectErr { 2240 if err == nil { 2241 t.Fatalf("%s: expected error but did not get one", test.name) 2242 } 2243 continue 2244 } 2245 if err != nil { 2246 t.Fatalf("%s: unexpected error: %v", test.name, err) 2247 } 2248 if len(allCoins) != len(test.multiOrder.Values) { 2249 t.Fatalf("%s: expected %d coins but got %d", test.name, len(test.multiOrder.Values), len(allCoins)) 2250 } 2251 if len(redeemScripts) != len(test.multiOrder.Values) { 2252 t.Fatalf("%s: expected %d redeem scripts but got %d", test.name, len(test.multiOrder.Values), len(redeemScripts)) 2253 } 2254 for i, coins := range allCoins { 2255 if len(coins) != 1 { 2256 t.Fatalf("%s: expected 1 coin but got %d", test.name, len(coins)) 2257 } 2258 expectedValue := test.multiOrder.Values[i].Value 2259 if assetID == BipID { 2260 expectedValue += swapGas * test.multiOrder.Values[i].MaxSwapCount * fromAsset.MaxFeeRate 2261 } 2262 if coins[0].Value() != expectedValue { 2263 t.Fatalf("%s: expected coin %d value %d but got %d", test.name, i, expectedValue, coins[0].Value()) 2264 } 2265 } 2266 } 2267 } 2268 2269 func TestPreSwap(t *testing.T) { 2270 const baseFee, tip = 42, 2 2271 const feeSuggestion = 90 // ignored by eth's PreSwap 2272 const lotSize = 10e9 2273 oneFee := ethGases.Swap * tETH.MaxFeeRate 2274 refund := ethGases.Refund * tETH.MaxFeeRate 2275 oneLock := lotSize + oneFee + refund 2276 2277 oneFeeToken := tokenGases.Swap*tToken.MaxFeeRate + tokenGases.Refund*tToken.MaxFeeRate 2278 2279 type testData struct { 2280 name string 2281 bal uint64 2282 balErr error 2283 lots uint64 2284 token bool 2285 parentBal uint64 2286 2287 wantErr bool 2288 wantLots uint64 2289 wantValue uint64 2290 wantMaxFees uint64 2291 wantWorstCase uint64 2292 wantBestCase uint64 2293 } 2294 2295 tests := []testData{ 2296 { 2297 name: "no balance", 2298 bal: 0, 2299 lots: 1, 2300 2301 wantErr: true, 2302 }, 2303 { 2304 name: "not enough for fees", 2305 bal: lotSize, 2306 lots: 1, 2307 2308 wantErr: true, 2309 }, 2310 { 2311 name: "not enough for fees - token", 2312 bal: lotSize, 2313 parentBal: oneFeeToken - 1, 2314 lots: 1, 2315 token: true, 2316 2317 wantErr: true, 2318 }, 2319 { 2320 name: "one lot enough for fees", 2321 bal: oneLock, 2322 lots: 1, 2323 2324 wantLots: 1, 2325 wantValue: lotSize, 2326 wantMaxFees: tETH.MaxFeeRate * ethGases.Swap, 2327 wantBestCase: (baseFee + tip) * ethGases.Swap, 2328 wantWorstCase: (baseFee + tip) * ethGases.Swap, 2329 }, 2330 { 2331 name: "one lot enough for fees - token", 2332 bal: lotSize, 2333 lots: 1, 2334 parentBal: oneFeeToken, 2335 token: true, 2336 2337 wantLots: 1, 2338 wantValue: lotSize, 2339 wantMaxFees: tToken.MaxFeeRate * tokenGases.Swap, 2340 wantBestCase: (baseFee + tip) * tokenGases.Swap, 2341 wantWorstCase: (baseFee + tip) * tokenGases.Swap, 2342 }, 2343 { 2344 name: "more lots than max lots", 2345 bal: oneLock*2 - 1, 2346 lots: 2, 2347 2348 wantErr: true, 2349 }, 2350 { 2351 name: "more lots than max lots - token", 2352 bal: lotSize*2 - 1, 2353 lots: 2, 2354 token: true, 2355 parentBal: oneFeeToken * 2, 2356 2357 wantErr: true, 2358 }, 2359 { 2360 name: "fewer than max lots", 2361 bal: 10 * oneLock, 2362 lots: 4, 2363 2364 wantLots: 4, 2365 wantValue: 4 * lotSize, 2366 wantMaxFees: 4 * tETH.MaxFeeRate * ethGases.Swap, 2367 wantBestCase: (baseFee + tip) * ethGases.Swap, 2368 wantWorstCase: 4 * (baseFee + tip) * ethGases.Swap, 2369 }, 2370 { 2371 name: "fewer than max lots - token", 2372 bal: 10 * lotSize, 2373 lots: 4, 2374 token: true, 2375 parentBal: oneFeeToken * 4, 2376 2377 wantLots: 4, 2378 wantValue: 4 * lotSize, 2379 wantMaxFees: 4 * tToken.MaxFeeRate * tokenGases.Swap, 2380 wantBestCase: (baseFee + tip) * tokenGases.Swap, 2381 wantWorstCase: 4 * (baseFee + tip) * tokenGases.Swap, 2382 }, 2383 { 2384 name: "balanceError", 2385 bal: 5 * lotSize, 2386 balErr: errors.New("test error"), 2387 lots: 1, 2388 2389 wantErr: true, 2390 }, 2391 { 2392 name: "balanceError - token", 2393 bal: 5 * lotSize, 2394 balErr: errors.New("test error"), 2395 lots: 1, 2396 token: true, 2397 2398 wantErr: true, 2399 }, 2400 } 2401 2402 runTest := func(t *testing.T, test testData) { 2403 var assetID uint32 = BipID 2404 assetCfg := tETH 2405 if test.token { 2406 assetID = usdcTokenID 2407 assetCfg = tToken 2408 } 2409 2410 w, _, node, shutdown := tassetWallet(assetID) 2411 defer shutdown() 2412 node.baseFee, node.tip = dexeth.GweiToWei(baseFee), dexeth.GweiToWei(tip) 2413 2414 if test.token { 2415 node.tContractor.gasEstimates = &tokenGases 2416 node.tokenContractor.bal = dexeth.GweiToWei(test.bal) 2417 node.bal = dexeth.GweiToWei(test.parentBal) 2418 } else { 2419 node.bal = dexeth.GweiToWei(test.bal) 2420 } 2421 2422 node.balErr = test.balErr 2423 2424 preSwap, err := w.PreSwap(&asset.PreSwapForm{ 2425 Version: assetCfg.Version, 2426 LotSize: lotSize, 2427 Lots: test.lots, 2428 MaxFeeRate: assetCfg.MaxFeeRate, 2429 FeeSuggestion: feeSuggestion, // ignored 2430 RedeemVersion: tBTC.Version, 2431 RedeemAssetID: tBTC.ID, 2432 }) 2433 2434 if test.wantErr { 2435 if err == nil { 2436 t.Fatalf("expected error") 2437 } 2438 return 2439 } 2440 if err != nil { 2441 t.Fatalf("unexpected error: %v", err) 2442 } 2443 2444 est := preSwap.Estimate 2445 2446 if est.Lots != test.wantLots { 2447 t.Fatalf("want lots %v got %v", test.wantLots, est.Lots) 2448 } 2449 if est.Value != test.wantValue { 2450 t.Fatalf("want value %v got %v", test.wantValue, est.Value) 2451 } 2452 if est.MaxFees != test.wantMaxFees { 2453 t.Fatalf("want maxFees %v got %v", test.wantMaxFees, est.MaxFees) 2454 } 2455 if est.RealisticBestCase != test.wantBestCase { 2456 t.Fatalf("want best case %v got %v", test.wantBestCase, est.RealisticBestCase) 2457 } 2458 if est.RealisticWorstCase != test.wantWorstCase { 2459 t.Fatalf("want worst case %v got %v", test.wantWorstCase, est.RealisticWorstCase) 2460 } 2461 } 2462 2463 for _, test := range tests { 2464 t.Run(test.name, func(t *testing.T) { 2465 runTest(t, test) 2466 }) 2467 } 2468 } 2469 2470 func TestSwap(t *testing.T) { 2471 t.Run("eth", func(t *testing.T) { testSwap(t, BipID) }) 2472 t.Run("token", func(t *testing.T) { testSwap(t, usdcTokenID) }) 2473 } 2474 2475 func testSwap(t *testing.T, assetID uint32) { 2476 w, eth, node, shutdown := tassetWallet(assetID) 2477 defer shutdown() 2478 2479 receivingAddress := "0x2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27" 2480 node.tContractor.initTx = types.NewTx(&types.DynamicFeeTx{}) 2481 2482 coinIDsForAmounts := func(coinAmounts []uint64, n uint64) []dex.Bytes { 2483 coinIDs := make([]dex.Bytes, 0, len(coinAmounts)) 2484 for _, amt := range coinAmounts { 2485 if assetID == BipID { 2486 coinIDs = append(coinIDs, createFundingCoin(eth.addr, amt).RecoveryID()) 2487 } else { 2488 fees := n * tokenGases.Swap * tToken.MaxFeeRate 2489 coinIDs = append(coinIDs, createTokenFundingCoin(eth.addr, amt, fees).RecoveryID()) 2490 } 2491 } 2492 return coinIDs 2493 } 2494 2495 refreshWalletAndFundCoins := func(bal uint64, coinAmounts []uint64, n uint64) asset.Coins { 2496 if assetID == BipID { 2497 node.bal = ethToWei(bal) 2498 } else { 2499 node.tokenContractor.bal = ethToWei(bal) 2500 node.bal = ethToWei(10) 2501 } 2502 2503 eth.lockedFunds.initiateReserves = 0 2504 eth.lockedFunds.redemptionReserves = 0 2505 eth.lockedFunds.refundReserves = 0 2506 coins, err := w.FundingCoins(coinIDsForAmounts(coinAmounts, n)) 2507 if err != nil { 2508 t.Fatalf("FundingCoins error: %v", err) 2509 } 2510 return coins 2511 } 2512 2513 gasNeededForSwaps := func(numSwaps int) uint64 { 2514 if assetID == BipID { 2515 return ethGases.Swap * uint64(numSwaps) 2516 } else { 2517 return tokenGases.Swap * uint64(numSwaps) 2518 } 2519 2520 } 2521 2522 testSwap := func(testName string, swaps asset.Swaps, expectError bool) { 2523 originalBalance, err := eth.Balance() 2524 if err != nil { 2525 t.Fatalf("%v: error getting balance: %v", testName, err) 2526 } 2527 2528 receipts, changeCoin, feeSpent, err := w.Swap(&swaps) 2529 if expectError { 2530 if err == nil { 2531 t.Fatalf("%v: expected error but did not get", testName) 2532 } 2533 return 2534 } 2535 if err != nil { 2536 t.Fatalf("%v: unexpected error doing Swap: %v", testName, err) 2537 } 2538 2539 if len(receipts) != len(swaps.Contracts) { 2540 t.Fatalf("%v: num receipts %d != num contracts %d", 2541 testName, len(receipts), len(swaps.Contracts)) 2542 } 2543 2544 var totalCoinValue uint64 2545 for i, contract := range swaps.Contracts { 2546 // Check that receipts match the contract inputs 2547 receipt := receipts[i] 2548 if uint64(receipt.Expiration().Unix()) != contract.LockTime { 2549 t.Fatalf("%v: expected expiration %v != expiration %v", 2550 testName, time.Unix(int64(contract.LockTime), 0), receipts[0].Expiration()) 2551 } 2552 if receipt.Coin().Value() != contract.Value { 2553 t.Fatalf("%v: receipt coin value: %v != expected: %v", 2554 testName, receipt.Coin().Value(), contract.Value) 2555 } 2556 contractData := receipt.Contract() 2557 ver, secretHash, err := dexeth.DecodeContractData(contractData) 2558 if err != nil { 2559 t.Fatalf("failed to decode contract data: %v", err) 2560 } 2561 if swaps.Version != ver { 2562 t.Fatal("wrong contract version") 2563 } 2564 if !bytes.Equal(contract.SecretHash, secretHash[:]) { 2565 t.Fatalf("%v, contract: %x != secret hash in input: %x", 2566 testName, receipt.Contract(), secretHash) 2567 } 2568 2569 totalCoinValue += receipt.Coin().Value() 2570 } 2571 2572 var totalInputValue uint64 2573 for _, coin := range swaps.Inputs { 2574 totalInputValue += coin.Value() 2575 } 2576 2577 // Check that the coins used in swaps are no longer locked 2578 postSwapBalance, err := eth.Balance() 2579 if err != nil { 2580 t.Fatalf("%v: error getting balance: %v", testName, err) 2581 } 2582 var expectedLocked uint64 2583 if swaps.LockChange { 2584 expectedLocked = originalBalance.Locked - totalCoinValue 2585 if assetID == BipID { 2586 expectedLocked -= gasNeededForSwaps(len(swaps.Contracts)) * swaps.FeeRate 2587 } 2588 } else { 2589 expectedLocked = originalBalance.Locked - totalInputValue 2590 } 2591 if expectedLocked != postSwapBalance.Locked { 2592 t.Fatalf("%v: funds locked after swap expected: %v != actual: %v", 2593 testName, expectedLocked, postSwapBalance.Locked) 2594 } 2595 2596 // Check that change coin is correctly returned 2597 expectedChangeValue := totalInputValue - totalCoinValue 2598 if assetID == BipID { 2599 expectedChangeValue -= gasNeededForSwaps(len(swaps.Contracts)) * swaps.FeeRate 2600 } 2601 if expectedChangeValue == 0 && changeCoin != nil { 2602 t.Fatalf("%v: change coin should be nil if change is 0", testName) 2603 } else if expectedChangeValue > 0 && changeCoin == nil && swaps.LockChange { 2604 t.Fatalf("%v: change coin should not be nil if there is expected change and change is locked", 2605 testName) 2606 } else if !swaps.LockChange && changeCoin != nil { 2607 t.Fatalf("%v: change should be nil if LockChange==False", testName) 2608 } else if changeCoin != nil && changeCoin.Value() != expectedChangeValue { 2609 t.Fatalf("%v: expected change value %v != change coin value: %v", 2610 testName, expectedChangeValue, changeCoin.Value()) 2611 } 2612 2613 expectedFees := gasNeededForSwaps(len(swaps.Contracts)) * swaps.FeeRate 2614 if feeSpent != expectedFees { 2615 t.Fatalf("%v: expected fees: %v != actual fees %v", testName, expectedFees, feeSpent) 2616 } 2617 } 2618 2619 secret := encode.RandomBytes(32) 2620 secretHash := sha256.Sum256(secret) 2621 secret2 := encode.RandomBytes(32) 2622 secretHash2 := sha256.Sum256(secret2) 2623 expiration := uint64(time.Now().Add(time.Hour * 8).Unix()) 2624 2625 // Ensure error when initializing swap errors 2626 node.tContractor.initErr = errors.New("test error") 2627 contracts := []*asset.Contract{ 2628 { 2629 Address: receivingAddress, 2630 Value: ethToGwei(1), 2631 SecretHash: secretHash[:], 2632 LockTime: expiration, 2633 }, 2634 } 2635 inputs := refreshWalletAndFundCoins(5, []uint64{ethToGwei(2)}, 1) 2636 assetCfg := tETH 2637 if assetID != BipID { 2638 assetCfg = tToken 2639 } 2640 swaps := asset.Swaps{ 2641 Version: assetCfg.Version, 2642 Inputs: inputs, 2643 Contracts: contracts, 2644 FeeRate: assetCfg.MaxFeeRate, 2645 LockChange: false, 2646 } 2647 testSwap("error initialize but no send", swaps, true) 2648 node.tContractor.initErr = nil 2649 2650 // Tests one contract without locking change 2651 contracts = []*asset.Contract{ 2652 { 2653 Address: receivingAddress, 2654 Value: ethToGwei(1), 2655 SecretHash: secretHash[:], 2656 LockTime: expiration, 2657 }, 2658 } 2659 2660 inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2)}, 1) 2661 swaps = asset.Swaps{ 2662 Version: assetCfg.Version, 2663 Inputs: inputs, 2664 Contracts: contracts, 2665 FeeRate: assetCfg.MaxFeeRate, 2666 LockChange: false, 2667 } 2668 testSwap("one contract, don't lock change", swaps, false) 2669 2670 // Test one contract with locking change 2671 inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2)}, 1) 2672 swaps = asset.Swaps{ 2673 Version: assetCfg.Version, 2674 Inputs: inputs, 2675 Contracts: contracts, 2676 FeeRate: assetCfg.MaxFeeRate, 2677 LockChange: true, 2678 } 2679 testSwap("one contract, lock change", swaps, false) 2680 2681 // Test two contracts 2682 contracts = []*asset.Contract{ 2683 { 2684 Address: receivingAddress, 2685 Value: ethToGwei(1), 2686 SecretHash: secretHash[:], 2687 LockTime: expiration, 2688 }, 2689 { 2690 Address: receivingAddress, 2691 Value: ethToGwei(1), 2692 SecretHash: secretHash2[:], 2693 LockTime: expiration, 2694 }, 2695 } 2696 inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(3)}, 2) 2697 swaps = asset.Swaps{ 2698 Version: assetCfg.Version, 2699 Inputs: inputs, 2700 Contracts: contracts, 2701 FeeRate: assetCfg.MaxFeeRate, 2702 LockChange: false, 2703 } 2704 testSwap("two contracts", swaps, false) 2705 2706 // Test error when funding coins are not enough to cover swaps 2707 inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(1)}, 2) 2708 swaps = asset.Swaps{ 2709 Version: assetCfg.Version, 2710 Inputs: inputs, 2711 Contracts: contracts, 2712 FeeRate: assetCfg.MaxFeeRate, 2713 LockChange: false, 2714 } 2715 testSwap("funding coins not enough balance", swaps, true) 2716 2717 // Ensure when funds are exactly the same as required works properly 2718 inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2) + (2 * 200 * dexeth.InitGas(1, 0))}, 2) 2719 swaps = asset.Swaps{ 2720 Inputs: inputs, 2721 Version: assetCfg.Version, 2722 Contracts: contracts, 2723 FeeRate: assetCfg.MaxFeeRate, 2724 LockChange: false, 2725 } 2726 testSwap("exact change", swaps, false) 2727 } 2728 2729 func TestPreRedeem(t *testing.T) { 2730 w, _, _, shutdown := tassetWallet(BipID) 2731 defer shutdown() 2732 2733 form := &asset.PreRedeemForm{ 2734 Version: tETH.Version, 2735 Lots: 5, 2736 FeeSuggestion: 100, 2737 } 2738 2739 preRedeem, err := w.PreRedeem(form) 2740 if err != nil { 2741 t.Fatalf("unexpected PreRedeem error: %v", err) 2742 } 2743 2744 if preRedeem.Estimate.RealisticBestCase >= preRedeem.Estimate.RealisticWorstCase { 2745 t.Fatalf("best case > worst case") 2746 } 2747 2748 // Token 2749 w, _, _, shutdown2 := tassetWallet(usdcTokenID) 2750 defer shutdown2() 2751 2752 form.Version = tToken.Version 2753 2754 preRedeem, err = w.PreRedeem(form) 2755 if err != nil { 2756 t.Fatalf("unexpected token PreRedeem error: %v", err) 2757 } 2758 2759 if preRedeem.Estimate.RealisticBestCase >= preRedeem.Estimate.RealisticWorstCase { 2760 t.Fatalf("token best case > worst case") 2761 } 2762 } 2763 2764 func TestRedeem(t *testing.T) { 2765 t.Run("eth", func(t *testing.T) { testRedeem(t, BipID) }) 2766 t.Run("token", func(t *testing.T) { testRedeem(t, usdcTokenID) }) 2767 } 2768 2769 func testRedeem(t *testing.T, assetID uint32) { 2770 w, eth, node, shutdown := tassetWallet(assetID) 2771 defer shutdown() 2772 2773 // Test with a non-zero contract version to ensure it makes it into the receipt 2774 contractVer := uint32(1) 2775 2776 eth.versionedGases[1] = ethGases 2777 if assetID != BipID { 2778 eth.versionedGases[1] = &tokenGases 2779 } 2780 2781 tokenContracts := eth.tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts 2782 tokenContracts[1] = tokenContracts[0] 2783 defer delete(tokenContracts, 1) 2784 2785 contractorV1 := &tContractor{ 2786 swapMap: make(map[[32]byte]*dexeth.SwapState, 1), 2787 gasEstimates: ethGases, 2788 redeemTx: types.NewTx(&types.DynamicFeeTx{Data: []byte{1, 2, 3}}), 2789 } 2790 var c contractor = contractorV1 2791 if assetID != BipID { 2792 c = &tTokenContractor{ 2793 tContractor: contractorV1, 2794 } 2795 } 2796 eth.contractors[1] = c 2797 2798 addSwapToSwapMap := func(secretHash [32]byte, value uint64, step dexeth.SwapStep) { 2799 swap := dexeth.SwapState{ 2800 BlockHeight: 1, 2801 LockTime: time.Now(), 2802 Initiator: testAddressB, 2803 Participant: testAddressA, 2804 Value: dexeth.GweiToWei(value), 2805 State: step, 2806 } 2807 contractorV1.swapMap[secretHash] = &swap 2808 } 2809 2810 numSecrets := 3 2811 secrets := make([][32]byte, 0, numSecrets) 2812 secretHashes := make([][32]byte, 0, numSecrets) 2813 for i := 0; i < numSecrets; i++ { 2814 var secret [32]byte 2815 copy(secret[:], encode.RandomBytes(32)) 2816 secretHash := sha256.Sum256(secret[:]) 2817 secrets = append(secrets, secret) 2818 secretHashes = append(secretHashes, secretHash) 2819 } 2820 2821 addSwapToSwapMap(secretHashes[0], 1e9, dexeth.SSInitiated) // states will be reset by tests though 2822 addSwapToSwapMap(secretHashes[1], 1e9, dexeth.SSInitiated) 2823 2824 /* COMMENTED while estimateRedeemGas is on the $#!t list 2825 var redeemGas uint64 2826 if assetID == BipID { 2827 redeemGas = ethGases.Redeem 2828 } else { 2829 redeemGas = tokenGases.Redeem 2830 } 2831 2832 var higherGasEstimate uint64 = redeemGas * 2 * 12 / 10 // 120% of estimate 2833 var doubleGasEstimate uint64 = (redeemGas * 2 * 2) * 10 / 11 // 200% of estimate after 10% increase 2834 // var moreThanDoubleGasEstimate uint64 = (redeemGas * 2 * 21 / 10) * 10 / 11 // > 200% of estimate after 10% increase 2835 // additionalFundsNeeded calculates the amount of available funds that we be 2836 // needed to use a higher gas estimate than the original, and double the base 2837 // fee if it is higher than the server's max fee rate. 2838 additionalFundsNeeded := func(feeSuggestion, baseFee, gasEstimate, numRedeems uint64) uint64 { 2839 originalReserves := feeSuggestion * redeemGas * numRedeems 2840 2841 var gasFeeCap, gasLimit uint64 2842 if gasEstimate > redeemGas { 2843 gasLimit = gasEstimate * 11 / 10 2844 } else { 2845 gasLimit = redeemGas * numRedeems 2846 } 2847 2848 if baseFee > feeSuggestion { 2849 gasFeeCap = 2 * baseFee 2850 } else { 2851 gasFeeCap = feeSuggestion 2852 } 2853 2854 amountRequired := gasFeeCap * gasLimit 2855 2856 return amountRequired - originalReserves 2857 } 2858 */ 2859 2860 var bestBlock int64 = 123 2861 node.bestHdr = &types.Header{ 2862 Number: big.NewInt(bestBlock), 2863 } 2864 2865 swappableSwapMap := map[[32]byte]dexeth.SwapStep{ 2866 secretHashes[0]: dexeth.SSInitiated, 2867 secretHashes[1]: dexeth.SSInitiated, 2868 } 2869 2870 tests := []struct { 2871 name string 2872 form asset.RedeemForm 2873 redeemErr error 2874 swapMap map[[32]byte]dexeth.SwapStep 2875 swapErr error 2876 ethBal *big.Int 2877 baseFee *big.Int 2878 redeemGasOverride *uint64 2879 expectedGasFeeCap *big.Int 2880 expectError bool 2881 }{ 2882 { 2883 name: "ok", 2884 expectError: false, 2885 swapMap: swappableSwapMap, 2886 ethBal: dexeth.GweiToWei(10e9), 2887 baseFee: dexeth.GweiToWei(100), 2888 expectedGasFeeCap: dexeth.GweiToWei(100), 2889 form: asset.RedeemForm{ 2890 Redemptions: []*asset.Redemption{ 2891 { 2892 Spends: &asset.AuditInfo{ 2893 Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), 2894 SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth 2895 Coin: &coin{ 2896 id: randomHash(), 2897 }, 2898 }, 2899 Secret: secrets[0][:], 2900 }, 2901 { 2902 Spends: &asset.AuditInfo{ 2903 Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), 2904 SecretHash: secretHashes[1][:], 2905 Coin: &coin{ 2906 id: randomHash(), 2907 }, 2908 }, 2909 Secret: secrets[1][:], 2910 }, 2911 }, 2912 FeeSuggestion: 100, 2913 }, 2914 }, 2915 /* COMMENTED while estimateRedeemGas is on the $#!t list 2916 { 2917 name: "higher gas estimate than reserved", 2918 expectError: false, 2919 swapMap: swappableSwapMap, 2920 ethBal: dexeth.GweiToWei(additionalFundsNeeded(100, 50, higherGasEstimate, 2)), 2921 baseFee: dexeth.GweiToWei(100), 2922 expectedGasFeeCap: dexeth.GweiToWei(100), 2923 redeemGasOverride: &higherGasEstimate, 2924 form: asset.RedeemForm{ 2925 Redemptions: []*asset.Redemption{ 2926 { 2927 Spends: &asset.AuditInfo{ 2928 Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), 2929 SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth 2930 Coin: &coin{ 2931 id: randomHash(), 2932 }, 2933 }, 2934 Secret: secrets[0][:], 2935 }, 2936 { 2937 Spends: &asset.AuditInfo{ 2938 Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), 2939 SecretHash: secretHashes[1][:], 2940 Coin: &coin{ 2941 id: randomHash(), 2942 }, 2943 }, 2944 Secret: secrets[1][:], 2945 }, 2946 }, 2947 FeeSuggestion: 100, 2948 }, 2949 }, 2950 { 2951 name: "gas estimate double reserved", 2952 expectError: false, 2953 swapMap: swappableSwapMap, 2954 ethBal: dexeth.GweiToWei(10e9), 2955 baseFee: dexeth.GweiToWei(100), 2956 expectedGasFeeCap: dexeth.GweiToWei(100), 2957 redeemGasOverride: &doubleGasEstimate, 2958 form: asset.RedeemForm{ 2959 Redemptions: []*asset.Redemption{ 2960 { 2961 Spends: &asset.AuditInfo{ 2962 Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), 2963 SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth 2964 Coin: &coin{ 2965 id: randomHash(), 2966 }, 2967 }, 2968 Secret: secrets[0][:], 2969 }, 2970 { 2971 Spends: &asset.AuditInfo{ 2972 Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), 2973 SecretHash: secretHashes[1][:], 2974 Coin: &coin{ 2975 id: randomHash(), 2976 }, 2977 }, 2978 Secret: secrets[1][:], 2979 }, 2980 }, 2981 FeeSuggestion: 100, 2982 }, 2983 }, 2984 { 2985 name: "gas estimate more than double reserved", 2986 expectError: true, 2987 swapMap: swappableSwapMap, 2988 ethBal: dexeth.GweiToWei(additionalFundsNeeded(100, 50, moreThanDoubleGasEstimate, 2)), 2989 baseFee: dexeth.GweiToWei(100), 2990 redeemGasOverride: &moreThanDoubleGasEstimate, 2991 form: asset.RedeemForm{ 2992 Redemptions: []*asset.Redemption{ 2993 { 2994 Spends: &asset.AuditInfo{ 2995 Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), 2996 SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth 2997 Coin: &coin{ 2998 id: randomHash(), 2999 }, 3000 }, 3001 Secret: secrets[0][:], 3002 }, 3003 { 3004 Spends: &asset.AuditInfo{ 3005 Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), 3006 SecretHash: secretHashes[1][:], 3007 Coin: &coin{ 3008 id: randomHash(), 3009 }, 3010 }, 3011 Secret: secrets[1][:], 3012 }, 3013 }, 3014 FeeSuggestion: 100, 3015 }, 3016 }, 3017 { 3018 name: "higher gas estimate than reserved, balance too low", 3019 expectError: true, 3020 swapMap: swappableSwapMap, 3021 ethBal: dexeth.GweiToWei(additionalFundsNeeded(100, 50, higherGasEstimate, 2) - 1), 3022 baseFee: dexeth.GweiToWei(100), 3023 redeemGasOverride: &higherGasEstimate, 3024 form: asset.RedeemForm{ 3025 Redemptions: []*asset.Redemption{ 3026 { 3027 Spends: &asset.AuditInfo{ 3028 Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), 3029 SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth 3030 Coin: &coin{ 3031 id: randomHash(), 3032 }, 3033 }, 3034 Secret: secrets[0][:], 3035 }, 3036 { 3037 Spends: &asset.AuditInfo{ 3038 Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), 3039 SecretHash: secretHashes[1][:], 3040 Coin: &coin{ 3041 id: randomHash(), 3042 }, 3043 }, 3044 Secret: secrets[1][:], 3045 }, 3046 }, 3047 FeeSuggestion: 100, 3048 }, 3049 }, 3050 { 3051 name: "base fee > fee suggestion", 3052 expectError: false, 3053 swapMap: swappableSwapMap, 3054 ethBal: dexeth.GweiToWei(additionalFundsNeeded(100, 200, higherGasEstimate, 2)), 3055 baseFee: dexeth.GweiToWei(150), 3056 expectedGasFeeCap: dexeth.GweiToWei(300), 3057 redeemGasOverride: &higherGasEstimate, 3058 form: asset.RedeemForm{ 3059 Redemptions: []*asset.Redemption{ 3060 { 3061 Spends: &asset.AuditInfo{ 3062 Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), 3063 SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth 3064 Coin: &coin{ 3065 id: randomHash(), 3066 }, 3067 }, 3068 Secret: secrets[0][:], 3069 }, 3070 { 3071 Spends: &asset.AuditInfo{ 3072 Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), 3073 SecretHash: secretHashes[1][:], 3074 Coin: &coin{ 3075 id: randomHash(), 3076 }, 3077 }, 3078 Secret: secrets[1][:], 3079 }, 3080 }, 3081 FeeSuggestion: 100, 3082 }, 3083 }, 3084 { 3085 name: "base fee > fee suggestion, not enough for 2x base fee", 3086 expectError: false, 3087 swapMap: swappableSwapMap, 3088 ethBal: dexeth.GweiToWei(additionalFundsNeeded(100, 149, higherGasEstimate, 2)), 3089 baseFee: dexeth.GweiToWei(150), 3090 expectedGasFeeCap: dexeth.GweiToWei(298), 3091 redeemGasOverride: &higherGasEstimate, 3092 form: asset.RedeemForm{ 3093 Redemptions: []*asset.Redemption{ 3094 { 3095 Spends: &asset.AuditInfo{ 3096 Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), 3097 SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth 3098 Coin: &coin{ 3099 id: randomHash(), 3100 }, 3101 }, 3102 Secret: secrets[0][:], 3103 }, 3104 { 3105 Spends: &asset.AuditInfo{ 3106 Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), 3107 SecretHash: secretHashes[1][:], 3108 Coin: &coin{ 3109 id: randomHash(), 3110 }, 3111 }, 3112 Secret: secrets[1][:], 3113 }, 3114 }, 3115 FeeSuggestion: 100, 3116 }, 3117 }, 3118 */ 3119 { 3120 name: "not redeemable", 3121 expectError: true, 3122 swapMap: map[[32]byte]dexeth.SwapStep{ 3123 secretHashes[0]: dexeth.SSNone, 3124 secretHashes[1]: dexeth.SSRedeemed, 3125 }, 3126 ethBal: dexeth.GweiToWei(10e9), 3127 baseFee: dexeth.GweiToWei(100), 3128 3129 form: asset.RedeemForm{ 3130 Redemptions: []*asset.Redemption{ 3131 { 3132 Spends: &asset.AuditInfo{ 3133 Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), 3134 SecretHash: secretHashes[0][:], 3135 Coin: &coin{ 3136 id: randomHash(), 3137 }, 3138 }, 3139 Secret: secrets[0][:], 3140 }, 3141 { 3142 Spends: &asset.AuditInfo{ 3143 Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), 3144 SecretHash: secretHashes[1][:], 3145 Coin: &coin{ 3146 id: randomHash(), 3147 }, 3148 }, 3149 Secret: secrets[1][:], 3150 }, 3151 }, 3152 FeeSuggestion: 100, 3153 }, 3154 }, 3155 { 3156 name: "isRedeemable error", 3157 expectError: true, 3158 ethBal: dexeth.GweiToWei(10e9), 3159 baseFee: dexeth.GweiToWei(100), 3160 swapErr: errors.New("swap() error"), 3161 form: asset.RedeemForm{ 3162 Redemptions: []*asset.Redemption{ 3163 { 3164 Spends: &asset.AuditInfo{ 3165 Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), 3166 SecretHash: secretHashes[0][:], 3167 Coin: &coin{ 3168 id: randomHash(), 3169 }, 3170 }, 3171 Secret: secrets[0][:], 3172 }, 3173 { 3174 Spends: &asset.AuditInfo{ 3175 Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), 3176 SecretHash: secretHashes[1][:], 3177 Coin: &coin{ 3178 id: randomHash(), 3179 }, 3180 }, 3181 Secret: secrets[1][:], 3182 }, 3183 }, 3184 FeeSuggestion: 100, 3185 }, 3186 }, 3187 { 3188 name: "redeem error", 3189 redeemErr: errors.New("test error"), 3190 swapMap: swappableSwapMap, 3191 expectError: true, 3192 ethBal: dexeth.GweiToWei(10e9), 3193 baseFee: dexeth.GweiToWei(100), 3194 form: asset.RedeemForm{ 3195 Redemptions: []*asset.Redemption{ 3196 { 3197 Spends: &asset.AuditInfo{ 3198 Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), 3199 SecretHash: secretHashes[0][:], 3200 Coin: &coin{ 3201 id: randomHash(), 3202 }, 3203 }, 3204 Secret: secrets[0][:], 3205 }, 3206 }, 3207 FeeSuggestion: 200, 3208 }, 3209 }, 3210 { 3211 name: "swap not found in contract", 3212 swapMap: swappableSwapMap, 3213 expectError: true, 3214 ethBal: dexeth.GweiToWei(10e9), 3215 baseFee: dexeth.GweiToWei(100), 3216 form: asset.RedeemForm{ 3217 Redemptions: []*asset.Redemption{ 3218 { 3219 Spends: &asset.AuditInfo{ 3220 Contract: dexeth.EncodeContractData(contractVer, secretHashes[2]), 3221 SecretHash: secretHashes[2][:], 3222 Coin: &coin{ 3223 id: randomHash(), 3224 }, 3225 }, 3226 Secret: secrets[2][:], 3227 }, 3228 }, 3229 FeeSuggestion: 100, 3230 }, 3231 }, 3232 { 3233 name: "empty redemptions slice error", 3234 ethBal: dexeth.GweiToWei(10e9), 3235 baseFee: dexeth.GweiToWei(100), 3236 swapMap: swappableSwapMap, 3237 expectError: true, 3238 form: asset.RedeemForm{ 3239 Redemptions: []*asset.Redemption{}, 3240 FeeSuggestion: 100, 3241 }, 3242 }, 3243 } 3244 3245 for _, test := range tests { 3246 contractorV1.redeemErr = test.redeemErr 3247 contractorV1.swapErr = test.swapErr 3248 contractorV1.redeemGasOverride = test.redeemGasOverride 3249 for secretHash, step := range test.swapMap { 3250 contractorV1.swapMap[secretHash].State = step 3251 } 3252 node.bal = test.ethBal 3253 node.baseFee = test.baseFee 3254 3255 txs, out, fees, err := w.Redeem(&test.form) 3256 if test.expectError { 3257 if err == nil { 3258 t.Fatalf("%v: expected error", test.name) 3259 } 3260 continue 3261 } 3262 if err != nil { 3263 t.Fatalf("%v: unexpected Redeem error: %v", test.name, err) 3264 } 3265 3266 if len(txs) != len(test.form.Redemptions) { 3267 t.Fatalf("%v: expected %d txn but got %d", 3268 test.name, len(test.form.Redemptions), len(txs)) 3269 } 3270 3271 // Check fees returned from Redeem are as expected 3272 expectedGas := dexeth.RedeemGas(len(test.form.Redemptions), 0) 3273 if assetID != BipID { 3274 expectedGas = tokenGases.Redeem + (uint64(len(test.form.Redemptions))-1)*tokenGases.RedeemAdd 3275 } 3276 expectedFees := expectedGas * test.form.FeeSuggestion 3277 if fees != expectedFees { 3278 t.Fatalf("%v: expected fees %d, but got %d", test.name, expectedFees, fees) 3279 } 3280 3281 // Check that value of output coin is as axpected 3282 var totalSwapValue uint64 3283 for _, redemption := range test.form.Redemptions { 3284 _, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract) 3285 if err != nil { 3286 t.Fatalf("DecodeContractData: %v", err) 3287 } 3288 // secretHash should equal redemption.Spends.SecretHash, but it's 3289 // not part of the Redeem code, just the test input consistency. 3290 swap := contractorV1.swapMap[secretHash] 3291 totalSwapValue += dexeth.WeiToGwei(swap.Value) 3292 } 3293 if out.Value() != totalSwapValue { 3294 t.Fatalf("expected coin value to be %d but got %d", 3295 totalSwapValue, out.Value()) 3296 } 3297 3298 // Check that gas limit in the transaction is as expected 3299 var expectedGasLimit uint64 3300 // if test.redeemGasOverride == nil { 3301 if assetID == BipID { 3302 expectedGasLimit = ethGases.Redeem * uint64(len(test.form.Redemptions)) 3303 } else { 3304 expectedGasLimit = tokenGases.Redeem * uint64(len(test.form.Redemptions)) 3305 } 3306 // } else { 3307 // expectedGasLimit = *test.redeemGasOverride * 11 / 10 3308 // } 3309 if contractorV1.lastRedeemOpts.GasLimit != expectedGasLimit { 3310 t.Fatalf("%s: expected gas limit %d, but got %d", test.name, expectedGasLimit, contractorV1.lastRedeemOpts.GasLimit) 3311 } 3312 3313 // Check that the gas fee cap in the transaction is as expected 3314 if contractorV1.lastRedeemOpts.GasFeeCap.Cmp(test.expectedGasFeeCap) != 0 { 3315 t.Fatalf("%s: expected gas fee cap %v, but got %v", test.name, test.expectedGasFeeCap, contractorV1.lastRedeemOpts.GasFeeCap) 3316 } 3317 } 3318 } 3319 3320 func TestMaxOrder(t *testing.T) { 3321 const baseFee, tip = 42, 2 3322 3323 type testData struct { 3324 name string 3325 bal uint64 3326 balErr error 3327 lotSize uint64 3328 maxFeeRate uint64 3329 feeSuggestion uint64 3330 token bool 3331 parentBal uint64 3332 wantErr bool 3333 wantLots uint64 3334 wantValue uint64 3335 wantMaxFees uint64 3336 wantWorstCase uint64 3337 wantBestCase uint64 3338 wantLocked uint64 3339 } 3340 tests := []testData{ 3341 { 3342 name: "no balance", 3343 bal: 0, 3344 lotSize: 10, 3345 feeSuggestion: 90, 3346 maxFeeRate: 100, 3347 }, 3348 { 3349 name: "no balance - token", 3350 bal: 0, 3351 lotSize: 10, 3352 feeSuggestion: 90, 3353 maxFeeRate: 100, 3354 token: true, 3355 parentBal: 100, 3356 }, 3357 { 3358 name: "not enough for fees", 3359 bal: 10, 3360 lotSize: 10, 3361 feeSuggestion: 90, 3362 maxFeeRate: 100, 3363 }, 3364 { 3365 name: "not enough for fees - token", 3366 bal: 10, 3367 token: true, 3368 parentBal: 0, 3369 lotSize: 10, 3370 feeSuggestion: 90, 3371 maxFeeRate: 100, 3372 }, 3373 { 3374 name: "one lot enough for fees", 3375 bal: 11, 3376 lotSize: 10, 3377 feeSuggestion: 90, 3378 maxFeeRate: 100, 3379 wantLots: 1, 3380 wantValue: ethToGwei(10), 3381 wantMaxFees: 100 * ethGases.Swap, 3382 wantBestCase: (baseFee + tip) * ethGases.Swap, 3383 wantWorstCase: (baseFee + tip) * ethGases.Swap, 3384 wantLocked: ethToGwei(10) + (100 * ethGases.Swap), 3385 }, 3386 { 3387 name: "one lot enough for fees - token", 3388 bal: 11, 3389 lotSize: 10, 3390 feeSuggestion: 90, 3391 maxFeeRate: 100, 3392 token: true, 3393 parentBal: 1, 3394 wantLots: 1, 3395 wantValue: ethToGwei(10), 3396 wantMaxFees: 100 * tokenGases.Swap, 3397 wantBestCase: (baseFee + tip) * tokenGases.Swap, 3398 wantWorstCase: (baseFee + tip) * tokenGases.Swap, 3399 wantLocked: ethToGwei(10) + (100 * tokenGases.Swap), 3400 }, 3401 { 3402 name: "multiple lots", 3403 bal: 51, 3404 lotSize: 10, 3405 feeSuggestion: 90, 3406 maxFeeRate: 100, 3407 wantLots: 5, 3408 wantValue: ethToGwei(50), 3409 wantMaxFees: 5 * 100 * ethGases.Swap, 3410 wantBestCase: (baseFee + tip) * ethGases.Swap, 3411 wantWorstCase: 5 * (baseFee + tip) * ethGases.Swap, 3412 wantLocked: ethToGwei(50) + (5 * 100 * ethGases.Swap), 3413 }, 3414 { 3415 name: "multiple lots - token", 3416 bal: 51, 3417 lotSize: 10, 3418 feeSuggestion: 90, 3419 maxFeeRate: 100, 3420 token: true, 3421 parentBal: 1, 3422 wantLots: 5, 3423 wantValue: ethToGwei(50), 3424 wantMaxFees: 5 * 100 * tokenGases.Swap, 3425 wantBestCase: (baseFee + tip) * tokenGases.Swap, 3426 wantWorstCase: 5 * (baseFee + tip) * tokenGases.Swap, 3427 wantLocked: ethToGwei(50) + (5 * 100 * tokenGases.Swap), 3428 }, 3429 { 3430 name: "balanceError", 3431 bal: 51, 3432 lotSize: 10, 3433 feeSuggestion: 90, 3434 maxFeeRate: 100, 3435 balErr: errors.New("test error"), 3436 wantErr: true, 3437 }, 3438 } 3439 3440 runTest := func(t *testing.T, test testData) { 3441 var assetID uint32 = BipID 3442 assetCfg := tETH 3443 if test.token { 3444 assetID = usdcTokenID 3445 assetCfg = tToken 3446 } 3447 3448 w, _, node, shutdown := tassetWallet(assetID) 3449 defer shutdown() 3450 node.baseFee, node.tip = dexeth.GweiToWei(baseFee), dexeth.GweiToWei(tip) 3451 3452 if test.token { 3453 node.tContractor.gasEstimates = &tokenGases 3454 node.tokenContractor.bal = dexeth.GweiToWei(ethToGwei(test.bal)) 3455 node.bal = dexeth.GweiToWei(ethToGwei(test.parentBal)) 3456 } else { 3457 node.bal = dexeth.GweiToWei(ethToGwei(test.bal)) 3458 } 3459 3460 node.balErr = test.balErr 3461 3462 maxOrder, err := w.MaxOrder(&asset.MaxOrderForm{ 3463 LotSize: ethToGwei(test.lotSize), 3464 FeeSuggestion: test.feeSuggestion, // ignored 3465 AssetVersion: assetCfg.Version, 3466 MaxFeeRate: test.maxFeeRate, 3467 RedeemVersion: tBTC.Version, 3468 RedeemAssetID: tBTC.ID, 3469 }) 3470 if test.wantErr { 3471 if err == nil { 3472 t.Fatalf("expected error") 3473 } 3474 return 3475 } 3476 if err != nil { 3477 t.Fatalf("unexpected error: %v", err) 3478 } 3479 3480 if maxOrder.Lots != test.wantLots { 3481 t.Fatalf("want lots %v got %v", test.wantLots, maxOrder.Lots) 3482 } 3483 if maxOrder.Value != test.wantValue { 3484 t.Fatalf("want value %v got %v", test.wantValue, maxOrder.Value) 3485 } 3486 if maxOrder.MaxFees != test.wantMaxFees { 3487 t.Fatalf("want maxFees %v got %v", test.wantMaxFees, maxOrder.MaxFees) 3488 } 3489 if maxOrder.RealisticBestCase != test.wantBestCase { 3490 t.Fatalf("want best case %v got %v", test.wantBestCase, maxOrder.RealisticBestCase) 3491 } 3492 if maxOrder.RealisticWorstCase != test.wantWorstCase { 3493 t.Fatalf("want worst case %v got %v", test.wantWorstCase, maxOrder.RealisticWorstCase) 3494 } 3495 } 3496 3497 for _, test := range tests { 3498 t.Run(test.name, func(t *testing.T) { 3499 runTest(t, test) 3500 }) 3501 } 3502 } 3503 3504 func overMaxWei() *big.Int { 3505 maxInt := ^uint64(0) 3506 maxWei := new(big.Int).SetUint64(maxInt) 3507 gweiFactorBig := big.NewInt(dexeth.GweiFactor) 3508 maxWei.Mul(maxWei, gweiFactorBig) 3509 overMaxWei := new(big.Int).Set(maxWei) 3510 return overMaxWei.Add(overMaxWei, gweiFactorBig) 3511 } 3512 3513 func packInitiateDataV0(initiations []*dexeth.Initiation) ([]byte, error) { 3514 abiInitiations := make([]swapv0.ETHSwapInitiation, 0, len(initiations)) 3515 for _, init := range initiations { 3516 bigVal := new(big.Int).Set(init.Value) 3517 abiInitiations = append(abiInitiations, swapv0.ETHSwapInitiation{ 3518 RefundTimestamp: big.NewInt(init.LockTime.Unix()), 3519 SecretHash: init.SecretHash, 3520 Participant: init.Participant, 3521 Value: new(big.Int).Mul(bigVal, big.NewInt(dexeth.GweiFactor)), 3522 }) 3523 } 3524 return (*dexeth.ABIs[0]).Pack("initiate", abiInitiations) 3525 } 3526 3527 func packRedeemDataV0(redemptions []*dexeth.Redemption) ([]byte, error) { 3528 abiRedemptions := make([]swapv0.ETHSwapRedemption, 0, len(redemptions)) 3529 for _, redeem := range redemptions { 3530 abiRedemptions = append(abiRedemptions, swapv0.ETHSwapRedemption{ 3531 Secret: redeem.Secret, 3532 SecretHash: redeem.SecretHash, 3533 }) 3534 } 3535 return (*dexeth.ABIs[0]).Pack("redeem", abiRedemptions) 3536 } 3537 3538 func TestAuditContract(t *testing.T) { 3539 t.Run("eth", func(t *testing.T) { testAuditContract(t, BipID) }) 3540 t.Run("token", func(t *testing.T) { testAuditContract(t, usdcTokenID) }) 3541 } 3542 3543 func testAuditContract(t *testing.T, assetID uint32) { 3544 _, eth, _, shutdown := tassetWallet(assetID) 3545 defer shutdown() 3546 3547 numSecretHashes := 3 3548 secretHashes := make([][32]byte, 0, numSecretHashes) 3549 for i := 0; i < numSecretHashes; i++ { 3550 var secretHash [32]byte 3551 copy(secretHash[:], encode.RandomBytes(32)) 3552 secretHashes = append(secretHashes, secretHash) 3553 } 3554 3555 now := time.Now() 3556 laterThanNow := now.Add(time.Hour) 3557 3558 tests := []struct { 3559 name string 3560 contract dex.Bytes 3561 initiations []*dexeth.Initiation 3562 differentHash bool 3563 badTxData bool 3564 badTxBinary bool 3565 wantErr bool 3566 wantRecipient string 3567 wantExpiration time.Time 3568 }{ 3569 { 3570 name: "ok", 3571 contract: dexeth.EncodeContractData(0, secretHashes[1]), 3572 initiations: []*dexeth.Initiation{ 3573 { 3574 LockTime: now, 3575 SecretHash: secretHashes[0], 3576 Participant: testAddressA, 3577 Value: dexeth.GweiToWei(1), 3578 }, 3579 { 3580 LockTime: laterThanNow, 3581 SecretHash: secretHashes[1], 3582 Participant: testAddressB, 3583 Value: dexeth.GweiToWei(1), 3584 }, 3585 }, 3586 wantRecipient: testAddressB.Hex(), 3587 wantExpiration: laterThanNow, 3588 }, 3589 { 3590 name: "coin id different than tx hash", 3591 contract: dexeth.EncodeContractData(0, secretHashes[0]), 3592 initiations: []*dexeth.Initiation{ 3593 { 3594 LockTime: now, 3595 SecretHash: secretHashes[0], 3596 Participant: testAddressA, 3597 Value: dexeth.GweiToWei(1), 3598 }, 3599 }, 3600 differentHash: true, 3601 wantErr: true, 3602 }, 3603 { 3604 name: "contract is invalid versioned bytes", 3605 contract: []byte{}, 3606 wantErr: true, 3607 }, 3608 { 3609 name: "contract not part of transaction", 3610 contract: dexeth.EncodeContractData(0, secretHashes[2]), 3611 initiations: []*dexeth.Initiation{ 3612 { 3613 LockTime: now, 3614 SecretHash: secretHashes[0], 3615 Participant: testAddressA, 3616 Value: dexeth.GweiToWei(1), 3617 }, 3618 { 3619 LockTime: laterThanNow, 3620 SecretHash: secretHashes[1], 3621 Participant: testAddressB, 3622 Value: dexeth.GweiToWei(1), 3623 }, 3624 }, 3625 wantErr: true, 3626 }, 3627 { 3628 name: "cannot parse tx data", 3629 contract: dexeth.EncodeContractData(0, secretHashes[2]), 3630 badTxData: true, 3631 wantErr: true, 3632 }, 3633 { 3634 name: "cannot unmarshal tx binary", 3635 contract: dexeth.EncodeContractData(0, secretHashes[1]), 3636 initiations: []*dexeth.Initiation{ 3637 { 3638 LockTime: now, 3639 SecretHash: secretHashes[0], 3640 Participant: testAddressA, 3641 Value: dexeth.GweiToWei(1), 3642 }, 3643 { 3644 LockTime: laterThanNow, 3645 SecretHash: secretHashes[1], 3646 Participant: testAddressB, 3647 Value: dexeth.GweiToWei(1), 3648 }, 3649 }, 3650 badTxBinary: true, 3651 wantErr: true, 3652 }, 3653 } 3654 3655 for _, test := range tests { 3656 txData, err := packInitiateDataV0(test.initiations) 3657 if err != nil { 3658 t.Fatalf("unexpected error: %v", err) 3659 } 3660 if test.badTxData { 3661 txData = []byte{0} 3662 } 3663 3664 tx := tTx(2, 300, uint64(len(test.initiations)), &testAddressC, txData, 21000) 3665 txBinary, err := tx.MarshalBinary() 3666 if err != nil { 3667 t.Fatalf(`"%v": failed to marshal binary: %v`, test.name, err) 3668 } 3669 if test.badTxBinary { 3670 txBinary = []byte{0} 3671 } 3672 3673 txHash := tx.Hash() 3674 if test.differentHash { 3675 copy(txHash[:], encode.RandomBytes(20)) 3676 } 3677 3678 auditInfo, err := eth.AuditContract(txHash[:], test.contract, txBinary, true) 3679 if test.wantErr { 3680 if err == nil { 3681 t.Fatalf(`"%v": expected error but did not get`, test.name) 3682 } 3683 continue 3684 } 3685 if err != nil { 3686 t.Fatalf(`"%v": unexpected error: %v`, test.name, err) 3687 } 3688 3689 if test.wantRecipient != auditInfo.Recipient { 3690 t.Fatalf(`"%v": expected recipient %v != actual %v`, test.name, test.wantRecipient, auditInfo.Recipient) 3691 } 3692 if test.wantExpiration.Unix() != auditInfo.Expiration.Unix() { 3693 t.Fatalf(`"%v": expected expiration %v != actual %v`, test.name, test.wantExpiration, auditInfo.Expiration) 3694 } 3695 if !bytes.Equal(txHash[:], auditInfo.Coin.ID()) { 3696 t.Fatalf(`"%v": tx hash %x != coin id %x`, test.name, txHash, auditInfo.Coin.ID()) 3697 } 3698 if !bytes.Equal(test.contract, auditInfo.Contract) { 3699 t.Fatalf(`"%v": expected contract %x != actual %x`, test.name, test.contract, auditInfo.Contract) 3700 } 3701 3702 _, expectedSecretHash, err := dexeth.DecodeContractData(test.contract) 3703 if err != nil { 3704 t.Fatalf(`"%v": failed to decode versioned bytes: %v`, test.name, err) 3705 } 3706 if !bytes.Equal(expectedSecretHash[:], auditInfo.SecretHash) { 3707 t.Fatalf(`"%v": expected secret hash %x != actual %x`, test.name, expectedSecretHash, auditInfo.SecretHash) 3708 } 3709 } 3710 } 3711 3712 func TestOwnsAddress(t *testing.T) { 3713 address := "0b84C791b79Ee37De042AD2ffF1A253c3ce9bc27" // no "0x" prefix 3714 if !common.IsHexAddress(address) { 3715 t.Fatalf("bad test address") 3716 } 3717 3718 var otherAddress common.Address 3719 rand.Read(otherAddress[:]) 3720 3721 eth := &baseWallet{ 3722 addr: common.HexToAddress(address), 3723 finalizeConfs: txConfsNeededToConfirm, 3724 } 3725 3726 tests := []struct { 3727 name string 3728 address string 3729 wantOwns bool 3730 wantErr bool 3731 }{ 3732 { 3733 name: "same (exact)", 3734 address: address, 3735 wantOwns: true, 3736 wantErr: false, 3737 }, 3738 { 3739 name: "same (lower)", 3740 address: strings.ToLower(address), 3741 wantOwns: true, 3742 wantErr: false, 3743 }, 3744 { 3745 name: "same (upper)", 3746 address: strings.ToUpper(address), 3747 wantOwns: true, 3748 wantErr: false, 3749 }, 3750 { 3751 name: "same (0x prefix)", 3752 address: "0x" + address, 3753 wantOwns: true, 3754 wantErr: false, 3755 }, 3756 { 3757 name: "different (valid canonical)", 3758 address: otherAddress.String(), 3759 wantOwns: false, 3760 wantErr: false, 3761 }, 3762 { 3763 name: "different (valid hex)", 3764 address: otherAddress.Hex(), 3765 wantOwns: false, 3766 wantErr: false, 3767 }, 3768 { 3769 name: "error (bad hex char)", 3770 address: strings.Replace(address, "b", "r", 1), 3771 wantOwns: false, 3772 wantErr: true, 3773 }, 3774 { 3775 name: "error (bad length)", 3776 address: "ababababababab", 3777 wantOwns: false, 3778 wantErr: true, 3779 }, 3780 } 3781 3782 for _, tt := range tests { 3783 t.Run(tt.name, func(t *testing.T) { 3784 owns, err := eth.OwnsDepositAddress(tt.address) 3785 if (err == nil) && tt.wantErr { 3786 t.Error("expected error") 3787 } 3788 if (err != nil) && !tt.wantErr { 3789 t.Errorf("unexpected error: %v", err) 3790 } 3791 if owns != tt.wantOwns { 3792 t.Errorf("got %v, want %v", owns, tt.wantOwns) 3793 } 3794 }) 3795 } 3796 } 3797 3798 func TestSignMessage(t *testing.T) { 3799 ctx, cancel := context.WithCancel(context.Background()) 3800 defer cancel() 3801 3802 node := newTestNode(BipID) 3803 eth := &assetWallet{ 3804 baseWallet: &baseWallet{ 3805 node: node, 3806 addr: node.address(), 3807 ctx: ctx, 3808 log: tLogger, 3809 finalizeConfs: txConfsNeededToConfirm, 3810 }, 3811 assetID: BipID, 3812 } 3813 3814 msg := []byte("msg") 3815 3816 // SignData error 3817 node.signDataErr = errors.New("test error") 3818 _, _, err := eth.SignMessage(nil, msg) 3819 if err == nil { 3820 t.Fatalf("expected error due to error in rpcclient signData") 3821 } 3822 node.signDataErr = nil 3823 3824 // Test no error 3825 pubKeys, sigs, err := eth.SignMessage(nil, msg) 3826 if err != nil { 3827 t.Fatalf("unexpected error signing message: %v", err) 3828 } 3829 if len(pubKeys) != 1 { 3830 t.Fatalf("expected 1 pubKey but got %v", len(pubKeys)) 3831 } 3832 if len(sigs) != 1 { 3833 t.Fatalf("expected 1 signature but got %v", len(sigs)) 3834 } 3835 if !crypto.VerifySignature(pubKeys[0], crypto.Keccak256(msg), sigs[0][:len(sigs[0])-1]) { 3836 t.Fatalf("failed to verify signature") 3837 } 3838 } 3839 3840 func TestSwapConfirmation(t *testing.T) { 3841 _, eth, node, shutdown := tassetWallet(BipID) 3842 defer shutdown() 3843 3844 var secretHash [32]byte 3845 copy(secretHash[:], encode.RandomBytes(32)) 3846 state := &dexeth.SwapState{} 3847 hdr := &types.Header{} 3848 3849 node.tContractor.swapMap[secretHash] = state 3850 3851 state.BlockHeight = 5 3852 state.State = dexeth.SSInitiated 3853 hdr.Number = big.NewInt(6) 3854 eth.currentTip = hdr 3855 3856 ver := uint32(0) 3857 3858 ctx, cancel := context.WithCancel(context.Background()) 3859 defer cancel() 3860 3861 checkResult := func(expErr bool, expConfs uint32, expSpent bool) { 3862 t.Helper() 3863 confs, spent, err := eth.SwapConfirmations(ctx, nil, dexeth.EncodeContractData(ver, secretHash), time.Time{}) 3864 if err != nil { 3865 if expErr { 3866 return 3867 } 3868 t.Fatalf("SwapConfirmations error: %v", err) 3869 } 3870 if confs != expConfs { 3871 t.Fatalf("expected %d confs, got %d", expConfs, confs) 3872 } 3873 if spent != expSpent { 3874 t.Fatalf("wrong spent. wanted %t, got %t", expSpent, spent) 3875 } 3876 } 3877 3878 checkResult(false, 2, false) 3879 3880 // unknown asset version 3881 ver = 12 3882 checkResult(true, 0, false) 3883 ver = 0 3884 3885 // swap error 3886 node.tContractor.swapErr = fmt.Errorf("test error") 3887 checkResult(true, 0, false) 3888 node.tContractor.swapErr = nil 3889 3890 // ErrSwapNotInitiated 3891 state.State = dexeth.SSNone 3892 _, _, err := eth.SwapConfirmations(ctx, nil, dexeth.EncodeContractData(0, secretHash), time.Time{}) 3893 if !errors.Is(err, asset.ErrSwapNotInitiated) { 3894 t.Fatalf("expected ErrSwapNotInitiated, got %v", err) 3895 } 3896 3897 // 1 conf, spent 3898 state.BlockHeight = 6 3899 state.State = dexeth.SSRedeemed 3900 checkResult(false, 1, true) 3901 } 3902 3903 func TestDriverOpen(t *testing.T) { 3904 drv := &Driver{} 3905 logger := dex.StdOutLogger("ETHTEST", dex.LevelOff) 3906 tmpDir := t.TempDir() 3907 3908 settings := map[string]string{providersKey: "a.ipc"} 3909 err := CreateEVMWallet(dexeth.ChainIDs[dex.Testnet], &asset.CreateWalletParams{ 3910 Type: walletTypeRPC, 3911 Seed: encode.RandomBytes(32), 3912 Pass: encode.RandomBytes(32), 3913 Settings: settings, 3914 DataDir: tmpDir, 3915 Net: dex.Testnet, 3916 Logger: logger, 3917 }, &testnetCompatibilityData, true) 3918 if err != nil { 3919 t.Fatalf("CreateWallet error: %v", err) 3920 } 3921 3922 // Make sure default gas fee limit is used when nothing is set 3923 cfg := &asset.WalletConfig{ 3924 Type: walletTypeRPC, 3925 Settings: settings, 3926 DataDir: tmpDir, 3927 } 3928 wallet, err := drv.Open(cfg, logger, dex.Testnet) 3929 if err != nil { 3930 t.Fatalf("driver open error: %v", err) 3931 } 3932 eth, ok := wallet.(*ETHWallet) 3933 if !ok { 3934 t.Fatalf("failed to cast wallet as assetWallet") 3935 } 3936 if eth.gasFeeLimit() != defaultGasFeeLimit { 3937 t.Fatalf("expected gasFeeLimit to be default, but got %v", eth.gasFeeLimit()) 3938 } 3939 3940 // Make sure gas fee limit is properly parsed from settings 3941 cfg.Settings["gasfeelimit"] = "150" 3942 wallet, err = drv.Open(cfg, logger, dex.Testnet) 3943 if err != nil { 3944 t.Fatalf("driver open error: %v", err) 3945 } 3946 eth, ok = wallet.(*ETHWallet) 3947 if !ok { 3948 t.Fatalf("failed to cast wallet as assetWallet") 3949 } 3950 if eth.gasFeeLimit() != 150 { 3951 t.Fatalf("expected gasFeeLimit to be 150, but got %v", eth.gasFeeLimit()) 3952 } 3953 } 3954 3955 func TestDriverExists(t *testing.T) { 3956 drv := &Driver{} 3957 tmpDir := t.TempDir() 3958 3959 settings := map[string]string{providersKey: "a.ipc"} 3960 3961 // no wallet 3962 exists, err := drv.Exists(walletTypeRPC, tmpDir, settings, dex.Simnet) 3963 if err != nil { 3964 t.Fatalf("Exists error for no geth wallet: %v", err) 3965 } 3966 if exists { 3967 t.Fatalf("Uninitiated wallet exists") 3968 } 3969 3970 // Create the wallet. 3971 err = CreateEVMWallet(dexeth.ChainIDs[dex.Simnet], &asset.CreateWalletParams{ 3972 Type: walletTypeRPC, 3973 Seed: encode.RandomBytes(32), 3974 Pass: encode.RandomBytes(32), 3975 Settings: settings, 3976 DataDir: tmpDir, 3977 Net: dex.Simnet, 3978 Logger: tLogger, 3979 }, &testnetCompatibilityData, true) 3980 if err != nil { 3981 t.Fatalf("CreateEVMWallet error: %v", err) 3982 } 3983 3984 // exists 3985 exists, err = drv.Exists(walletTypeRPC, tmpDir, settings, dex.Simnet) 3986 if err != nil { 3987 t.Fatalf("Exists error for existent geth wallet: %v", err) 3988 } 3989 if !exists { 3990 t.Fatalf("Initiated wallet doesn't exist") 3991 } 3992 3993 // Wrong wallet type 3994 if _, err := drv.Exists("not-geth", tmpDir, settings, dex.Simnet); err == nil { 3995 t.Fatalf("no error for unknown wallet type") 3996 } 3997 } 3998 3999 func TestDriverDecodeCoinID(t *testing.T) { 4000 drv := &Driver{} 4001 addressStr := "0xB6De8BB5ed28E6bE6d671975cad20C03931bE981" 4002 address := common.HexToAddress(addressStr) 4003 4004 // Test tx hash 4005 txHash := encode.RandomBytes(common.HashLength) 4006 coinID, err := drv.DecodeCoinID(txHash) 4007 if err != nil { 4008 t.Fatalf("error decoding coin id: %v", err) 4009 } 4010 var hash common.Hash 4011 hash.SetBytes(txHash) 4012 if coinID != hash.String() { 4013 t.Fatalf("expected coin id to be %s but got %s", hash.String(), coinID) 4014 } 4015 4016 // Test funding coin id 4017 fundingCoin := createFundingCoin(address, 1000) 4018 coinID, err = drv.DecodeCoinID(fundingCoin.RecoveryID()) 4019 if err != nil { 4020 t.Fatalf("error decoding coin id: %v", err) 4021 } 4022 if coinID != fundingCoin.String() { 4023 t.Fatalf("expected coin id to be %s but got %s", fundingCoin.String(), coinID) 4024 } 4025 4026 // Token funding coin 4027 fc := createTokenFundingCoin(address, 1000, 1) 4028 coinID, err = drv.DecodeCoinID(fc.RecoveryID()) 4029 if err != nil { 4030 t.Fatalf("error decoding token coin id: %v", err) 4031 } 4032 if coinID != fc.String() { 4033 t.Fatalf("expected coin id to be %s but got %s", fc.String(), coinID) 4034 } 4035 4036 // Test byte encoded address string 4037 coinID, err = drv.DecodeCoinID([]byte(addressStr)) 4038 if err != nil { 4039 t.Fatalf("error decoding coin id: %v", err) 4040 } 4041 if coinID != addressStr { 4042 t.Fatalf("expected coin id to be %s but got %s", addressStr, coinID) 4043 } 4044 4045 // Test invalid coin id 4046 _, err = drv.DecodeCoinID(encode.RandomBytes(20)) 4047 if err == nil { 4048 t.Fatal("expected error but did not get") 4049 } 4050 } 4051 4052 func TestLocktimeExpired(t *testing.T) { 4053 _, eth, node, shutdown := tassetWallet(BipID) 4054 defer shutdown() 4055 4056 var secretHash [32]byte 4057 copy(secretHash[:], encode.RandomBytes(32)) 4058 4059 state := &dexeth.SwapState{ 4060 LockTime: time.Now(), 4061 State: dexeth.SSInitiated, 4062 } 4063 4064 header := &types.Header{ 4065 Time: uint64(time.Now().Add(time.Second).Unix()), 4066 } 4067 4068 node.tContractor.swapMap[secretHash] = state 4069 node.bestHdr = header 4070 4071 contract := make([]byte, 36) 4072 copy(contract[4:], secretHash[:]) 4073 4074 ensureResult := func(tag string, expErr, expExpired bool) { 4075 t.Helper() 4076 expired, _, err := eth.ContractLockTimeExpired(context.Background(), contract) 4077 switch { 4078 case err != nil: 4079 if !expErr { 4080 t.Fatalf("%s: ContractLockTimeExpired error existing expired swap: %v", tag, err) 4081 } 4082 case expErr: 4083 t.Fatalf("%s: expected error, got none", tag) 4084 case expExpired != expired: 4085 t.Fatalf("%s: expired wrong. %t != %t", tag, expired, expExpired) 4086 } 4087 } 4088 4089 // locktime expired 4090 ensureResult("locktime expired", false, true) 4091 4092 // header error 4093 node.bestHdrErr = errors.New("test error") 4094 ensureResult("header error", true, false) 4095 node.bestHdrErr = nil 4096 4097 // swap not initiated 4098 saveState := state.State 4099 state.State = dexeth.SSNone 4100 ensureResult("swap not initiated", true, false) 4101 state.State = saveState 4102 4103 // missing swap 4104 delete(node.tContractor.swapMap, secretHash) 4105 ensureResult("missing swap", true, false) 4106 node.tContractor.swapMap[secretHash] = state 4107 4108 // lock time not expired 4109 state.LockTime = time.Now().Add(time.Minute) 4110 ensureResult("lock time not expired", false, false) 4111 4112 // wrong contract version 4113 contract[3] = 1 4114 ensureResult("wrong contract version", true, false) 4115 contract[3] = 0 4116 4117 // bad contract 4118 contract = append(contract, 0) // nolint:makezero 4119 ensureResult("bad contract", true, false) 4120 } 4121 4122 func TestFindRedemption(t *testing.T) { 4123 t.Run("eth", func(t *testing.T) { testFindRedemption(t, BipID) }) 4124 t.Run("token", func(t *testing.T) { testFindRedemption(t, usdcTokenID) }) 4125 } 4126 4127 func testFindRedemption(t *testing.T, assetID uint32) { 4128 _, eth, node, shutdown := tassetWallet(assetID) 4129 defer shutdown() 4130 4131 var secret [32]byte 4132 copy(secret[:], encode.RandomBytes(32)) 4133 secretHash := sha256.Sum256(secret[:]) 4134 4135 contract := dexeth.EncodeContractData(0, secretHash) 4136 state := &dexeth.SwapState{ 4137 Secret: secret, 4138 State: dexeth.SSInitiated, 4139 } 4140 4141 node.tContractor.swapMap[secretHash] = state 4142 4143 baseCtx := context.Background() 4144 4145 runTest := func(tag string, wantErr bool, initStep dexeth.SwapStep) { 4146 // The queue should always be empty. 4147 eth.findRedemptionMtx.RLock() 4148 reqsPending := len(eth.findRedemptionReqs) > 0 4149 eth.findRedemptionMtx.RUnlock() 4150 if reqsPending { 4151 t.Fatalf("%s: requests pending at beginning of test", tag) 4152 } 4153 4154 state.State = initStep 4155 var err error 4156 ctx, cancel := context.WithTimeout(baseCtx, time.Second) 4157 defer cancel() 4158 _, secretB, err := eth.FindRedemption(ctx, nil, contract) 4159 if err != nil { 4160 if wantErr { 4161 return 4162 } 4163 t.Fatalf("%s: %v", tag, err) 4164 } else if wantErr { 4165 t.Fatalf("%s: didn't see expected error", tag) 4166 } 4167 if !bytes.Equal(secretB, secret[:]) { 4168 t.Fatalf("%s: wrong secret. %x != %x", tag, []byte(secretB), secret) 4169 } 4170 } 4171 4172 runWithUpdate := func(tag string, wantErr bool, initStep dexeth.SwapStep, updateFunc func()) { 4173 var wg sync.WaitGroup 4174 wg.Add(1) 4175 go func() { 4176 defer wg.Done() 4177 timeout := time.After(time.Second) 4178 for { 4179 select { 4180 case <-time.After(time.Millisecond): 4181 eth.findRedemptionMtx.RLock() 4182 pending := eth.findRedemptionReqs[secretHash] != nil 4183 eth.findRedemptionMtx.RUnlock() 4184 if !pending { 4185 continue 4186 } 4187 updateFunc() 4188 eth.checkFindRedemptions() 4189 case <-timeout: 4190 return 4191 } 4192 } 4193 }() 4194 runTest(tag, wantErr, initStep) 4195 wg.Wait() 4196 } 4197 4198 // Already redeemed. 4199 runTest("already redeemed", false, dexeth.SSRedeemed) 4200 4201 // Redeemed after queuing 4202 runWithUpdate("redeemed after queuing", false, dexeth.SSInitiated, func() { 4203 state.State = dexeth.SSRedeemed 4204 }) 4205 4206 // Doesn't exist 4207 runTest("already refunded", true, dexeth.SSNone) 4208 4209 // Unknown swap state 4210 runTest("already refunded", true, dexeth.SwapStep(^uint8(0))) 4211 4212 // Already refunded 4213 runTest("already refunded", true, dexeth.SSRefunded) 4214 4215 // Refunded after queuing 4216 runWithUpdate("refunded after queuing", true, dexeth.SSInitiated, func() { 4217 state.State = dexeth.SSRefunded 4218 }) 4219 4220 // swap error 4221 node.tContractor.swapErr = errors.New("test error") 4222 runTest("swap error", true, 0) 4223 node.tContractor.swapErr = nil 4224 4225 // swap error after queuing 4226 runWithUpdate("swap error after queuing", true, dexeth.SSInitiated, func() { 4227 node.tContractor.swapErr = errors.New("test error") 4228 }) 4229 node.tContractor.swapErr = nil 4230 4231 // cancelled context error 4232 var cancel context.CancelFunc 4233 baseCtx, cancel = context.WithCancel(context.Background()) 4234 cancel() 4235 runTest("context cancellation", true, dexeth.SSInitiated) 4236 baseCtx = context.Background() 4237 4238 // bad contract 4239 goodContract := contract 4240 contract = append(contract, 0) 4241 runTest("bad contract", true, dexeth.SSInitiated) 4242 contract = goodContract 4243 4244 // dupe 4245 eth.findRedemptionMtx.Lock() 4246 eth.findRedemptionReqs[secretHash] = &findRedemptionRequest{} 4247 eth.findRedemptionMtx.Unlock() 4248 res := make(chan error, 1) 4249 go func() { 4250 _, _, err := eth.FindRedemption(baseCtx, nil, contract) 4251 res <- err 4252 }() 4253 4254 select { 4255 case err := <-res: 4256 if err == nil { 4257 t.Fatalf("no error for dupe") 4258 } 4259 case <-time.After(time.Second): 4260 t.Fatalf("timed out on dupe test") 4261 } 4262 } 4263 4264 func TestRefundReserves(t *testing.T) { 4265 t.Run("eth", func(t *testing.T) { testRefundReserves(t, BipID) }) 4266 t.Run("token", func(t *testing.T) { testRefundReserves(t, usdcTokenID) }) 4267 } 4268 4269 func testRefundReserves(t *testing.T, assetID uint32) { 4270 wi, eth, node, shutdown := tassetWallet(assetID) 4271 defer shutdown() 4272 4273 w := wi.(asset.AccountLocker) 4274 4275 node.bal = dexeth.GweiToWei(1e9) 4276 node.refundable = true 4277 node.swapVers = map[uint32]struct{}{0: {}} 4278 4279 var secretHash [32]byte 4280 node.swapMap = map[[32]byte]*dexeth.SwapState{secretHash: {}} 4281 4282 feeWallet := eth 4283 gasesV0 := dexeth.VersionedGases[0] 4284 gasesV1 := &dexeth.Gases{Refund: 1e6} 4285 assetV0 := *tETH 4286 4287 assetV1 := *tETH 4288 if assetID == BipID { 4289 eth.versionedGases[1] = gasesV1 4290 } else { 4291 feeWallet = node.tokenParent 4292 assetV0 = *tToken 4293 assetV1 = *tToken 4294 tokenContracts := eth.tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts 4295 tc := *tokenContracts[0] 4296 tc.Gas = *gasesV1 4297 tokenContracts[1] = &tc 4298 defer delete(tokenContracts, 1) 4299 gasesV0 = &tokenGases 4300 eth.versionedGases[0] = gasesV0 4301 eth.versionedGases[1] = gasesV1 4302 node.tokenContractor.bal = dexeth.GweiToWei(1e9) 4303 } 4304 4305 assetV0.MaxFeeRate = 45 4306 assetV1.Version = 1 4307 assetV1.MaxFeeRate = 50 4308 4309 // Lock for 3 refunds with contract version 0 4310 v0Val, err := w.ReserveNRefunds(3, assetV0.Version, assetV0.MaxFeeRate) 4311 if err != nil { 4312 t.Fatalf("ReserveNRefunds error: %v", err) 4313 } 4314 lockPerV0 := gasesV0.Refund * assetV0.MaxFeeRate 4315 4316 expLock := 3 * lockPerV0 4317 if feeWallet.lockedFunds.refundReserves != expLock { 4318 t.Fatalf("wrong v0 locked. wanted %d, got %d", expLock, feeWallet.lockedFunds.refundReserves) 4319 } 4320 if v0Val != expLock { 4321 t.Fatalf("expected locked %d, got %d", expLock, v0Val) 4322 } 4323 4324 // Lock for 2 refunds with contract version 1 4325 v1Val, err := w.ReserveNRefunds(2, assetV1.Version, assetV1.MaxFeeRate) 4326 if err != nil { 4327 t.Fatalf("ReserveNRefunds error: %v", err) 4328 } 4329 lockPerV1 := gasesV1.Refund * assetV1.MaxFeeRate 4330 v1Lock := 2 * lockPerV1 4331 expLock += v1Lock 4332 if feeWallet.lockedFunds.refundReserves != expLock { 4333 t.Fatalf("wrong v1 locked. wanted %d, got %d", expLock, feeWallet.lockedFunds.refundReserves) 4334 } 4335 if v1Val != v1Lock { 4336 t.Fatalf("expected locked %d, got %d", v1Lock, v1Val) 4337 } 4338 4339 w.UnlockRefundReserves(9e5) 4340 expLock -= 9e5 4341 if feeWallet.lockedFunds.refundReserves != expLock { 4342 t.Fatalf("incorrect amount locked. wanted %d, got %d", expLock, feeWallet.lockedFunds.refundReserves) 4343 } 4344 4345 // Reserve more than available should return an error 4346 err = w.ReReserveRefund(1e9 + 1) 4347 if err == nil { 4348 t.Fatalf("expected an error but did not get") 4349 } 4350 4351 err = w.ReReserveRefund(5e6) 4352 if err != nil { 4353 t.Fatalf("unexpected error: %v", err) 4354 } 4355 expLock += 5e6 4356 if feeWallet.lockedFunds.refundReserves != expLock { 4357 t.Fatalf("incorrect amount locked. wanted %d, got %d", expLock, feeWallet.lockedFunds.refundReserves) 4358 } 4359 } 4360 4361 func TestRedemptionReserves(t *testing.T) { 4362 t.Run("eth", func(t *testing.T) { testRedemptionReserves(t, BipID) }) 4363 t.Run("token", func(t *testing.T) { testRedemptionReserves(t, usdcTokenID) }) 4364 } 4365 4366 func testRedemptionReserves(t *testing.T, assetID uint32) { 4367 wi, eth, node, shutdown := tassetWallet(assetID) 4368 defer shutdown() 4369 4370 w := wi.(asset.AccountLocker) 4371 4372 node.bal = dexeth.GweiToWei(1e9) 4373 // node.tContractor.swapMap = map[[32]byte]*dexeth.SwapState{ 4374 // secretHashes[0]: { 4375 // State: dexeth.SSInitiated, 4376 // }, 4377 // }, 4378 4379 var secretHash [32]byte 4380 node.tContractor.swapMap[secretHash] = &dexeth.SwapState{} 4381 4382 gasesV1 := &dexeth.Gases{Redeem: 1e6, RedeemAdd: 85e5} 4383 gasesV0 := dexeth.VersionedGases[0] 4384 assetV0 := *tETH 4385 assetV1 := *tETH 4386 feeWallet := eth 4387 if assetID == BipID { 4388 eth.versionedGases[1] = gasesV1 4389 } else { 4390 node.tokenContractor.allow = unlimitedAllowanceReplenishThreshold 4391 feeWallet = node.tokenParent 4392 assetV0 = *tToken 4393 assetV1 = *tToken 4394 gasesV0 = &tokenGases 4395 eth.versionedGases[0] = gasesV0 4396 eth.versionedGases[1] = gasesV1 4397 } 4398 4399 assetV0.MaxFeeRate = 45 4400 assetV1.Version = 1 4401 assetV1.MaxFeeRate = 50 4402 4403 v0Val, err := w.ReserveNRedemptions(3, assetV0.Version, assetV0.MaxFeeRate) 4404 if err != nil { 4405 t.Fatalf("reservation error: %v", err) 4406 } 4407 4408 lockPerV0 := gasesV0.Redeem * assetV0.MaxFeeRate 4409 expLock := 3 * lockPerV0 4410 4411 if feeWallet.lockedFunds.redemptionReserves != expLock { 4412 t.Fatalf("wrong v0 locked. wanted %d, got %d", expLock, feeWallet.lockedFunds.redemptionReserves) 4413 } 4414 4415 if v0Val != expLock { 4416 t.Fatalf("expected value %d, got %d", lockPerV0, v0Val) 4417 } 4418 4419 v1Val, err := w.ReserveNRedemptions(2, assetV1.Version, assetV1.MaxFeeRate) 4420 if err != nil { 4421 t.Fatalf("reservation error: %v", err) 4422 } 4423 4424 lockPerV1 := gasesV1.Redeem * assetV1.MaxFeeRate 4425 v1Lock := 2 * lockPerV1 4426 if v1Val != v1Lock { 4427 t.Fatal("wrong reserved val", v1Val, v1Lock) 4428 } 4429 4430 expLock += v1Lock 4431 if feeWallet.lockedFunds.redemptionReserves != expLock { 4432 t.Fatalf("wrong v1 locked. wanted %d, got %d", expLock, feeWallet.lockedFunds.redemptionReserves) 4433 } 4434 4435 // Run some token tests 4436 if assetID == BipID { 4437 return 4438 } 4439 } 4440 4441 func ethToGwei(v uint64) uint64 { 4442 return v * dexeth.GweiFactor 4443 } 4444 4445 func ethToWei(v uint64) *big.Int { 4446 bigV := new(big.Int).SetUint64(ethToGwei(v)) 4447 return new(big.Int).Mul(bigV, big.NewInt(dexeth.GweiFactor)) 4448 } 4449 4450 func TestReconfigure(t *testing.T) { 4451 w, eth, _, shutdown := tassetWallet(BipID) 4452 defer shutdown() 4453 4454 reconfigurer, is := w.(asset.LiveReconfigurer) 4455 if !is { 4456 t.Fatal("wallet is not a reconfigurer") 4457 } 4458 4459 ethCfg := &WalletConfig{ 4460 GasFeeLimit: 123, 4461 } 4462 4463 settings, err := config.Mapify(ethCfg) 4464 if err != nil { 4465 t.Fatal("failed to mapify") 4466 } 4467 4468 walletCfg := &asset.WalletConfig{ 4469 Type: walletTypeRPC, 4470 Settings: settings, 4471 } 4472 4473 restart, err := reconfigurer.Reconfigure(context.Background(), walletCfg, "") 4474 if err != nil { 4475 t.Fatalf("unexpected error: %v", err) 4476 } 4477 if restart { 4478 t.Fatalf("unexpected restart") 4479 } 4480 4481 if eth.baseWallet.gasFeeLimit() != ethCfg.GasFeeLimit { 4482 t.Fatal("gas fee limit was not updated properly") 4483 } 4484 } 4485 4486 func TestSend(t *testing.T) { 4487 t.Run("eth", func(t *testing.T) { testSend(t, BipID) }) 4488 t.Run("token", func(t *testing.T) { testSend(t, usdcTokenID) }) 4489 } 4490 4491 func testSend(t *testing.T, assetID uint32) { 4492 w, eth, node, shutdown := tassetWallet(assetID) 4493 defer shutdown() 4494 4495 tx := tTx(0, 0, 0, &testAddressA, nil, 21000) 4496 txHash := tx.Hash() 4497 4498 node.sendTxTx = tx 4499 node.tokenContractor.transferTx = tx 4500 4501 maxFeeRate, _, _ := eth.recommendedMaxFeeRate(eth.ctx) 4502 ethFees := dexeth.WeiToGwei(maxFeeRate) * defaultSendGasLimit 4503 tokenFees := dexeth.WeiToGwei(maxFeeRate) * tokenGases.Transfer 4504 4505 const val = 10e9 4506 const testAddr = "dd93b447f7eBCA361805eBe056259853F3912E04" 4507 tests := []struct { 4508 name string 4509 sendAdj, feeAdj uint64 4510 balErr, sendTxErr error 4511 addr string 4512 wantErr bool 4513 }{{ 4514 name: "ok", 4515 addr: testAddr, 4516 }, { 4517 name: "balance error", 4518 balErr: errors.New("test error"), 4519 wantErr: true, 4520 addr: testAddr, 4521 }, { 4522 name: "not enough", 4523 sendAdj: 1, 4524 wantErr: true, 4525 addr: testAddr, 4526 }, { 4527 name: "low fees", 4528 feeAdj: 1, 4529 wantErr: true, 4530 addr: testAddr, 4531 }, { 4532 name: "sendToAddr error", 4533 sendTxErr: errors.New("test error"), 4534 wantErr: true, 4535 addr: testAddr, 4536 }, { 4537 name: "Invalid address", 4538 sendTxErr: errors.New("invalid hex address error"), 4539 wantErr: true, 4540 addr: "", 4541 }} 4542 4543 for _, test := range tests { 4544 node.setBalanceError(eth, test.balErr) 4545 node.sendTxErr = test.sendTxErr 4546 node.tokenContractor.transferErr = test.sendTxErr 4547 if assetID == BipID { 4548 node.bal = dexeth.GweiToWei(val + ethFees - test.sendAdj - test.feeAdj) 4549 } else { 4550 node.tokenContractor.bal = dexeth.GweiToWei(val - test.sendAdj) 4551 node.bal = dexeth.GweiToWei(tokenFees - test.feeAdj) 4552 } 4553 coin, err := w.Send(test.addr, val, 0) 4554 if test.wantErr { 4555 if err == nil { 4556 t.Fatalf("expected error for test %v", test.name) 4557 } 4558 continue 4559 } 4560 if err != nil { 4561 t.Fatalf("unexpected error for test %v: %v", test.name, err) 4562 } 4563 if !bytes.Equal(txHash[:], coin.ID()) { 4564 t.Fatal("coin is not the tx hash") 4565 } 4566 } 4567 } 4568 4569 func TestConfirmRedemption(t *testing.T) { 4570 t.Run("eth", func(t *testing.T) { testConfirmRedemption(t, BipID) }) 4571 t.Run("token", func(t *testing.T) { testConfirmRedemption(t, usdcTokenID) }) 4572 } 4573 4574 func testConfirmRedemption(t *testing.T, assetID uint32) { 4575 wi, eth, node, shutdown := tassetWallet(assetID) 4576 defer shutdown() 4577 4578 db := eth.txDB.(*tTxDB) 4579 4580 const tip = 12 4581 const confBlock = tip - txConfsNeededToConfirm + 1 4582 4583 var secret, secretHash [32]byte 4584 copy(secret[:], encode.RandomBytes(32)) 4585 copy(secretHash[:], encode.RandomBytes(32)) 4586 var txHash common.Hash 4587 copy(txHash[:], encode.RandomBytes(32)) 4588 4589 redemption := &asset.Redemption{ 4590 Spends: &asset.AuditInfo{ 4591 Contract: dexeth.EncodeContractData(0, secretHash), 4592 }, 4593 Secret: secret[:], 4594 } 4595 4596 pendingTx := &extendedWalletTx{ 4597 WalletTransaction: &asset.WalletTransaction{ 4598 ID: txHash.String(), 4599 BlockNumber: confBlock + 1, 4600 }, 4601 txHash: txHash, 4602 } 4603 dbTx := &extendedWalletTx{ 4604 WalletTransaction: &asset.WalletTransaction{ 4605 ID: txHash.String(), 4606 BlockNumber: confBlock, 4607 }, 4608 } 4609 4610 type test struct { 4611 name string 4612 expectedConfs uint64 4613 expectErr bool 4614 expectSwapRefundedErr bool 4615 expectRedemptionFailedErr bool 4616 pendingTx *extendedWalletTx 4617 dbTx *extendedWalletTx 4618 dbErr error 4619 step dexeth.SwapStep 4620 receipt *types.Receipt 4621 receiptErr error 4622 } 4623 4624 tests := []*test{ 4625 { 4626 name: "found on-chain. not yet confirmed", 4627 receipt: &types.Receipt{ 4628 Status: types.ReceiptStatusSuccessful, 4629 BlockNumber: big.NewInt(confBlock + 1), 4630 }, 4631 expectedConfs: txConfsNeededToConfirm - 1, 4632 }, 4633 { 4634 name: "found on-chain. confirmed", 4635 step: dexeth.SSRedeemed, 4636 expectedConfs: txConfsNeededToConfirm, 4637 receipt: &types.Receipt{ 4638 Status: types.ReceiptStatusSuccessful, 4639 BlockNumber: big.NewInt(confBlock), 4640 }, 4641 }, 4642 { 4643 name: "found in pending txs", 4644 step: dexeth.SSRedeemed, 4645 pendingTx: pendingTx, 4646 expectedConfs: txConfsNeededToConfirm - 1, 4647 }, 4648 { 4649 name: "found in db", 4650 step: dexeth.SSRedeemed, 4651 dbTx: dbTx, 4652 expectedConfs: txConfsNeededToConfirm, 4653 receipt: &types.Receipt{ 4654 Status: types.ReceiptStatusSuccessful, 4655 BlockNumber: big.NewInt(confBlock), 4656 }, 4657 }, 4658 { 4659 name: "db error not propagated. unconfirmed", 4660 step: dexeth.SSRedeemed, 4661 dbErr: errors.New("test error"), 4662 expectedConfs: txConfsNeededToConfirm - 1, 4663 receipt: &types.Receipt{ 4664 Status: types.ReceiptStatusSuccessful, 4665 BlockNumber: big.NewInt(confBlock + 1), 4666 }, 4667 }, 4668 { 4669 name: "found on-chain. tx failed", 4670 step: dexeth.SSInitiated, 4671 expectErr: true, 4672 expectRedemptionFailedErr: true, 4673 receipt: &types.Receipt{ 4674 Status: types.ReceiptStatusFailed, 4675 BlockNumber: big.NewInt(confBlock), 4676 }, 4677 }, 4678 { 4679 name: "found on-chain. redeemed by another unknown transaction", 4680 step: dexeth.SSRedeemed, 4681 receipt: &types.Receipt{ 4682 Status: types.ReceiptStatusFailed, 4683 BlockNumber: big.NewInt(confBlock), 4684 }, 4685 expectedConfs: txConfsNeededToConfirm, 4686 }, 4687 } 4688 4689 runTest := func(test *test) { 4690 fmt.Printf("###### %s ###### \n", test.name) 4691 4692 node.tContractor.swapMap = map[[32]byte]*dexeth.SwapState{ 4693 secretHash: {State: test.step}, 4694 } 4695 node.tContractor.lastRedeems = nil 4696 node.tokenContractor.bal = big.NewInt(1e9) 4697 node.bal = big.NewInt(1e9) 4698 4699 eth.pendingTxs = []*extendedWalletTx{} 4700 if test.pendingTx != nil { 4701 eth.pendingTxs = append(eth.pendingTxs, test.pendingTx) 4702 } 4703 4704 db.txToGet = test.dbTx 4705 db.getTxErr = test.dbErr 4706 4707 node.lastSignedTx = nil 4708 eth.currentTip = &types.Header{Number: big.NewInt(tip)} 4709 node.receipt = test.receipt 4710 node.receiptErr = test.receiptErr 4711 4712 result, err := wi.ConfirmRedemption(txHash[:], redemption, 0) 4713 if test.expectErr { 4714 if err == nil { 4715 t.Fatalf("%s: expected error but did not get", test.name) 4716 } 4717 if test.expectRedemptionFailedErr && !errors.Is(err, asset.ErrTxRejected) { 4718 t.Fatalf("%s: expected rejected tx error. got %v", test.name, err) 4719 } 4720 if test.expectSwapRefundedErr && !errors.Is(asset.ErrSwapRefunded, err) { 4721 t.Fatalf("%s: expected swap refunded error but got %v", test.name, err) 4722 } 4723 return 4724 } 4725 if err != nil { 4726 t.Fatalf("%s: unexpected error: %v", test.name, err) 4727 } 4728 4729 // Check that the resulting status is as expected 4730 if test.expectedConfs != result.Confs || 4731 txConfsNeededToConfirm != result.Req { 4732 t.Fatalf("%s: expected confs %d != result %d", test.name, test.expectedConfs, result.Confs) 4733 } 4734 } 4735 4736 for _, test := range tests { 4737 runTest(test) 4738 } 4739 } 4740 4741 // Ensures that a small rise in the base fee between estimation 4742 // and sending will not cause a failure. 4743 func TestEstimateVsActualSendFees(t *testing.T) { 4744 t.Run("eth", func(t *testing.T) { testEstimateVsActualSendFees(t, BipID) }) 4745 t.Run("token", func(t *testing.T) { testEstimateVsActualSendFees(t, usdcTokenID) }) 4746 } 4747 4748 func testEstimateVsActualSendFees(t *testing.T, assetID uint32) { 4749 w, _, node, shutdown := tassetWallet(assetID) 4750 defer shutdown() 4751 4752 tx := tTx(0, 0, 0, &testAddressA, nil, 21000) 4753 node.sendTxTx = tx 4754 node.tokenContractor.transferTx = tx 4755 4756 const testAddr = "dd93b447f7eBCA361805eBe056259853F3912E04" 4757 4758 txFeeEstimator := w.(asset.TxFeeEstimator) 4759 fee, _, err := txFeeEstimator.EstimateSendTxFee("", 0, 0, false, false) 4760 if err != nil { 4761 t.Fatalf("error estimating fee: %v", err) 4762 } 4763 4764 // Increase the base fee by 10%. 4765 node.baseFee = node.baseFee.Mul(node.baseFee, big.NewInt(11)) 4766 node.baseFee = node.baseFee.Div(node.baseFee, big.NewInt(10)) 4767 4768 if assetID == BipID { 4769 node.bal = dexeth.GweiToWei(11e9) 4770 canSend := new(big.Int).Sub(node.bal, dexeth.GweiToWei(fee)) 4771 canSendGwei, err := dexeth.WeiToGweiSafe(canSend) 4772 if err != nil { 4773 t.Fatalf("error converting canSend to gwei: %v", err) 4774 } 4775 _, err = w.Send(testAddr, canSendGwei, 0) 4776 if err != nil { 4777 t.Fatalf("error sending: %v", err) 4778 } 4779 } else { 4780 tokenVal := uint64(10e9) 4781 node.tokenContractor.bal = dexeth.GweiToWei(tokenVal) 4782 node.bal = dexeth.GweiToWei(fee) 4783 _, err = w.Send(testAddr, tokenVal, 0) 4784 if err != nil { 4785 t.Fatalf("error sending: %v", err) 4786 } 4787 } 4788 } 4789 4790 func TestEstimateSendTxFee(t *testing.T) { 4791 t.Run("eth", func(t *testing.T) { testEstimateSendTxFee(t, BipID) }) 4792 t.Run("token", func(t *testing.T) { testEstimateSendTxFee(t, usdcTokenID) }) 4793 } 4794 4795 func testEstimateSendTxFee(t *testing.T, assetID uint32) { 4796 w, eth, node, shutdown := tassetWallet(assetID) 4797 defer shutdown() 4798 4799 maxFeeRate, _, _ := eth.recommendedMaxFeeRate(eth.ctx) 4800 ethFees := dexeth.WeiToGwei(maxFeeRate) * defaultSendGasLimit 4801 tokenFees := dexeth.WeiToGwei(maxFeeRate) * tokenGases.Transfer 4802 4803 ethFees = ethFees * 12 / 10 4804 tokenFees = tokenFees * 12 / 10 4805 4806 const testAddr = "dd93b447f7eBCA361805eBe056259853F3912E04" 4807 4808 const val = 10e9 4809 tests := []struct { 4810 name, addr string 4811 sendAdj, feeAdj uint64 4812 balErr error 4813 withdraw bool 4814 wantErr bool 4815 }{{ 4816 name: "ok", 4817 addr: testAddr, 4818 }, { 4819 name: "ok: empty address", 4820 addr: "", 4821 }, { 4822 name: "not enough", 4823 sendAdj: 1, 4824 wantErr: true, 4825 addr: testAddr, 4826 }, { 4827 name: "low fees", 4828 feeAdj: 1, 4829 wantErr: true, 4830 addr: testAddr, 4831 }, { 4832 name: "subtract", 4833 feeAdj: 1, 4834 withdraw: true, 4835 wantErr: true, 4836 addr: testAddr, 4837 }, { 4838 name: "balance error", 4839 balErr: errors.New("test error"), 4840 wantErr: true, 4841 addr: testAddr, 4842 }} 4843 4844 for _, test := range tests { 4845 node.setBalanceError(eth, test.balErr) 4846 if assetID == BipID { 4847 node.bal = dexeth.GweiToWei(val + ethFees - test.sendAdj - test.feeAdj) 4848 } else { 4849 node.tokenContractor.bal = dexeth.GweiToWei(val - test.sendAdj) 4850 node.bal = dexeth.GweiToWei(tokenFees - test.feeAdj) 4851 } 4852 txFeeEstimator := w.(asset.TxFeeEstimator) 4853 estimate, _, err := txFeeEstimator.EstimateSendTxFee(test.addr, val, 0, false, false) 4854 if test.wantErr { 4855 if err == nil { 4856 t.Fatalf("expected error for test %v", test.name) 4857 } 4858 continue 4859 } 4860 if assetID == BipID { 4861 if estimate != ethFees { 4862 t.Fatalf("%s: expected fees to be %v, got %v", test.name, ethFees, estimate) 4863 } 4864 } else { 4865 if estimate != tokenFees { 4866 t.Fatalf("%s: expected fees to be %v, got %v", test.name, tokenFees, estimate) 4867 } 4868 } 4869 if err != nil { 4870 t.Fatalf("%s: unexpected error: %v", test.name, err) 4871 } 4872 } 4873 } 4874 4875 // This test will fail if new versions of the eth or the test token 4876 // contract (that require more gas) are added. 4877 func TestMaxSwapRedeemLots(t *testing.T) { 4878 t.Run("eth", func(t *testing.T) { testMaxSwapRedeemLots(t, BipID) }) 4879 t.Run("token", func(t *testing.T) { testMaxSwapRedeemLots(t, usdcTokenID) }) 4880 } 4881 4882 func testMaxSwapRedeemLots(t *testing.T, assetID uint32) { 4883 drv := &Driver{} 4884 logger := dex.StdOutLogger("ETHTEST", dex.LevelOff) 4885 tmpDir := t.TempDir() 4886 4887 settings := map[string]string{providersKey: "a.ipc"} 4888 err := CreateEVMWallet(dexeth.ChainIDs[dex.Testnet], &asset.CreateWalletParams{ 4889 Type: walletTypeRPC, 4890 Seed: encode.RandomBytes(32), 4891 Pass: encode.RandomBytes(32), 4892 Settings: settings, 4893 DataDir: tmpDir, 4894 Net: dex.Testnet, 4895 Logger: logger, 4896 }, &testnetCompatibilityData, true) 4897 if err != nil { 4898 t.Fatalf("CreateEVMWallet error: %v", err) 4899 } 4900 4901 wallet, err := drv.Open(&asset.WalletConfig{ 4902 Type: walletTypeRPC, 4903 Settings: settings, 4904 DataDir: tmpDir, 4905 }, logger, dex.Testnet) 4906 if err != nil { 4907 t.Fatalf("driver open error: %v", err) 4908 } 4909 4910 if assetID != BipID { 4911 eth, _ := wallet.(*ETHWallet) 4912 eth.net = dex.Simnet 4913 wallet, err = eth.OpenTokenWallet(&asset.TokenConfig{ 4914 AssetID: assetID, 4915 }) 4916 if err != nil { 4917 t.Fatal(err) 4918 } 4919 } 4920 4921 info := wallet.Info() 4922 if assetID == BipID { 4923 if info.MaxSwapsInTx != 28 { 4924 t.Fatalf("expected 28 for max swaps but got %d", info.MaxSwapsInTx) 4925 } 4926 if info.MaxRedeemsInTx != 63 { 4927 t.Fatalf("expected 63 for max redemptions but got %d", info.MaxRedeemsInTx) 4928 } 4929 } else { 4930 if info.MaxSwapsInTx != 20 { 4931 t.Fatalf("expected 20 for max swaps but got %d", info.MaxSwapsInTx) 4932 } 4933 if info.MaxRedeemsInTx != 45 { 4934 t.Fatalf("expected 45 for max redemptions but got %d", info.MaxRedeemsInTx) 4935 } 4936 } 4937 } 4938 4939 func TestSwapOrRedemptionFeesPaid(t *testing.T) { 4940 ctx, cancel := context.WithCancel(context.Background()) 4941 defer cancel() 4942 _, bw, node, shutdown := tassetWallet(BipID) 4943 defer shutdown() 4944 4945 secretHA, secretHB := encode.RandomBytes(32), encode.RandomBytes(32) 4946 contractDataFn := func(ver uint32, secretH []byte) []byte { 4947 s := [32]byte{} 4948 copy(s[:], secretH) 4949 return dexeth.EncodeContractData(ver, s) 4950 } 4951 const tip = 100 4952 const feeRate = 2 // gwei / gas 4953 const gasUsed = 100 4954 const fees = feeRate * gasUsed 4955 4956 bw.currentTip = &types.Header{ 4957 BaseFee: dexeth.GweiToWei(2), 4958 Number: big.NewInt(tip), 4959 } 4960 4961 confirmedReceipt := &types.Receipt{ 4962 GasUsed: gasUsed, 4963 EffectiveGasPrice: dexeth.GweiToWei(feeRate), 4964 BlockNumber: big.NewInt(tip - txConfsNeededToConfirm + 1), 4965 } 4966 4967 unconfirmedReceipt := &types.Receipt{ 4968 BlockNumber: big.NewInt(tip - txConfsNeededToConfirm + 2), 4969 } 4970 4971 initFn := func(secretHs [][]byte) []byte { 4972 inits := make([]*dexeth.Initiation, 0, len(secretHs)) 4973 for i := range secretHs { 4974 s := [32]byte{} 4975 copy(s[:], secretHs[i]) 4976 init := &dexeth.Initiation{ 4977 SecretHash: s, 4978 Value: big.NewInt(0), 4979 } 4980 inits = append(inits, init) 4981 } 4982 data, err := packInitiateDataV0(inits) 4983 if err != nil { 4984 t.Fatalf("problem packing inits: %v", err) 4985 } 4986 return data 4987 } 4988 redeemFn := func(secretHs [][]byte) []byte { 4989 redeems := make([]*dexeth.Redemption, 0, len(secretHs)) 4990 for i := range secretHs { 4991 s := [32]byte{} 4992 copy(s[:], secretHs[i]) 4993 redeem := &dexeth.Redemption{ 4994 SecretHash: s, 4995 } 4996 redeems = append(redeems, redeem) 4997 } 4998 data, err := packRedeemDataV0(redeems) 4999 if err != nil { 5000 t.Fatalf("problem packing redeems: %v", err) 5001 } 5002 return data 5003 } 5004 abFn := func() [][]byte { 5005 return [][]byte{secretHA, secretHB} 5006 } 5007 sortedFn := func() [][]byte { 5008 ab := abFn() 5009 sort.Slice(ab, func(i, j int) bool { return bytes.Compare(ab[i], ab[j]) < 0 }) 5010 return ab 5011 } 5012 initTx := tTx(200, 2, 0, nil, initFn(abFn()), 200) 5013 redeemTx := tTx(200, 3, 0, nil, redeemFn(abFn()), 200) 5014 tests := []struct { 5015 name string 5016 contractData []byte 5017 isInit, wantErr bool 5018 receipt *types.Receipt 5019 receiptTx *types.Transaction 5020 receiptErr error 5021 wantSecrets [][]byte 5022 pendingTx *types.Transaction 5023 pendingTxBlock uint64 5024 }{{ 5025 name: "ok init", 5026 contractData: contractDataFn(0, secretHA), 5027 isInit: true, 5028 receipt: confirmedReceipt, 5029 receiptTx: initTx, 5030 wantSecrets: sortedFn(), 5031 }, { 5032 name: "ok redeem", 5033 contractData: contractDataFn(0, secretHB), 5034 receipt: confirmedReceipt, 5035 receiptTx: redeemTx, 5036 wantSecrets: sortedFn(), 5037 }, { 5038 name: "ok init from pending txs", 5039 contractData: contractDataFn(0, secretHA), 5040 isInit: true, 5041 pendingTx: initTx, 5042 pendingTxBlock: confirmedReceipt.BlockNumber.Uint64(), 5043 wantSecrets: sortedFn(), 5044 }, { 5045 name: "ok redeem from pending txs", 5046 contractData: contractDataFn(0, secretHB), 5047 pendingTx: redeemTx, 5048 pendingTxBlock: confirmedReceipt.BlockNumber.Uint64(), 5049 wantSecrets: sortedFn(), 5050 }, { 5051 name: "bad contract data", 5052 contractData: nil, 5053 wantErr: true, 5054 }, { 5055 name: "receipt error", 5056 contractData: contractDataFn(0, secretHA), 5057 receiptErr: errors.New("test error"), 5058 wantErr: true, 5059 }, { 5060 name: "not enough confirms", 5061 contractData: contractDataFn(0, secretHA), 5062 receipt: unconfirmedReceipt, 5063 wantErr: true, 5064 }, { 5065 name: "not enough confs, pending tx", 5066 contractData: contractDataFn(0, secretHA), 5067 isInit: true, 5068 pendingTx: initTx, 5069 pendingTxBlock: confirmedReceipt.BlockNumber.Uint64() + 1, 5070 wantSecrets: sortedFn(), 5071 wantErr: true, 5072 }, { 5073 name: "bad init data", 5074 contractData: contractDataFn(0, secretHA), 5075 isInit: true, 5076 receipt: confirmedReceipt, 5077 receiptTx: tTx(200, 2, 0, nil, nil, 200), 5078 wantErr: true, 5079 }, { 5080 name: "bad redeem data", 5081 contractData: contractDataFn(0, secretHA), 5082 receipt: confirmedReceipt, 5083 receiptTx: tTx(200, 2, 0, nil, nil, 200), 5084 wantErr: true, 5085 }, { 5086 name: "secret hash not found", 5087 contractData: contractDataFn(0, secretHB), 5088 isInit: true, 5089 receipt: confirmedReceipt, 5090 receiptTx: tTx(200, 2, 0, nil, initFn([][]byte{secretHA}), 200), 5091 wantErr: true, 5092 }} 5093 for _, test := range tests { 5094 var txHash common.Hash 5095 if test.pendingTx != nil { 5096 wt := bw.extendedTx(test.pendingTx, asset.Unknown, 1, nil) 5097 wt.BlockNumber = test.pendingTxBlock 5098 wt.Fees = fees 5099 bw.pendingTxs = []*extendedWalletTx{wt} 5100 txHash = test.pendingTx.Hash() 5101 } 5102 node.receiptTx = test.receiptTx 5103 node.receipt = test.receipt 5104 node.receiptErr = test.receiptErr 5105 feesPaid, secretHs, err := bw.swapOrRedemptionFeesPaid(ctx, txHash[:], test.contractData, test.isInit) 5106 if test.wantErr { 5107 if err == nil { 5108 t.Fatalf("%q: expected error", test.name) 5109 } 5110 continue 5111 } 5112 if err != nil { 5113 t.Fatalf("%q: unexpected error: %v", test.name, err) 5114 } 5115 if feesPaid != fees { 5116 t.Fatalf("%q: wanted fee %d but got %d", test.name, fees, feesPaid) 5117 } 5118 if len(test.wantSecrets) != len(secretHs) { 5119 t.Fatalf("%q: wanted %d secrets but got %d", test.name, len(test.wantSecrets), len(secretHs)) 5120 } 5121 for i := range test.wantSecrets { 5122 sGot := secretHs[i] 5123 sWant := test.wantSecrets[i] 5124 if !bytes.Equal(sGot, sWant) { 5125 t.Fatalf("%q: wanted secret %x but got %x at position %d", test.name, sWant, sGot, i) 5126 } 5127 } 5128 } 5129 } 5130 5131 func TestReceiptCache(t *testing.T) { 5132 m := &multiRPCClient{ 5133 finalizeConfs: 3, 5134 } 5135 c := make(map[common.Hash]*receiptRecord) 5136 m.receipts.cache = c 5137 5138 r := &receiptRecord{ 5139 r: &types.Receipt{ 5140 Type: 50, 5141 }, 5142 lastAccess: time.Now(), 5143 } 5144 5145 var txHash common.Hash 5146 copy(txHash[:], encode.RandomBytes(32)) 5147 c[txHash] = r 5148 5149 if r := m.cachedReceipt(txHash); r == nil { 5150 t.Fatalf("cached receipt not returned") 5151 } 5152 5153 r.lastAccess = time.Now().Add(-(unconfirmedReceiptExpiration + 1)) 5154 if r := m.cachedReceipt(txHash); r != nil { 5155 t.Fatalf("expired receipt returned") 5156 } 5157 5158 // The receipt still hasn't been pruned. 5159 if len(c) != 1 { 5160 t.Fatalf("receipt was pruned?") 5161 } 5162 5163 // An if it was confirmed, it would be returned. 5164 r.confirmed = true 5165 if r := m.cachedReceipt(txHash); r == nil { 5166 t.Fatalf("confirmed receipt not returned") 5167 } 5168 5169 r.lastAccess = time.Now().Add(-(receiptCacheExpiration + 1)) 5170 m.receipts.lastClean = time.Time{} 5171 m.cachedReceipt(common.Hash{}) 5172 // The receipt still hasn't been pruned. 5173 if len(c) != 0 { 5174 t.Fatalf("receipt wasn't pruned") 5175 } 5176 5177 } 5178 5179 func TestFreshProviderList(t *testing.T) { 5180 5181 tests := []struct { 5182 times []int64 5183 expOrder []int 5184 }{ 5185 { 5186 times: []int64{1, 2, 3}, 5187 expOrder: []int{0, 1, 2}, 5188 }, 5189 { 5190 times: []int64{3, 2, 1}, 5191 expOrder: []int{2, 1, 0}, 5192 }, 5193 { 5194 times: []int64{1, 5, 4, 2, 3}, 5195 expOrder: []int{0, 3, 4, 2, 1}, 5196 }, 5197 } 5198 5199 for i, tt := range tests { 5200 t.Run(fmt.Sprintf("test#%d", i), func(t *testing.T) { 5201 node := &multiRPCClient{ 5202 finalizeConfs: 3, 5203 providers: make([]*provider, len(tt.times)), 5204 } 5205 for i, stamp := range tt.times { 5206 p := &provider{} 5207 p.tip.headerStamp = time.Unix(stamp, 0) 5208 p.tip.failCount = i // hi-jacking field for initial sort order 5209 node.providers[i] = p 5210 } 5211 providers := node.freshnessSortedProviders() 5212 for i, p := range providers { 5213 if p.tip.failCount != tt.expOrder[i] { 5214 t.Fatalf("%d'th provider in sorted list is unexpected", i) 5215 } 5216 } 5217 }) 5218 } 5219 } 5220 5221 func TestDomain(t *testing.T) { 5222 tests := []struct { 5223 addr string 5224 wantDomain string 5225 wantErr bool 5226 }{ 5227 { 5228 addr: "http://www.place.io/v3/234234wfsefe", 5229 wantDomain: "place.io", 5230 }, 5231 { 5232 addr: "wss://www.place.nz/stuff?=token", 5233 wantDomain: "place.nz", 5234 }, 5235 { 5236 addr: "http://en.us.lotssubdomains.place.io/v3/234234wfsefe", 5237 wantDomain: "place.io", 5238 }, 5239 { 5240 addr: "https://www.place.co.uk:443/blog/article/search?docid=720&hl=en#dayone", 5241 wantDomain: "place.co.uk:443", 5242 }, 5243 { 5244 addr: "wmba://www.place.com", 5245 wantDomain: "place.com", 5246 }, 5247 { 5248 addr: "ws://127.0.0.1:3000", 5249 wantDomain: "127.0.0.1:3000", 5250 }, 5251 { 5252 addr: "ws://localhost:3000", 5253 wantDomain: "localhost:3000", 5254 }, 5255 { 5256 addr: "http://123.123.123.123:3000", 5257 wantDomain: "123.123.123.123:3000", 5258 }, 5259 { 5260 addr: "https://123.123.123.123", 5261 wantDomain: "123.123.123.123", 5262 }, 5263 { 5264 addr: "ws://[abab:fde3:0:0:0:0:0:1]:8080", 5265 wantDomain: "[abab:fde3:0:0:0:0:0:1]:8080", 5266 }, 5267 { 5268 addr: "ws://[::1]:8000", 5269 wantDomain: "[::1]:8000", 5270 }, 5271 { 5272 addr: "[::1]:8000", 5273 wantDomain: "[::1]:8000", 5274 }, 5275 { 5276 addr: "[::1]", 5277 wantDomain: "[::1]", 5278 }, 5279 { 5280 addr: "127.0.0.1", 5281 wantDomain: "127.0.0.1", 5282 }, 5283 { 5284 addr: "/home/john/.geth/geth.ipc", 5285 wantDomain: "/home/john/.geth/geth.ipc", 5286 }, 5287 { 5288 addr: "/home/john/.geth/geth", 5289 wantDomain: "/home/john/.geth/geth", 5290 }, 5291 { 5292 addr: "https://\n:1234", 5293 wantErr: true, 5294 }, 5295 { 5296 addr: ":asdf", 5297 wantErr: true, 5298 }, 5299 { 5300 wantErr: true, 5301 }, 5302 } 5303 5304 for i, test := range tests { 5305 t.Run(fmt.Sprintf("test#%d", i), func(t *testing.T) { 5306 d, err := domain(test.addr) 5307 if test.wantErr { 5308 if err == nil { 5309 t.Fatalf("expected error") 5310 } 5311 return 5312 } 5313 if err != nil { 5314 t.Fatalf("unexpected error: %v", err) 5315 } 5316 if test.wantDomain != d { 5317 t.Fatalf("wanted domain %s but got %s", test.wantDomain, d) 5318 } 5319 }) 5320 } 5321 } 5322 5323 func parseRecoveryID(c asset.Coin) []byte { 5324 return c.(asset.RecoveryCoin).RecoveryID() 5325 } 5326 5327 func randomHash() common.Hash { 5328 return common.BytesToHash(encode.RandomBytes(20)) 5329 }