From f683573222317c8399e82ccff3439ab5e161f2c4 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Fri, 18 Nov 2022 16:00:26 +0900 Subject: [PATCH] erc20: Add testnet usdc. --- client/asset/eth/contractor.go | 21 +- client/asset/eth/eth.go | 84 +++-- client/asset/eth/eth_test.go | 92 ++++-- client/asset/eth/multirpc.go | 83 ++++- client/asset/eth/nodeclient_harness_test.go | 293 +++++++++++++----- client/core/bookie.go | 7 +- client/core/core.go | 4 +- client/core/exchangeratefetcher.go | 4 +- .../webserver/site/src/img/coins/usdc.eth.png | Bin 0 -> 4537 bytes client/webserver/site/src/js/app.ts | 2 +- client/webserver/site/src/js/doc.ts | 3 +- client/webserver/site/src/js/markets.ts | 10 +- client/webserver/site/src/js/order.ts | 31 +- client/webserver/site/src/js/wallets.ts | 34 +- dex/bip-id.go | 1 + dex/networks/erc20/params.go | 21 -- dex/networks/eth/params.go | 1 + dex/networks/eth/tokens.go | 97 +++++- server/asset/eth/eth.go | 4 +- server/market/balancer.go | 2 +- server/market/orderrouter.go | 31 +- 21 files changed, 607 insertions(+), 218 deletions(-) create mode 100644 client/webserver/site/src/img/coins/usdc.eth.png delete mode 100644 dex/networks/erc20/params.go diff --git a/client/asset/eth/contractor.go b/client/asset/eth/contractor.go index 68b7a064f3..88f48f9b16 100644 --- a/client/asset/eth/contractor.go +++ b/client/asset/eth/contractor.go @@ -44,6 +44,7 @@ type contractor interface { // case will always be zero. value(context.Context, *types.Transaction) (incoming, outgoing uint64, err error) isRefundable(secretHash [32]byte) (bool, error) + voidUnusedNonce() } // tokenContractor interacts with an ERC20 token contract and a token swap @@ -113,7 +114,7 @@ func newV0Contractor(net dex.Network, acctAddr common.Address, cb bind.ContractB } // initiate sends the initiations to the swap contract's initiate function. -func (c *contractorV0) initiate(txOpts *bind.TransactOpts, contracts []*asset.Contract) (*types.Transaction, error) { +func (c *contractorV0) initiate(txOpts *bind.TransactOpts, contracts []*asset.Contract) (tx *types.Transaction, err error) { inits := make([]swapv0.ETHSwapInitiation, 0, len(contracts)) secrets := make(map[[32]byte]bool, len(contracts)) @@ -146,9 +147,10 @@ func (c *contractorV0) initiate(txOpts *bind.TransactOpts, contracts []*asset.Co } // redeem sends the redemptions to the swap contracts redeem method. -func (c *contractorV0) redeem(txOpts *bind.TransactOpts, redemptions []*asset.Redemption) (*types.Transaction, error) { +func (c *contractorV0) redeem(txOpts *bind.TransactOpts, redemptions []*asset.Redemption) (tx *types.Transaction, err error) { redemps := make([]swapv0.ETHSwapRedemption, 0, len(redemptions)) secretHashes := make(map[[32]byte]bool, len(redemptions)) + for _, r := range redemptions { secretB, secretHashB := r.Secret, r.Spends.SecretHash if len(secretB) != 32 || len(secretHashB) != 32 { @@ -194,7 +196,7 @@ func (c *contractorV0) swap(ctx context.Context, secretHash [32]byte) (*dexeth.S // refund issues the refund command to the swap contract. Use isRefundable first // to ensure the refund will be accepted. -func (c *contractorV0) refund(txOpts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { +func (c *contractorV0) refund(txOpts *bind.TransactOpts, secretHash [32]byte) (tx *types.Transaction, err error) { return c.contractV0.Refund(txOpts, secretHash) } @@ -318,6 +320,15 @@ func (c *contractorV0) outgoingValue(tx *types.Transaction) (swapped uint64) { return } +// voidUnusedNonce allows the next nonce received from a provider to be the same +// as a recent nonce. Use when we fetch a nonce but error before or while +// sending a transaction. +func (c *contractorV0) voidUnusedNonce() { + if mRPC, is := c.cb.(*multiRPCClient); is { + mRPC.voidUnusedNonce() + } +} + // tokenContractorV0 is a contractor that implements the tokenContractor // methods, providing access to the methods of the token's ERC20 contract. type tokenContractorV0 struct { @@ -395,13 +406,13 @@ func (c *tokenContractorV0) allowance(ctx context.Context) (*big.Int, error) { // approve sends an approve transaction approving the linked contract to call // transferFrom for the specified amount. -func (c *tokenContractorV0) approve(txOpts *bind.TransactOpts, amount *big.Int) (*types.Transaction, error) { +func (c *tokenContractorV0) approve(txOpts *bind.TransactOpts, amount *big.Int) (tx *types.Transaction, err error) { return c.tokenContract.Approve(txOpts, c.contractAddr, amount) } // transfer calls the transfer method of the erc20 token contract. Used for // sends or withdrawals. -func (c *tokenContractorV0) transfer(txOpts *bind.TransactOpts, addr common.Address, amount *big.Int) (*types.Transaction, error) { +func (c *tokenContractorV0) transfer(txOpts *bind.TransactOpts, addr common.Address, amount *big.Int) (tx *types.Transaction, err error) { return c.tokenContract.Transfer(txOpts, addr, amount) } diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index 5b86c9b1de..2d46c6f1f4 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -30,7 +30,6 @@ import ( "decred.org/dcrdex/dex/config" "decred.org/dcrdex/dex/encode" "decred.org/dcrdex/dex/keygen" - "decred.org/dcrdex/dex/networks/erc20" dexeth "decred.org/dcrdex/dex/networks/eth" "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" "github.com/decred/dcrd/hdkeychain/v3" @@ -60,7 +59,8 @@ func registerToken(tokenID uint32, desc string, nets ...dex.Network) { func init() { asset.Register(BipID, &Driver{}) // Test token - registerToken(testTokenID, "A token wallet for the DEX test token. Used for testing DEX software.", dex.Simnet) + registerToken(simnetTokenID, "A token wallet for the DEX test token. Used for testing DEX software.", dex.Simnet) + registerToken(usdcTokenID, "The USDC Ethereum ERC20 token.", dex.Testnet) } const ( @@ -83,7 +83,8 @@ const ( ) var ( - testTokenID, _ = dex.BipSymbolID("dextt.eth") + simnetTokenID, _ = dex.BipSymbolID("dextt.eth") + usdcTokenID, _ = dex.BipSymbolID("usdc.eth") // blockTicker is the delay between calls to check for new blocks. blockTicker = time.Second peerCountTicker = 5 * time.Second @@ -1482,7 +1483,7 @@ func (w *assetWallet) approvalGas(newGas *big.Int, ver uint32) (uint64, error) { if approveEst, err := w.estimateApproveGas(newGas); err != nil { return 0, fmt.Errorf("error estimating approve gas: %v", err) } else if approveEst > approveGas { - w.log.Warnf("Approve gas estimate %d is greater than the expected value %d. Using live estimate + 10%.") + w.log.Warnf("Approve gas estimate %d is greater than the expected value %d. Using live estimate + 10%%.", approveEst, approveGas) return approveEst * 11 / 10, nil } return approveGas, nil @@ -1756,6 +1757,14 @@ func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uin return fail("Swap: initiate error: %w", err) } + if dexeth.Tokens[w.assetID] == nil || + dexeth.Tokens[w.assetID].NetTokens[w.net] == nil || + dexeth.Tokens[w.assetID].NetTokens[w.net].SwapContracts[swaps.Version] == nil { + return fail("unable to find contract address for asset %d", w.assetID) + } + + contractAddr := dexeth.Tokens[w.assetID].NetTokens[w.net].SwapContracts[swaps.Version].Address.String() + txHash := tx.Hash() for _, swap := range swaps.Contracts { var secretHash [dexeth.SecretHashSize]byte @@ -1766,7 +1775,7 @@ func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uin txHash: txHash, secretHash: secretHash, ver: swaps.Version, - contractAddr: erc20.ContractAddresses[swaps.Version][w.net].String(), + contractAddr: contractAddr, }) } @@ -2039,11 +2048,13 @@ func (w *assetWallet) approveToken(amount *big.Int, maxFeeRate uint64, contractV return tx, w.withTokenContractor(w.assetID, contractVer, func(c tokenContractor) error { tx, err = c.approve(txOpts, amount) - if err == nil { - w.log.Infof("Approval sent for %s at token address %s, nonce = %s", - dex.BipIDSymbol(w.assetID), c.tokenAddress(), txOpts.Nonce) + if err != nil { + c.voidUnusedNonce() + return err } - return err + w.log.Infof("Approval sent for %s at token address %s, nonce = %s", + dex.BipIDSymbol(w.assetID), c.tokenAddress(), txOpts.Nonce) + return nil }) } @@ -3387,7 +3398,7 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede monitoredTx, err := w.getLatestMonitoredTx(txHash) if err != nil { - w.log.Error("getLatestMonitoredTx error: %v", err) + w.log.Errorf("getLatestMonitoredTx error: %v", err) return w.confirmRedemptionWithoutMonitoredTx(txHash, redemption, feeWallet) } // This mutex is locked inside of getLatestMonitoredTx. @@ -3571,7 +3582,14 @@ func (w *ETHWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate *big. if err != nil { return nil, err } - return w.node.sendTransaction(w.ctx, txOpts, addr, nil) + tx, err = w.node.sendTransaction(w.ctx, txOpts, addr, nil) + if err != nil { + if mRPC, is := w.node.(*multiRPCClient); is { + mRPC.voidUnusedNonce() + } + return nil, err + } + return tx, nil } // sendToAddr sends funds to the address. @@ -3588,7 +3606,11 @@ func (w *TokenWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate *bi return err } tx, err = c.transfer(txOpts, addr, w.evmify(amt)) - return err + if err != nil { + c.voidUnusedNonce() + return err + } + return nil }) } @@ -3612,10 +3634,17 @@ func (w *assetWallet) initiate(ctx context.Context, assetID uint32, contracts [] } w.nonceSendMtx.Lock() defer w.nonceSendMtx.Unlock() - txOpts, _ := w.node.txOpts(ctx, val, gasLimit, dexeth.GweiToWei(maxFeeRate)) + txOpts, err := w.node.txOpts(ctx, val, gasLimit, dexeth.GweiToWei(maxFeeRate)) + if err != nil { + return nil, err + } return tx, w.withContractor(contractVer, func(c contractor) error { tx, err = c.initiate(txOpts, contracts) - return err + if err != nil { + c.voidUnusedNonce() + return err + } + return nil }) } @@ -3740,7 +3769,11 @@ func (w *assetWallet) redeem(ctx context.Context, assetID uint32, redemptions [] return tx, w.withContractor(contractVer, func(c contractor) error { tx, err = c.redeem(txOpts, redemptions) - return err + if err != nil { + c.voidUnusedNonce() + return err + } + return nil }) } @@ -3756,12 +3789,16 @@ func (w *assetWallet) refund(secretHash [32]byte, maxFeeRate uint64, contractVer defer w.nonceSendMtx.Unlock() txOpts, err := w.node.txOpts(w.ctx, 0, gas.Refund, dexeth.GweiToWei(maxFeeRate)) if err != nil { - return nil, fmt.Errorf("addSignerToOpts error: %w", err) + return nil, err } return tx, w.withContractor(contractVer, func(c contractor) error { tx, err = c.refund(txOpts, secretHash) - return err + if err != nil { + c.voidUnusedNonce() + return err + } + return nil }) } @@ -3799,7 +3836,8 @@ func checkTxStatus(receipt *types.Receipt, gasLimit uint64) error { // factor of 2. The account should already have a trading balance of at least // maxSwaps gwei (token or eth), and sufficient eth balance to cover the // requisite tx fees. -func GetGasEstimates(ctx context.Context, cl ethFetcher, c contractor, maxSwaps int, g *dexeth.Gases, waitForMined func()) error { +func GetGasEstimates(ctx context.Context, cl ethFetcher, c contractor, maxSwaps int, g *dexeth.Gases, + toAddress common.Address, waitForMined func(), waitForReceipt func(ethFetcher, *types.Transaction) (*types.Receipt, error)) error { tokenContractor, isToken := c.(tokenContractor) stats := struct { @@ -3872,7 +3910,7 @@ func GetGasEstimates(ctx context.Context, cl ethFetcher, c contractor, maxSwaps if err != nil { return fmt.Errorf("error constructing signed tx opts for transfer: %v", err) } - transferTx, err = tokenContractor.transfer(txOpts, cl.address(), big.NewInt(1)) + transferTx, err = tokenContractor.transfer(txOpts, toAddress, big.NewInt(1)) if err != nil { return fmt.Errorf("error estimating transfer gas: %v", err) } @@ -3909,7 +3947,7 @@ func GetGasEstimates(ctx context.Context, cl ethFetcher, c contractor, maxSwaps return fmt.Errorf("initiate error for %d swaps: %v", n, err) } waitForMined() - receipt, _, err := cl.transactionReceipt(ctx, tx.Hash()) + receipt, err := waitForReceipt(cl, tx) if err != nil { return err } @@ -3919,7 +3957,7 @@ func GetGasEstimates(ctx context.Context, cl ethFetcher, c contractor, maxSwaps stats.swaps = append(stats.swaps, receipt.GasUsed) if isToken { - receipt, _, err = cl.transactionReceipt(ctx, approveTx.Hash()) + receipt, err = waitForReceipt(cl, approveTx) if err != nil { return err } @@ -3927,7 +3965,7 @@ func GetGasEstimates(ctx context.Context, cl ethFetcher, c contractor, maxSwaps return err } stats.approves = append(stats.approves, receipt.GasUsed) - receipt, _, err = cl.transactionReceipt(ctx, transferTx.Hash()) + receipt, err = waitForReceipt(cl, transferTx) if err != nil { return err } @@ -3962,7 +4000,7 @@ func GetGasEstimates(ctx context.Context, cl ethFetcher, c contractor, maxSwaps return fmt.Errorf("redeem error for %d swaps: %v", n, err) } waitForMined() - receipt, _, err = cl.transactionReceipt(ctx, tx.Hash()) + receipt, err = waitForReceipt(cl, tx) if err != nil { return err } diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index f23d4b1b6d..c10a88e3e1 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -46,7 +46,7 @@ var ( testAddressC = common.HexToAddress("2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27") ethGases = dexeth.VersionedGases[0] - tokenGases = dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas + tokenGases = dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas tETH = &dex.Asset{ // Version meaning? @@ -70,7 +70,7 @@ var ( } tToken = &dex.Asset{ - ID: testTokenID, + ID: simnetTokenID, Symbol: "dextt.eth", Version: 0, SwapSize: tokenGases.Swap, @@ -353,6 +353,8 @@ func (c *tContractor) isRefundable(secretHash [32]byte) (bool, error) { return c.refundable, c.refundableErr } +func (c *tContractor) voidUnusedNonce() {} + type tTokenContractor struct { *tContractor tokenAddr common.Address @@ -629,19 +631,15 @@ func tassetWallet(assetID uint32) (asset.Wallet, *assetWallet, *testNode, contex assetID: BipID, atomize: dexeth.WeiToGwei, } - testTokenID, found := dex.BipSymbolID("dextt.eth") - if !found { - panic("could not find test token ID") - } w = &TokenWallet{ assetWallet: aw, cfg: &tokenWalletConfig{}, parent: node.tokenParent, - token: dexeth.Tokens[testTokenID], + token: dexeth.Tokens[simnetTokenID], } aw.wallets = map[uint32]*assetWallet{ - testTokenID: aw, - BipID: node.tokenParent, + simnetTokenID: aw, + BipID: node.tokenParent, } } @@ -735,7 +733,7 @@ func TestBalance(t *testing.T) { for _, test := range tests { var assetID uint32 = BipID if test.token { - assetID = testTokenID + assetID = simnetTokenID } _, eth, node, shutdown := tassetWallet(assetID) @@ -848,7 +846,7 @@ func TestFeeRate(t *testing.T) { func TestRefund(t *testing.T) { t.Run("eth", func(t *testing.T) { testRefund(t, BipID) }) - t.Run("token", func(t *testing.T) { testRefund(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testRefund(t, simnetTokenID) }) } func testRefund(t *testing.T, assetID uint32) { @@ -871,7 +869,7 @@ func testRefund(t *testing.T, assetID uint32) { dexeth.VersionedGases[1] = gasesV1 defer delete(dexeth.VersionedGases, 1) } else { - tokenContracts := dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts + tokenContracts := dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts tc := *tokenContracts[0] tc.Gas = *gasesV1 tokenContracts[1] = &tc @@ -1031,7 +1029,7 @@ func (b *badCoin) Value() uint64 { func TestFundOrderReturnCoinsFundingCoins(t *testing.T) { t.Run("eth", func(t *testing.T) { testFundOrderReturnCoinsFundingCoins(t, BipID) }) - t.Run("token", func(t *testing.T) { testFundOrderReturnCoinsFundingCoins(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testFundOrderReturnCoinsFundingCoins(t, simnetTokenID) }) } func testFundOrderReturnCoinsFundingCoins(t *testing.T, assetID uint32) { @@ -1450,7 +1448,7 @@ func TestPreSwap(t *testing.T) { var assetID uint32 = BipID assetCfg := tETH if test.token { - assetID = testTokenID + assetID = simnetTokenID assetCfg = tToken } @@ -1508,7 +1506,7 @@ func TestPreSwap(t *testing.T) { func TestSwap(t *testing.T) { t.Run("eth", func(t *testing.T) { testSwap(t, BipID) }) - t.Run("token", func(t *testing.T) { testSwap(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testSwap(t, simnetTokenID) }) } func testSwap(t *testing.T, assetID uint32) { @@ -1787,7 +1785,7 @@ func TestPreRedeem(t *testing.T) { } // Token - w, _, node, shutdown2 := tassetWallet(testTokenID) + w, _, node, shutdown2 := tassetWallet(simnetTokenID) defer shutdown2() form.Version = tToken.Version @@ -1805,7 +1803,7 @@ func TestPreRedeem(t *testing.T) { func TestRedeem(t *testing.T) { t.Run("eth", func(t *testing.T) { testRedeem(t, BipID) }) - t.Run("token", func(t *testing.T) { testRedeem(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testRedeem(t, simnetTokenID) }) } func testRedeem(t *testing.T, assetID uint32) { @@ -1815,7 +1813,7 @@ func testRedeem(t *testing.T, assetID uint32) { // Test with a non-zero contract version to ensure it makes it into the receipt contractVer := uint32(1) dexeth.VersionedGases[1] = ethGases // for dexeth.RedeemGas(..., 1) - tokenContracts := dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts + tokenContracts := dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts tokenContracts[1] = tokenContracts[0] defer delete(dexeth.VersionedGases, 1) defer delete(tokenContracts, 1) @@ -2480,7 +2478,7 @@ func TestMaxOrder(t *testing.T) { var assetID uint32 = BipID dexAsset := tETH if test.token { - assetID = testTokenID + assetID = simnetTokenID } w, _, node, shutdown := tassetWallet(assetID) @@ -2570,7 +2568,7 @@ func packRedeemDataV0(redemptions []*dexeth.Redemption) ([]byte, error) { func TestAuditContract(t *testing.T) { t.Run("eth", func(t *testing.T) { testAuditContract(t, BipID) }) - t.Run("token", func(t *testing.T) { testAuditContract(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testAuditContract(t, simnetTokenID) }) } func testAuditContract(t *testing.T, assetID uint32) { @@ -3156,7 +3154,7 @@ func TestLocktimeExpired(t *testing.T) { func TestFindRedemption(t *testing.T) { t.Run("eth", func(t *testing.T) { testFindRedemption(t, BipID) }) - t.Run("token", func(t *testing.T) { testFindRedemption(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testFindRedemption(t, simnetTokenID) }) } func testFindRedemption(t *testing.T, assetID uint32) { @@ -3298,7 +3296,7 @@ func testFindRedemption(t *testing.T, assetID uint32) { func TestRefundReserves(t *testing.T) { t.Run("eth", func(t *testing.T) { testRefundReserves(t, BipID) }) - t.Run("token", func(t *testing.T) { testRefundReserves(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testRefundReserves(t, simnetTokenID) }) } func testRefundReserves(t *testing.T, assetID uint32) { @@ -3326,7 +3324,7 @@ func testRefundReserves(t *testing.T, assetID uint32) { feeWallet = node.tokenParent assetV0 = *tToken assetV1 = *tToken - tokenContracts := dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts + tokenContracts := dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts gasesV0 = &tokenGases tc := *tokenContracts[0] tc.Gas = *gasesV1 @@ -3393,7 +3391,7 @@ func testRefundReserves(t *testing.T, assetID uint32) { func TestRedemptionReserves(t *testing.T) { t.Run("eth", func(t *testing.T) { testRedemptionReserves(t, BipID) }) - t.Run("token", func(t *testing.T) { testRedemptionReserves(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testRedemptionReserves(t, simnetTokenID) }) } func testRedemptionReserves(t *testing.T, assetID uint32) { @@ -3421,7 +3419,7 @@ func testRedemptionReserves(t *testing.T, assetID uint32) { feeWallet = node.tokenParent assetV0 = *tToken assetV1 = *tToken - tokenContracts := dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts + tokenContracts := dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts gasesV0 = &tokenGases tc := *tokenContracts[0] tc.Gas = *gasesV1 @@ -3525,7 +3523,7 @@ func TestReconfigure(t *testing.T) { func TestSend(t *testing.T) { t.Run("eth", func(t *testing.T) { testSend(t, BipID) }) - t.Run("token", func(t *testing.T) { testSend(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testSend(t, simnetTokenID) }) } func testSend(t *testing.T, assetID uint32) { @@ -3610,7 +3608,7 @@ func testSend(t *testing.T, assetID uint32) { func TestConfirmRedemption(t *testing.T) { t.Run("eth", func(t *testing.T) { testConfirmRedemption(t, BipID) }) - t.Run("token", func(t *testing.T) { testConfirmRedemption(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testConfirmRedemption(t, simnetTokenID) }) } func testConfirmRedemption(t *testing.T, assetID uint32) { @@ -4466,7 +4464,7 @@ func TestMarshalMonitoredTx(t *testing.T) { func TestEstimateSendTxFee(t *testing.T) { t.Run("eth", func(t *testing.T) { testEstimateSendTxFee(t, BipID) }) - t.Run("token", func(t *testing.T) { testEstimateSendTxFee(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testEstimateSendTxFee(t, simnetTokenID) }) } func testEstimateSendTxFee(t *testing.T, assetID uint32) { @@ -4550,7 +4548,7 @@ func testEstimateSendTxFee(t *testing.T, assetID uint32) { // contract (that require more gas) are added. func TestMaxSwapRedeemLots(t *testing.T) { t.Run("eth", func(t *testing.T) { testMaxSwapRedeemLots(t, BipID) }) - t.Run("token", func(t *testing.T) { testMaxSwapRedeemLots(t, testTokenID) }) + t.Run("token", func(t *testing.T) { testMaxSwapRedeemLots(t, simnetTokenID) }) } func testMaxSwapRedeemLots(t *testing.T, assetID uint32) { @@ -4845,6 +4843,42 @@ func TestReceiptCache(t *testing.T) { } +func TestUnusedNonce(t *testing.T) { + mRPC := new(multiRPCClient) + tests := []struct { + name string + nonce uint64 + want bool + wait bool + }{{ + name: "ok initiation", + nonce: 0, + want: true, + }, { + name: "ok larger", + nonce: 1, + want: true, + }, { + name: "same nonce", + nonce: 1, + // Uncomment for full tests. + // }, { + // name: "ok after expiration", + // nonce: 1, + // wait: true, + // want: true, + }} + for _, test := range tests { + if test.wait { + time.Sleep(time.Minute + time.Second) + } + got := mRPC.unusedNonce(test.nonce) + if test.want != got { + t.Fatalf("%q: wanted %v got %v", test.name, test.want, got) + } + } +} + func parseRecoveryID(c asset.Coin) []byte { return c.(asset.RecoveryCoin).RecoveryID() } diff --git a/client/asset/eth/multirpc.go b/client/asset/eth/multirpc.go index bbb3314900..f7247197d9 100644 --- a/client/asset/eth/multirpc.go +++ b/client/asset/eth/multirpc.go @@ -293,6 +293,12 @@ type multiRPCClient struct { endpoints []string providers []*provider + lastNonce struct { + sync.Mutex + nonce uint64 + stamp time.Time + } + // When we send transactions close together, we'll want to use the same // provider. lastProvider struct { @@ -502,6 +508,44 @@ func (m *multiRPCClient) connect(ctx context.Context) (err error) { return nil } +// unusedNonce returns true and saves the nonce for the next call when a nonce +// has not been received recently. +func (m *multiRPCClient) unusedNonce(nonce uint64) bool { + const expiration = time.Minute + ln := &m.lastNonce + set := func() bool { + ln.nonce = nonce + ln.stamp = time.Now() + return true + } + ln.Lock() + defer ln.Unlock() + // Ok if the nonce is larger than previous. + if ln.nonce < nonce { + return set() + } + // Ok if initiation. + if ln.stamp.IsZero() { + return set() + } + // Ok if expiration has passed. + if time.Now().After(ln.stamp.Add(expiration)) { + return set() + } + // Nonce is the same or less than previous and expiration has not + // passed. + return false +} + +// voidUnusedNonce sets time to zero time so that the next call to unusedNonce +// will return true. This is needed when we know that a tx has failed at the +// time of sending so that the same nonce can be used again. +func (m *multiRPCClient) voidUnusedNonce() { + m.lastNonce.Lock() + defer m.lastNonce.Unlock() + m.lastNonce.stamp = time.Time{} +} + func (m *multiRPCClient) reconfigure(ctx context.Context, settings map[string]string) error { providerDef := settings[providersKey] if len(providerDef) == 0 { @@ -577,7 +621,7 @@ func (m *multiRPCClient) transactionReceipt(ctx context.Context, txHash common.H return err }); err != nil { if isNotFoundError(err) { - return nil, nil, asset.ErrNotEnoughConfirms + return nil, nil, asset.CoinNotFoundError } return nil, nil, err } @@ -800,10 +844,33 @@ func (m *multiRPCClient) nonceProviderList() []*provider { // nextNonce returns the next nonce number for the account. func (m *multiRPCClient) nextNonce(ctx context.Context) (nonce uint64, err error) { - return nonce, m.withPreferred(func(p *provider) error { - nonce, err = p.ec.PendingNonceAt(ctx, m.creds.addr) - return err - }) + checks := 5 + checkDelay := time.Second * 5 + for i := 0; i < checks; i++ { + var host string + err = m.withPreferred(func(p *provider) error { + host = p.host + nonce, err = p.ec.PendingNonceAt(ctx, m.creds.addr) + return err + }) + if err != nil { + return 0, err + } + if m.unusedNonce(nonce) { + return nonce, nil + } + m.log.Warnf("host %s returned recently used account nonce number %d. try %d of %d.", + host, nonce, i+1, checks) + // Delay all but the last check. + if i+1 < checks { + select { + case <-time.After(checkDelay): + case <-ctx.Done(): + return 0, ctx.Err() + } + } + } + return 0, errors.New("preferred provider returned a recently used account nonce") } func (m *multiRPCClient) address() common.Address { @@ -954,19 +1021,15 @@ func (m *multiRPCClient) syncProgress(ctx context.Context) (prog *ethereum.SyncP func (m *multiRPCClient) transactionConfirmations(ctx context.Context, txHash common.Hash) (confs uint32, err error) { var r *types.Receipt var tip *types.Header - var notFound bool if err := m.withPreferred(func(p *provider) error { r, err = p.ec.TransactionReceipt(ctx, txHash) if err != nil { - if isNotFoundError(err) { - notFound = true - } return err } tip, err = p.bestHeader(ctx, m.log) return err }); err != nil { - if notFound { + if isNotFoundError(err) { return 0, asset.CoinNotFoundError } return 0, err diff --git a/client/asset/eth/nodeclient_harness_test.go b/client/asset/eth/nodeclient_harness_test.go index 9442732c4c..0be1371ce5 100644 --- a/client/asset/eth/nodeclient_harness_test.go +++ b/client/asset/eth/nodeclient_harness_test.go @@ -29,11 +29,13 @@ import ( "errors" "flag" "fmt" + "math" "math/big" "os" "os/exec" "os/signal" "path/filepath" + "strconv" "sync" "testing" "time" @@ -98,7 +100,7 @@ var ( simnetTokenContractor tokenContractor participantTokenContractor tokenContractor ethGases = dexeth.VersionedGases[0] - tokenGases = &dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas + tokenGases *dexeth.Gases testnetSecPerBlock = 15 * time.Second // secPerBlock is one for simnet, because it takes one second to mine a // block currently. Is set in code to testnetSecPerBlock if runing on @@ -135,6 +137,9 @@ var ( // funds will be printed. testnetWalletSeed string testnetParticipantWalletSeed string + usdcID, _ = dex.BipSymbolID("usdc.eth") + testTokenID uint32 + masterToken *dexeth.Token ) func newContract(stamp uint64, secretHash [32]byte, val uint64) *asset.Contract { @@ -215,7 +220,9 @@ func waitForMinedRPC() error { // waitForMined will multiply the time limit by testnetSecPerBlock for // testnet and mine blocks when on simnet. -func waitForMined(cl ethFetcher, nBlock int, waitTimeLimit bool) error { +func waitForMined(t *testing.T, nBlock int, waitTimeLimit bool) error { + t.Helper() + timesUp := time.After(time.Duration(nBlock) * secPerBlock) if isTestnet && useRPC { waitForMinedRPC() } @@ -228,7 +235,6 @@ func waitForMined(cl ethFetcher, nBlock int, waitTimeLimit bool) error { _ = exec.Command("geth", "--datadir="+alphaNodeDir, "attach", "--exec", "miner.stop()").Run() }() } - timesUp := time.After(time.Duration(nBlock) * secPerBlock) out: for { select { @@ -237,7 +243,9 @@ out: case <-timesUp: return errors.New("timed out") case <-time.After(time.Second): - // TODO RPC + // NOTE: Not effectual for providers. waitForMinedRPC + // above handles waiting for mined blocks that we assume + // have our transactions. txsa, err := ethClient.pendingTransactions() if err != nil { return fmt.Errorf("initiator pendingTransactions error: %v", err) @@ -252,7 +260,11 @@ out: } } if waitTimeLimit { - <-timesUp + select { + case <-ctx.Done(): + return ctx.Err() + case <-timesUp: + } } return nil } @@ -348,6 +360,7 @@ func prepareTestNodeClients(initiatorDir, participantDir string, net dex.Network } func runSimnet(m *testing.M) (int, error) { + testTokenID = simnetTokenID // Create dir if none yet exists. This persists for the life of the // testing harness. err := os.MkdirAll(simnetWalletDir, 0755) @@ -359,8 +372,11 @@ func runSimnet(m *testing.M) (int, error) { return 1, fmt.Errorf("error creating participant wallet dir: %v", err) } + tokenGases = &dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas + // ETH swap contract. - token := dexeth.Tokens[testTokenID].NetTokens[dex.Simnet] + masterToken = dexeth.Tokens[testTokenID] + token := masterToken.NetTokens[dex.Simnet] fmt.Printf("ETH swap contract address is %v\n", dexeth.ContractAddresses[0][dex.Simnet]) fmt.Printf("Token swap contract addr is %v\n", token.SwapContracts[0].Address) fmt.Printf("Test token contract addr is %v\n", token.Address) @@ -476,6 +492,9 @@ func runSimnet(m *testing.M) (int, error) { } func runTestnet(m *testing.M) (int, error) { + testTokenID = usdcID + masterToken = dexeth.Tokens[testTokenID] + tokenGases = &masterToken.NetTokens[dex.Testnet].SwapContracts[0].Gas if testnetWalletSeed == "" || testnetParticipantWalletSeed == "" { return 1, errors.New("testnet seeds not set") } @@ -554,6 +573,19 @@ func runTestnet(m *testing.M) (int, error) { return 1, fmt.Errorf("error unlocking initiator client: %w", err) } + if simnetTokenContractor, err = newV0TokenContractor(dex.Testnet, usdcID, simnetAddr, ethClient.contractBackend()); err != nil { + return 1, fmt.Errorf("newV0TokenContractor error: %w", err) + } + + // I don't know why this is needed for the participant client but not + // the initiator. Without this, we'll get a bind.ErrNoCode from + // (*BoundContract).Call while calling (*ERC20Swap).TokenAddress. + time.Sleep(time.Second) + + if participantTokenContractor, err = newV0TokenContractor(dex.Testnet, usdcID, participantAddr, participantEthClient.contractBackend()); err != nil { + return 1, fmt.Errorf("participant newV0TokenContractor error: %w", err) + } + code := m.Run() if code != 0 { @@ -664,7 +696,10 @@ func prepareTokenClients(t *testing.T) { if err != nil { t.Fatalf("initiator unlock error; %v", err) } - txOpts, _ := ethClient.txOpts(ctx, 0, tokenGases.Approve, nil) + txOpts, err := ethClient.txOpts(ctx, 0, tokenGases.Approve, nil) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } var tx1, tx2 *types.Transaction if tx1, err = simnetTokenContractor.approve(txOpts, unlimitedAllowance); err != nil { t.Fatalf("initiator approveToken error: %v", err) @@ -674,13 +709,15 @@ func prepareTokenClients(t *testing.T) { t.Fatalf("participant unlock error; %v", err) } - txOpts, _ = participantEthClient.txOpts(ctx, 0, tokenGases.Approve, nil) + txOpts, err = participantEthClient.txOpts(ctx, 0, tokenGases.Approve, nil) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } if tx2, err = participantTokenContractor.approve(txOpts, unlimitedAllowance); err != nil { t.Fatalf("participant approveToken error: %v", err) } - time.Sleep(1) // Give txs time to propagate. - if err := waitForMined(participantEthClient, 8, true); err != nil { + if err := waitForMined(t, 8, true); err != nil { t.Fatalf("unexpected error while waiting to mine approval block: %v", err) } @@ -844,7 +881,19 @@ func testTokenBalance(t *testing.T) { if bal == nil { t.Fatalf("empty balance") } - spew.Dump(bal) + + fmt.Println("### Balance:", simnetAddr, stringifyTokenBalance(t, simnetTokenContractor, bal)) +} + +func stringifyTokenBalance(t *testing.T, ci tokenContractor, evmBal *big.Int) string { + t.Helper() + atomicBal := masterToken.EVMToAtomic(evmBal) + ui, err := asset.UnitInfo(testTokenID) + if err != nil { + t.Fatalf("cannot get unit info: %v", err) + } + prec := math.Round(math.Log10(float64(ui.Conventional.ConversionFactor))) + return strconv.FormatFloat(float64(atomicBal)/float64(ui.Conventional.ConversionFactor), 'f', int(prec), 64) } // testAddressesHaveFundsFn returns a function that tests that addresses used @@ -893,7 +942,10 @@ func testSendTransaction(t *testing.T) { t.Fatalf("no CoinNotFoundError") } - txOpts, _ := ethClient.txOpts(ctx, 1, defaultSendGasLimit, nil) + txOpts, err := ethClient.txOpts(ctx, 1, defaultSendGasLimit, nil) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } tx, err := ethClient.sendTransaction(ctx, txOpts, participantAddr, nil) if err != nil { @@ -912,7 +964,7 @@ func testSendTransaction(t *testing.T) { } spew.Dump(tx) - if err := waitForMined(ethClient, 10, false); err != nil { + if err := waitForMined(t, 10, false); err != nil { t.Fatal(err) } @@ -1009,7 +1061,7 @@ func testSendSignedTransaction(t *testing.T) { } spew.Dump(tx) - if err := waitForMined(ethClient, 10, false); err != nil { + if err := waitForMined(t, 10, false); err != nil { t.Fatal(err) } @@ -1023,12 +1075,15 @@ func testSendSignedTransaction(t *testing.T) { } func testTransactionReceipt(t *testing.T) { - txOpts, _ := ethClient.txOpts(ctx, 1, defaultSendGasLimit, nil) + txOpts, err := ethClient.txOpts(ctx, 1, defaultSendGasLimit, nil) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } tx, err := ethClient.sendTransaction(ctx, txOpts, simnetAddr, nil) if err != nil { t.Fatal(err) } - if err := waitForMined(ethClient, 10, false); err != nil { + if err := waitForMined(t, 10, false); err != nil { t.Fatal(err) } receipt, err := waitForReceipt(t, ethClient, tx) @@ -1077,7 +1132,11 @@ func testInitiateGas(t *testing.T, assetID uint32) { c = simnetTokenContractor } - gases := gases(assetID, 0, dex.Simnet) + net := dex.Simnet + if isTestnet { + net = dex.Testnet + } + gases := gases(assetID, 0, net) var previousGas uint64 maxSwaps := 50 @@ -1154,7 +1213,6 @@ func initiateOverflow(c *contractorV0, txOpts *bind.TransactOpts, contracts []*a val = big.NewInt(2) val.Exp(val, big.NewInt(256), nil) val.Sub(val, c.evmify(1)) - fmt.Println(val) } inits = append(inits, swapv0.ETHSwapInitiation{ RefundTimestamp: big.NewInt(int64(contract.LockTime)), @@ -1179,12 +1237,15 @@ func testInitiate(t *testing.T, assetID uint32) { return ethClient.addressBalance(ctx, ethClient.address()) } gases := ethGases + evmify := dexeth.GweiToWei if !isETH { sc = simnetTokenContractor balance = func() (*big.Int, error) { return simnetTokenContractor.balance(ctx) } gases = tokenGases + tc := sc.(*tokenContractorV0) + evmify = tc.evmify } // Create a slice of random secret hashes that can be used in the tests and @@ -1303,7 +1364,10 @@ func testInitiate(t *testing.T, assetID uint32) { } expGas := gases.SwapN(len(test.swaps)) - txOpts, _ := ethClient.txOpts(ctx, optsVal, expGas, dexeth.GweiToWei(maxFeeRate)) + txOpts, err := ethClient.txOpts(ctx, optsVal, expGas, dexeth.GweiToWei(maxFeeRate)) + if err != nil { + t.Fatalf("%s: txOpts error: %v", test.name, err) + } var tx *types.Transaction if test.overflow { switch c := sc.(type) { @@ -1317,12 +1381,13 @@ func testInitiate(t *testing.T, assetID uint32) { } if err != nil { if test.swapErr { + voidUnusedNonce(sc) continue } t.Fatalf("%s: initiate error: %v", test.name, err) } - if err := waitForMined(ethClient, 10, false); err != nil { + if err := waitForMined(t, 10, false); err != nil { t.Fatalf("%s: post-initiate mining error: %v", test.name, err) } @@ -1349,7 +1414,7 @@ func testInitiate(t *testing.T, assetID uint32) { wantBal := new(big.Int).Set(originalBal) if test.success { - wantBal.Sub(wantBal, dexeth.GweiToWei(totalVal)) + wantBal.Sub(wantBal, evmify(totalVal)) } bal, err := balance() if err != nil { @@ -1365,16 +1430,16 @@ func testInitiate(t *testing.T, assetID uint32) { } wantParentBal := new(big.Int).Sub(originalParentBal, txFee) diff := new(big.Int).Sub(wantParentBal, parentBal) - if diff.CmpAbs(dexeth.GweiToWei(1)) >= 0 { // Ugh. Need to get to != 0 again. - t.Fatalf("%s: unexpected parent chain balance change: want %d got %d, diff = %.9f", - test.name, dexeth.WeiToGwei(wantParentBal), dexeth.WeiToGwei(parentBal), float64(diff.Int64())/dexeth.GweiFactor) + if len(diff.Bits()) != 0 { + t.Fatalf("%s: unexpected parent chain balance change: want %d got %d, diff = %d", + test.name, wantParentBal, parentBal, diff) } } diff := new(big.Int).Sub(wantBal, bal) - if diff.CmpAbs(new(big.Int)) != 0 { - t.Fatalf("%s: unexpected balance change: want %d got %d gwei, diff = %.9f gwei", - test.name, dexeth.WeiToGwei(wantBal), dexeth.WeiToGwei(bal), float64(diff.Int64())/dexeth.GweiFactor) + if len(diff.Bits()) != 0 { + t.Fatalf("%s: unexpected balance change: want %d got %d units, diff = %d units", + test.name, wantBal, bal, diff) } for _, tSwap := range test.swaps { @@ -1432,12 +1497,15 @@ func testRedeemGas(t *testing.T, assetID uint32) { pc = participantTokenContractor } - txOpts, _ := ethClient.txOpts(ctx, optsVal, gases.SwapN(len(swaps)), dexeth.GweiToWei(maxFeeRate)) + txOpts, err := ethClient.txOpts(ctx, optsVal, gases.SwapN(len(swaps)), dexeth.GweiToWei(maxFeeRate)) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } tx, err := c.initiate(txOpts, swaps) if err != nil { t.Fatalf("Unable to initiate swap: %v ", err) } - if err := waitForMined(ethClient, 8, true); err != nil { + if err := waitForMined(t, 8, true); err != nil { t.Fatalf("unexpected error while waiting to mine: %v", err) } receipt, err := waitForReceipt(t, ethClient, tx) @@ -1508,9 +1576,12 @@ func testRedeem(t *testing.T, assetID uint32) { isETH := assetID == BipID gases := ethGases c, pc := simnetContractor, participantContractor + evmify := dexeth.GweiToWei if !isETH { gases = tokenGases c, pc = simnetTokenContractor, participantTokenContractor + tc := c.(*tokenContractorV0) + evmify = tc.evmify } tests := []struct { @@ -1643,14 +1714,17 @@ func testRedeem(t *testing.T, assetID uint32) { } } - txOpts, _ := test.redeemerClient.txOpts(ctx, optsVal, gases.SwapN(len(test.swaps)), dexeth.GweiToWei(maxFeeRate)) + txOpts, err := test.redeemerClient.txOpts(ctx, optsVal, gases.SwapN(len(test.swaps)), dexeth.GweiToWei(maxFeeRate)) + if err != nil { + t.Fatalf("%s: txOpts error: %v", test.name, err) + } tx, err := test.redeemerContractor.initiate(txOpts, test.swaps) if err != nil { t.Fatalf("%s: initiate error: %v ", test.name, err) } // This waitForMined will always take test.sleepNBlocks to complete. - if err := waitForMined(test.redeemerClient, test.sleepNBlocks, true); err != nil { + if err := waitForMined(t, test.sleepNBlocks, true); err != nil { t.Fatalf("%s: post-init mining error: %v", test.name, err) } @@ -1700,9 +1774,13 @@ func testRedeem(t *testing.T, assetID uint32) { } expGas := gases.RedeemN(len(test.redemptions)) - txOpts, _ = test.redeemerClient.txOpts(ctx, 0, expGas, dexeth.GweiToWei(maxFeeRate)) + txOpts, err = test.redeemerClient.txOpts(ctx, 0, expGas, dexeth.GweiToWei(maxFeeRate)) + if err != nil { + t.Fatalf("%s: txOpts error: %v", test.name, err) + } tx, err = test.redeemerContractor.redeem(txOpts, test.redemptions) if test.expectRedeemErr { + voidUnusedNonce(test.redeemerContractor) if err == nil { t.Fatalf("%s: expected error but did not get", test.name) } @@ -1713,7 +1791,7 @@ func testRedeem(t *testing.T, assetID uint32) { } spew.Dump(tx) - if err := waitForMined(test.redeemerClient, 10, false); err != nil { + if err := waitForMined(t, 10, false); err != nil { t.Fatalf("%s: post-redeem mining error: %v", test.name, err) } @@ -1755,7 +1833,7 @@ func testRedeem(t *testing.T, assetID uint32) { wantBal := new(big.Int).Set(originalBal) if test.addAmt { - wantBal.Add(wantBal, dexeth.GweiToWei(uint64(len(test.redemptions)))) + wantBal.Add(wantBal, evmify(uint64(len(test.redemptions)))) } if isETH { @@ -1767,16 +1845,16 @@ func testRedeem(t *testing.T, assetID uint32) { } wantParentBal := new(big.Int).Sub(originalParentBal, txFee) diff := new(big.Int).Sub(wantParentBal, parentBal) - if diff.CmpAbs(dexeth.GweiToWei(1)) >= 0 { + if len(diff.Bits()) != 0 { t.Fatalf("%s: unexpected parent chain balance change: want %d got %d, diff = %d", - test.name, dexeth.WeiToGwei(wantParentBal), dexeth.WeiToGwei(parentBal), dexeth.WeiToGwei(diff)) + test.name, wantParentBal, parentBal, diff) } } diff := new(big.Int).Sub(wantBal, bal) - if diff.CmpAbs(new(big.Int)) != 0 { - t.Fatalf("%s: unexpected balance change: want %d got %d, diff = %.9f", - test.name, dexeth.WeiToGwei(wantBal), dexeth.WeiToGwei(bal), float64(diff.Int64())/dexeth.GweiFactor) + if len(diff.Bits()) != 0 { + t.Fatalf("%s: unexpected balance change: want %d got %d, diff = %d", + test.name, wantBal, bal, diff) } for i, redemption := range test.redemptions { @@ -1815,12 +1893,15 @@ func testRefundGas(t *testing.T, assetID uint32) { lockTime := uint64(time.Now().Unix()) - txOpts, _ := ethClient.txOpts(ctx, optsVal, gases.SwapN(1), nil) - _, err := c.initiate(txOpts, []*asset.Contract{newContract(lockTime, secretHash, 1)}) + txOpts, err := ethClient.txOpts(ctx, optsVal, gases.SwapN(1), nil) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } + _, err = c.initiate(txOpts, []*asset.Contract{newContract(lockTime, secretHash, 1)}) if err != nil { t.Fatalf("Unable to initiate swap: %v ", err) } - if err := waitForMined(ethClient, 8, true); err != nil { + if err := waitForMined(t, 8, true); err != nil { t.Fatalf("unexpected error while waiting to mine: %v", err) } @@ -1858,9 +1939,12 @@ func testRefund(t *testing.T, assetID uint32) { gases := ethGases c, pc := simnetContractor, participantContractor + evmify := dexeth.GweiToWei if !isETH { gases = tokenGases c, pc = simnetTokenContractor, participantTokenContractor + tc := c.(*tokenContractorV0) + evmify = tc.evmify } sleepForNBlocks := 8 tests := []struct { @@ -1938,14 +2022,17 @@ func testRefund(t *testing.T, assetID uint32) { inLocktime := uint64(time.Now().Add(test.addTime).Unix()) - txOpts, _ := ethClient.txOpts(ctx, optsVal, gases.SwapN(1), nil) + txOpts, err := ethClient.txOpts(ctx, optsVal, gases.SwapN(1), nil) + if err != nil { + t.Fatalf("%s: txOpts error: %v", test.name, err) + } _, err = c.initiate(txOpts, []*asset.Contract{newContract(inLocktime, secretHash, amt)}) if err != nil { t.Fatalf("%s: initiate error: %v ", test.name, err) } if test.redeem { - if err := waitForMined(ethClient, sleepForNBlocks, false); err != nil { + if err := waitForMined(t, sleepForNBlocks, false); err != nil { t.Fatalf("%s: pre-redeem mining error: %v", test.name, err) } @@ -1957,7 +2044,7 @@ func testRefund(t *testing.T, assetID uint32) { } // This waitForMined will always take test.sleep to complete. - if err := waitForMined(participantEthClient, sleepForNBlocks, true); err != nil { + if err := waitForMined(t, sleepForNBlocks, true); err != nil { t.Fatalf("unexpected post-init mining error for test %v: %v", test.name, err) } @@ -1983,7 +2070,10 @@ func testRefund(t *testing.T, assetID uint32) { test.name, test.isRefundable, isRefundable) } - txOpts, _ = test.refunderClient.txOpts(ctx, 0, gases.Refund, nil) + txOpts, err = test.refunderClient.txOpts(ctx, 0, gases.Refund, dexeth.GweiToWei(maxFeeRate)) + if err != nil { + t.Fatalf("%s: txOpts error: %v", test.name, err) + } tx, err := test.refunderContractor.refund(txOpts, secretHash) if err != nil { t.Fatalf("%s: refund error: %v", test.name, err) @@ -1996,7 +2086,7 @@ func testRefund(t *testing.T, assetID uint32) { t.Fatalf("%s: unexpected pending in balance %d", test.name, in) } - if err := waitForMined(test.refunderClient, 10, false); err != nil { + if err := waitForMined(t, 10, false); err != nil { t.Fatalf("%s: post-refund mining error: %v", test.name, err) } @@ -2007,27 +2097,27 @@ func testRefund(t *testing.T, assetID uint32) { spew.Dump(receipt) err = checkTxStatus(receipt, txOpts.GasLimit) - // test.addAmt being true indicates the refund shoud succeed. + // test.addAmt being true indicates the refund should succeed. if err != nil && test.addAmt { t.Fatalf("%s: failed refund transaction status: %v", test.name, err) } - fmt.Printf("Gas used for refund, success = %t: %d \n", test.addAmt, receipt.GasUsed) + fmt.Printf("Gas used for refund, success = %t: %d\n", test.addAmt, receipt.GasUsed) // Balance should increase or decrease by a certain amount - // depending on whether redeem completed successfully on-chain. + // depending on whether refund completed successfully on-chain. // If unsuccessful the fee is subtracted. If successful, amt is // added. gasPrice, err := feesAtBlk(ctx, ethClient, receipt.BlockNumber.Int64()) if err != nil { t.Fatalf("%s: feesAtBlk error: %v", test.name, err) } + fmt.Printf("Gas price for refund: %d\n", gasPrice) bigGasUsed := new(big.Int).SetUint64(receipt.GasUsed) txFee := new(big.Int).Mul(bigGasUsed, gasPrice) wantBal := new(big.Int).Set(originalBal) if test.addAmt { - wantBal.Add(wantBal, dexeth.GweiToWei(amt)) - + wantBal.Add(wantBal, evmify(amt)) } if isETH { @@ -2035,13 +2125,13 @@ func testRefund(t *testing.T, assetID uint32) { } else { parentBal, err := test.refunderClient.addressBalance(ctx, test.refunderClient.address()) if err != nil { - t.Fatalf("%s: post-redeem eth balance error: %v", test.name, err) + t.Fatalf("%s: post-refund eth balance error: %v", test.name, err) } wantParentBal := new(big.Int).Sub(originalParentBal, txFee) diff := new(big.Int).Sub(wantParentBal, parentBal) - if diff.CmpAbs(dexeth.GweiToWei(1)) >= 0 { + if len(diff.Bits()) != 0 { t.Fatalf("%s: unexpected parent chain balance change: want %d got %d, diff = %d", - test.name, dexeth.WeiToGwei(wantParentBal), dexeth.WeiToGwei(parentBal), dexeth.WeiToGwei(diff)) + test.name, wantParentBal, parentBal, diff) } } @@ -2051,9 +2141,9 @@ func testRefund(t *testing.T, assetID uint32) { } diff := new(big.Int).Sub(wantBal, bal) - if diff.CmpAbs(dexeth.GweiToWei(1)) >= 0 { + if len(diff.Bits()) != 0 { t.Fatalf("%s: unexpected balance change: want %d got %d, diff = %d", - test.name, dexeth.WeiToGwei(wantBal), dexeth.WeiToGwei(bal), dexeth.WeiToGwei(diff)) + test.name, wantBal, bal, diff) } swap, err = test.refunderContractor.swap(ctx, secretHash) @@ -2072,7 +2162,16 @@ func testApproveAllowance(t *testing.T) { t.Fatal(err) } - if err := waitForMined(ethClient, 10, false); err != nil { + txOpts, err := ethClient.txOpts(ctx, 0, tokenGases.Approve, nil) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } + + if _, err = simnetTokenContractor.approve(txOpts, unlimitedAllowance); err != nil { + t.Fatalf("initiator approveToken error: %v", err) + } + + if err := waitForMined(t, 10, false); err != nil { t.Fatalf("post approve mining error: %v", err) } @@ -2091,7 +2190,7 @@ func testTransferGas(t *testing.T) { if err != nil { t.Fatalf("unlock error: %v", err) } - gas, err := simnetTokenContractor.estimateTransferGas(ctx, dexeth.GweiToWei(1e8)) + gas, err := simnetTokenContractor.estimateTransferGas(ctx, simnetTokenContractor.(*tokenContractorV0).contractorV0.evmify(1)) if err != nil { t.Fatalf("estimateTransferGas error: %v", err) } @@ -2099,7 +2198,7 @@ func testTransferGas(t *testing.T) { } func testApproveGas(t *testing.T) { - gas, err := simnetTokenContractor.estimateApproveGas(ctx, dexeth.GweiToWei(1e8)) + gas, err := simnetTokenContractor.estimateApproveGas(ctx, simnetTokenContractor.(*tokenContractorV0).contractorV0.evmify(1)) if err != nil { t.Fatalf("") } @@ -2128,7 +2227,7 @@ func TestReplayAttack(t *testing.T) { if err != nil { t.Fatal(err) } - if err := waitForMined(cl, 10, false); err != nil { + if err := waitForMined(t, 10, false); err != nil { t.Fatal(err) } @@ -2148,13 +2247,16 @@ func TestReplayAttack(t *testing.T) { if i != 4 { inLocktime := uint64(time.Now().Add(time.Hour).Unix()) - txOpts, _ := cl.txOpts(ctx, 1, ethGases.SwapN(1), nil) + txOpts, err := cl.txOpts(ctx, 1, ethGases.SwapN(1), nil) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } _, err = simnetContractor.initiate(txOpts, []*asset.Contract{newContract(inLocktime, secretHash, 1)}) if err != nil { t.Fatalf("unable to initiate swap: %v ", err) } - if err := waitForMined(cl, 10, false); err != nil { + if err := waitForMined(t, 10, false); err != nil { t.Fatal(err) } continue @@ -2167,17 +2269,23 @@ func TestReplayAttack(t *testing.T) { // Set some variables in the contract used for the exploit. This // will fail (silently) due to require(msg.origin == msg.sender) // in the real contract. - txOpts, _ := cl.txOpts(ctx, 1, defaultSendGasLimit*5, nil) - _, err := reentryContract.SetUsUpTheBomb(txOpts, ethSwapContractAddr, secretHash, big.NewInt(inLocktime), participantAddr) + txOpts, err := cl.txOpts(ctx, 1, defaultSendGasLimit*5, nil) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } + _, err = reentryContract.SetUsUpTheBomb(txOpts, ethSwapContractAddr, secretHash, big.NewInt(inLocktime), participantAddr) if err != nil { t.Fatalf("unable to set up the bomb: %v", err) } - if err = waitForMined(cl, 10, false); err != nil { + if err = waitForMined(t, 10, false); err != nil { t.Fatal(err) } } - txOpts, _ = cl.txOpts(ctx, 1, defaultSendGasLimit*5, nil) + txOpts, err = cl.txOpts(ctx, 1, defaultSendGasLimit*5, nil) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } txOpts.Value = nil // Siphon funds into the contract. tx, err := reentryContract.AllYourBase(txOpts) @@ -2185,13 +2293,13 @@ func TestReplayAttack(t *testing.T) { t.Fatalf("unable to get all your base: %v", err) } spew.Dump(tx) - if err = waitForMined(ethClient, 10, false); err != nil { + if err = waitForMined(t, 10, false); err != nil { t.Fatal(err) } receipt, err := waitForReceipt(t, ethClient, tx) spew.Dump(receipt) - if err = waitForMined(cl, 10, false); err != nil { + if err = waitForMined(t, 10, false); err != nil { t.Fatal(err) } @@ -2201,12 +2309,15 @@ func TestReplayAttack(t *testing.T) { } // Send the siphoned funds to us. - txOpts, _ = cl.txOpts(ctx, 1, defaultSendGasLimit*5, nil) + txOpts, err = cl.txOpts(ctx, 1, defaultSendGasLimit*5, nil) + if err != nil { + t.Fatalf("txOpts error: %v", err) + } tx, err = reentryContract.AreBelongToUs(txOpts) if err != nil { t.Fatalf("unable to are belong to us: %v", err) } - if err = waitForMined(ethClient, 10, false); err != nil { + if err = waitForMined(t, 10, false); err != nil { t.Fatal(err) } receipt, err = waitForReceipt(t, ethClient, tx) @@ -2383,11 +2494,14 @@ func testSignMessage(t *testing.T) { func TestTokenGasEstimates(t *testing.T) { prepareTokenClients(t) - if err := GetGasEstimates(ctx, ethClient, simnetTokenContractor, 5, tokenGases, func() { - if err := waitForMined(ethClient, 10, false); err != nil { - t.Fatalf("mining error: %v", err) - } - }); err != nil { + if err := GetGasEstimates(ctx, ethClient, simnetTokenContractor, 5, tokenGases, participantAddr, + func() { + if err := waitForMined(t, 10, false); err != nil { + t.Fatalf("mining error: %v", err) + } + }, func(nc ethFetcher, tx *types.Transaction) (*types.Receipt, error) { + return waitForReceipt(t, nc, tx) + }); err != nil { t.Fatalf("getGasEstimates error: %v", err) } } @@ -2401,11 +2515,15 @@ func TestConfirmedNonce(t *testing.T) { func TestTokenGasEstimatesParticipant(t *testing.T) { prepareTokenClients(t) - if err := GetGasEstimates(ctx, participantEthClient, participantTokenContractor, 5, tokenGases, func() { - if err := waitForMined(participantEthClient, 10, false); err != nil { - t.Fatalf("mining error: %v", err) - } - }); err != nil { + if err := GetGasEstimates(ctx, participantEthClient, participantTokenContractor, 5, tokenGases, simnetAddr, + func() { + if err := waitForMined(t, 10, false); err != nil { + t.Fatalf("mining error: %v", err) + } + }, + func(nc ethFetcher, tx *types.Transaction) (*types.Receipt, error) { + return waitForReceipt(t, nc, tx) + }); err != nil { t.Fatalf("getGasEstimates error: %v", err) } } @@ -2428,3 +2546,16 @@ func exportAccountsFromNode(node *node.Node) ([]accounts.Account, error) { } return ks.Accounts(), nil } + +func voidUnusedNonce(sc interface{}) { + switch c := sc.(type) { + case *contractorV0: + if mRPC, is := c.cb.(*multiRPCClient); is { + mRPC.voidUnusedNonce() + } + case *tokenContractorV0: + if mRPC, is := c.cb.(*multiRPCClient); is { + mRPC.voidUnusedNonce() + } + } +} diff --git a/client/core/bookie.go b/client/core/bookie.go index 67f04a7ee7..6d28988e84 100644 --- a/client/core/bookie.go +++ b/client/core/bookie.go @@ -977,14 +977,17 @@ func (dc *dexConnection) subPriceFeed() { // handlePriceUpdateNote handles the price_update note that is part of the // price feed. -func handlePriceUpdateNote(_ *Core, dc *dexConnection, msg *msgjson.Message) error { +func handlePriceUpdateNote(c *Core, dc *dexConnection, msg *msgjson.Message) error { spot := new(msgjson.Spot) if err := msg.Unmarshal(spot); err != nil { return fmt.Errorf("error unmarshaling price update: %v", err) } mktName, err := dex.MarketName(spot.BaseID, spot.QuoteID) if err != nil { - return fmt.Errorf("error parsing market for base = %d, quote = %d: %v", spot.BaseID, spot.QuoteID, err) + // It's possible for the dex server to have a market with coin + // ID's we do not know, especially in the case of eth tokens. + c.log.Debugf("error parsing market for base = %d, quote = %d: %v", spot.BaseID, spot.QuoteID, err) + return nil } dc.spotsMtx.Lock() dc.spots[mktName] = spot diff --git a/client/core/core.go b/client/core/core.go index ea83c9d581..1852e531bc 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -1748,8 +1748,8 @@ func (c *Core) connectAndUpdateWallet(w *xcWallet) error { } if !parentWallet.connected() { if err := c.connectAndUpdateWallet(parentWallet); err != nil { - return fmt.Errorf("failed to connect %s parent wallet for %s token", - unbip(token.ParentID), unbip(assetID)) + return fmt.Errorf("failed to connect %s parent wallet for %s token: %v", + unbip(token.ParentID), unbip(assetID), err) } } } diff --git a/client/core/exchangeratefetcher.go b/client/core/exchangeratefetcher.go index 337b3946da..aff2933cdb 100644 --- a/client/core/exchangeratefetcher.go +++ b/client/core/exchangeratefetcher.go @@ -120,7 +120,9 @@ func newCommonRateSource(fetcher rateFetcher) *commonRateSource { func fetchCoinpaprikaRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 { fiatRates := make(map[uint32]float64) for assetID, sa := range assets { - if sa.Wallet == nil { + // TODO: asset.Info will be nil for tokens. Make this work for + // tokens. + if sa.Wallet == nil || sa.Info == nil { // we don't want to fetch rates for assets with no wallet. continue } diff --git a/client/webserver/site/src/img/coins/usdc.eth.png b/client/webserver/site/src/img/coins/usdc.eth.png new file mode 100644 index 0000000000000000000000000000000000000000..dd63a58a2153241c714d0ea9378129da76e1ce64 GIT binary patch literal 4537 zcmV;q5k~HbP)EX>4Tx04R}tkv&MmKpe$i(@LdOMC>5qkfAzR5EXIMDionYs1;guFuC*#ni!H4 z7e~Rh;NZt%)xpJCR|i)?5c~jfbaGO3krMxx6k5c1aNLh~_a1le0HIN3niU!YG~G5c zsic_8uZZDSL=Z$6eF(|SGG-+y4d3x~j{slq5;;BvB z;Ji;9V`W(-J|`YE>4LKlt6PRh$_2lA=kV>&06}?{H=(}=qZr1_%5sbosLiQUNO->8W)>?7eSqcE? zy7d6VGhr!XpRKi`=UfEMG4Uein*s;Py-yLz7Rix}Mx~P&B!{uI|~=L}GJL78oVPI?kSLNpp)6iOoS*_iSmw09jw#oRH;BovpQQ0bw@ku-9ZZ z>b8J6TWggB3f^B|+ngu}AdZfyK^b7!Ee6~???BJFioAf3sYdb{%WTx4=UfGDo_D}* zF-QT$(J?hB6_DzV88@Zy$dHPW&z-O6aTX1X2BiU_WvseNGe$mlN|6>y;i>MJap&{9 zIk%{zbGDq))c*i5icG4vr2_5Eb{O>e31uvv#J~UUPZ;yb%io1#n~24eaJ}~zdCkM1 zSJ33N;N2qz;dxMke|Y=UKYtaTyb%@hvK$@L6O_^qMGW`Qc?TTFOnLo&D6)YsM}rvm zMdW%tFU{$+(}Ii5_Pia5=|Bt@y!VCXLkU8td;Te&B`DYWs%<7zzGH;_hyf+VdW2(}n#XvS zpqL88Fz$=uZ+}_}Gb0wUnu~DCaad%|#$+OOEd2XlkK`>Oj?UR~n%sOKGa`PO#(hzA z-3kav#r9N4hD1{>CB-`QoU7bQ-LPzmNCtuEiYl^YhO@gGsm*TQFS>l@cup{eCYOZ` zWVOxo3K#&;bFKo_HWT-_tLsTz@ZQJJadW%X3Qzr*zmY2K2O4`J*{DNrj*X~AN_I*}D$&NP zs#tbo+!vLiruK@i+Y35d4FY{b^DG|m62+O#IA{*$s6_6G$oUOH@x21yl zofn(!@SG}>lN{Vqw*iDA8(chKi^)!&lNV9>j5|eB!&=8{Z?-c7s@5BROfANQm|?dV zaPj9hxNEd0`lbUhd^s8vo&x~XSD7*NO9wN+GB6rMES^*~@I2}I?Yvijbe)_hXLYgJ&YZYN;O9A2#w`_UjD5h03;I$&C*eAGpS}sLy--5?=B13jINK^jPpNU!`Qtj{Gm0h z#uF$p>fx}N(DbuXW)96=W5w;6u#hVC-d%>L;b_+N+boArZ8KpZw5Dmy=NQcCpZ~v6 zrtP>b7;jwsxs3_Ll{=5JGIY!r!N6z`^;PM#h8i%J&m4#Qs!wQ+w`ant&Wz_&89Kk# zoC<8*Qdv9&sM8F;duZ~Bu)bHbORd*)du>yTF|=OytLl2kF_TcfE7uxNbHpSW43v!S z(yFf14DGltDr7tAtIW)(Ip48Y*OSa(l=VINj;|IhnvFUxz@3+8T}m>Qa>0Uq*CQt|L}tZ%5VKJ))w6Q<-$T#2${p_R z?U}GCvzf|g;cZ6?h-J5%2%1g)+oF&&6y&Eq-v zY*eG>6I6DKLDhc9*mZu!7@mfs@>)G5k=mNgMjcl!O`(7$8)83lNb}^hP%!dL^)s90 z5Qb!R6c!keT)fI2vBRa;8lkg{E{mC378r_bs0N*mVY7^f&-CwHU+H zVr;8x{evnq>MKjQkHEQWtoYN@4XI~O-Y7t}OU&?0ct<-?eU%w@ivf0v0UuPE^F9pZ zIaRi&4c@x~NJ+77*B6*}J7dqiv-HoJoECUal`+9F8`IJz+AAhIbD$8Ahq6)|4#66O zEE@Ag@ac6w+P&X%2f2AW3yd-R8eMGy@=yW6k|wngPZV^4vVj>G4Hb00tT9-;QbA-% z56~q5A8Jz(g$1PjrA7e}T|b;h)L$q-v}Z2pk{%#SfVP7tYPqKR$`auP_O?{KQo{YD z)`|<>`&`j>q0=X7Yvv1T$&x}qG~dUe#u%IBkWePJzRH}no^G=o;%-7KB-t$nRo8ne zu&NynkU51aI?aHvN|#MPE9G8b@TvN)I_@M_Je>t^!J`zO1A-}FQ}KoIMEgxR$S2D zY|{jIZ%c)0EE0<+h3fy+Hj`>L+W%O4>OX^l0sus(jL2l1h`t}539I%~0qHY@*E}bf zpf@=!xOv{eY(@3nT^4S2<>8I0ggnRKd8DpP0NN%ub}y>h?&+?v;>-UHap~yQ^(4B! zet-ebUoz}Tx;Pb!CtnG^9E_f^?K@bcna^%9Fu@+X$CC(ECblHR-+OWi1oK+e7AphG zY6s27X&Ct~_bxLVb*QT}@A#Jsj95GkxQKMH);8{oYTt+i^O+;kTj_TNWKiqGyo_Du zuCWT)lFqLm^%ZRA{g!-(CvQaGZuso)0F-N0U22|>-C{s2 zp2Ym4KXWr6@IxHl$z=p18z3O4d?)`~qGItRemDOV7rftNcqYt!eaCh~-tyP)sD9pc z>j8oqHxD7?TmQe+Y+XjPGySe8gGeSn`7BO>zt{R*=~M<`b_rU|Mfip5CzuMvxNd;UFn;on zEBJ-$CtEj+X3ii6euxWsQ#a37qvCC24iz2Z4qO#Ny6*jJ&4h~NsMAk7rsjArPtI2P zQ(M@6>5s~FP002jvy)R*fnffRef2K&sGUK2eNmf8>LZ_gRC}{+&--rctJ0Sy7aWW{ zc*<1dKis|Xvu${(zQFtexNDb~@mIzP7(VRwmJN5kb zk~dQ^1DbTQ|}n}MWr9uyyiKf35bkqD|FmD{gy~x z>OZgUci)wha|%eC2qVUP5xB4Vn89N>=p$p$0OwhrU4id?%z(}5UTZ}@S+dtXYoWBMh8dd+H{>o|hZq<71D{%C43hEEs$ zsJ593a%ovUuB`X&vfM+V1oK9d-p%)?oRoe&LEf8G+w(}Lofb4b?^NW@7pbI0E}uD$ z)%7ICe38PuQ9>|ZY$|HxeTG&1>D7*@K^e~>y$+PN8QRs9R6u?NMIIQ^0096;=XY&1 znf%W;5)7U0A73uT?l~Yp`bM=H$ = { 145: 'bch', 60: 'eth', 133: 'zec', - 60000: 'dextt.eth' + 60000: 'dextt.eth', + 60001: 'usdc.eth' } const BipSymbols = Object.values(BipIDs) diff --git a/client/webserver/site/src/js/markets.ts b/client/webserver/site/src/js/markets.ts index 5620390c1d..d4ed729ce2 100644 --- a/client/webserver/site/src/js/markets.ts +++ b/client/webserver/site/src/js/markets.ts @@ -2530,6 +2530,7 @@ interface BalanceWidgetElement { tmpl: Record iconBox: PageElement stateIcons: WalletIcons + parentBal?: PageElement } /* @@ -2570,7 +2571,6 @@ class BalanceWidget { stateIcons: new WalletIcons(qtmpl.walletState) } qtmpl.balanceRowTmpl.remove() - // this.parentRow = els.parentBalance app().registerNoteFeeder({ balance: (note: BalanceNote) => { this.updateAsset(note.assetID) }, @@ -2663,7 +2663,10 @@ class BalanceWidget { const balTmpl = Doc.parseTemplate(row) balTmpl.title.textContent = title balTmpl.bal.textContent = Doc.formatCoinValue(bal, ui) - if (icon) balTmpl.bal.append(icon) + if (icon) { + balTmpl.bal.append(icon) + tmpl.parentBal = balTmpl.bal + } } addRow(intl.prep(intl.ID_AVAILABLE), bal.available, asset.unitInfo) @@ -2688,9 +2691,8 @@ class BalanceWidget { /* updateParent updates the side's parent asset balance. */ updateParent (side: BalanceWidgetElement) { - const { wallet: { balance }, unitInfo, symbol } = app().assets[side.parentID] + const { wallet: { balance }, unitInfo } = app().assets[side.parentID] side.tmpl.parentBal.textContent = Doc.formatCoinValue(balance.available, unitInfo) - side.tmpl.parentLogo.src = Doc.logoPath(symbol) } /* diff --git a/client/webserver/site/src/js/order.ts b/client/webserver/site/src/js/order.ts index 2828962916..f24c148d76 100644 --- a/client/webserver/site/src/js/order.ts +++ b/client/webserver/site/src/js/order.ts @@ -497,6 +497,21 @@ function setCoinHref (assetID: number, link: PageElement) { link.href = formatter(link.dataset.explorerCoin || '') } +const ethExplorers: Record string> = { + [Mainnet]: (cid: string) => { + if (cid.length === 42) { + return `https://etherscan.io/address/${cid}` + } + return `https://etherscan.io/tx/${cid}` + }, + [Testnet]: (cid: string) => { + if (cid.length === 42) { + return `https://goerli.etherscan.io/address/${cid}` + } + return `https://goerli.etherscan.io/tx/${cid}` + } +} + const CoinExplorers: Record string>> = { 42: { // dcr [Mainnet]: (cid: string) => { @@ -516,20 +531,8 @@ const CoinExplorers: Record string>> = { [Mainnet]: (cid: string) => `https://ltc.bitaps.com/${cid.split(':')[0]}`, [Testnet]: (cid: string) => `https://sochain.com/tx/LTCTEST/${cid.split(':')[0]}` }, - 60: { // eth - [Mainnet]: (cid: string) => { - if (cid.length === 42) { - return `https://etherscan.io/address/${cid}` - } - return `https://etherscan.io/tx/${cid}` - }, - [Testnet]: (cid: string) => { - if (cid.length === 42) { - return `https://goerli.etherscan.io/address/${cid}` - } - return `https://goerli.etherscan.io/tx/${cid}` - } - }, + 60: ethExplorers, + 60001: ethExplorers, 3: { // doge [Mainnet]: (cid: string) => `https://dogeblocks.com/tx/${cid.split(':')[0]}`, [Testnet]: (cid: string) => `https://blockexplorer.one/dogecoin/testnet/tx/${cid.split(':')[0]}` diff --git a/client/webserver/site/src/js/wallets.ts b/client/webserver/site/src/js/wallets.ts index 888fc91bad..abff340899 100644 --- a/client/webserver/site/src/js/wallets.ts +++ b/client/webserver/site/src/js/wallets.ts @@ -231,6 +231,7 @@ export default class WalletsPage extends BasePage { const page = this.page Doc.hide(page.vSendErr, page.sendErr, page.vSendEstimates, page.txFeeNotAvailable) const assetID = parseInt(page.sendForm.dataset.assetID || '') + const token = app().assets[assetID].token const subtract = page.subtractCheckBox.checked || false const conversionFactor = app().unitInfo(assetID).conventional.conversionFactor const value = Math.round(parseFloat(page.sendAmt.value || '') * conversionFactor) @@ -273,7 +274,12 @@ export default class WalletsPage extends BasePage { page.vSendSymbol.textContent = symbol.toUpperCase() page.vSendLogo.src = Doc.logoPath(symbol) - page.vSendFee.textContent = Doc.formatFullPrecision(txfee, ui) + if (token) { + const { unitInfo: feeUI, symbol: feeSymbol } = app().assets[token.parentID] + page.vSendFee.textContent = Doc.formatFullPrecision(txfee, feeUI) + ' ' + feeSymbol + } else { + page.vSendFee.textContent = Doc.formatFullPrecision(txfee, ui) + } this.showFiatValue(assetID, txfee, page.vSendFeeFiat) page.vSendDestinationAmt.textContent = Doc.formatFullPrecision(value - txfee, ui) page.vTotalSend.textContent = Doc.formatFullPrecision(value, ui) @@ -283,13 +289,16 @@ export default class WalletsPage extends BasePage { page.balanceAfterSend.textContent = Doc.formatFullPrecision(bal, ui) this.showFiatValue(assetID, bal, page.balanceAfterSendFiat) Doc.show(page.approxSign) + // NOTE: All tokens take this route because they cannot pay the fee. if (!subtract) { Doc.hide(page.approxSign) page.vSendDestinationAmt.textContent = Doc.formatFullPrecision(value, ui) - const totalSend = value + txfee + let totalSend = value + if (!token) totalSend += txfee page.vTotalSend.textContent = Doc.formatFullPrecision(totalSend, ui) - this.showFiatValue(assetID, value, page.vTotalSendFiat) - const bal = wallet.balance.available - totalSend + this.showFiatValue(assetID, totalSend, page.vTotalSendFiat) + let bal = wallet.balance.available - value + if (!token) bal -= txfee // handle edge cases where bal is not enough to cover totalSend. // we don't want a minus display of user bal. if (bal <= 0) { @@ -925,7 +934,7 @@ export default class WalletsPage extends BasePage { async showSendForm (assetID: number) { const page = this.page const box = page.sendForm - const { wallet, name, unitInfo: ui, symbol } = app().assets[assetID] + const { wallet, name, unitInfo: ui, symbol, token } = app().assets[assetID] Doc.hide(page.toggleSubtract) page.subtractCheckBox.checked = false @@ -956,12 +965,21 @@ export default class WalletsPage extends BasePage { const res = await postJSON('/api/txfee', feeReq) loaded() if (app().checkResponse(res)) { - const canSend = wallet.balance.available - res.txfee + let canSend = wallet.balance.available + if (!token) { + canSend -= res.txfee + } this.maxSend = canSend page.maxSend.textContent = Doc.formatFullPrecision(canSend, ui) this.showFiatValue(assetID, canSend, page.maxSendFiat) - page.maxSendFee.textContent = Doc.formatFullPrecision(res.txfee, ui) - this.showFiatValue(assetID, res.txfee, page.maxSendFeeFiat) + if (token) { + const { unitInfo: feeUI, symbol: feeSymbol } = app().assets[token.parentID] + page.maxSendFee.textContent = Doc.formatFullPrecision(res.txfee, feeUI) + ' ' + feeSymbol + this.showFiatValue(token.parentID, res.txfee, page.maxSendFeeFiat) + } else { + page.maxSendFee.textContent = Doc.formatFullPrecision(res.txfee, ui) + this.showFiatValue(assetID, res.txfee, page.maxSendFeeFiat) + } Doc.show(page.maxSendDisplay) } } diff --git a/dex/bip-id.go b/dex/bip-id.go index c564bc1399..5f8ae43e90 100644 --- a/dex/bip-id.go +++ b/dex/bip-id.go @@ -591,6 +591,7 @@ var bipIDs = map[uint32]string{ 49344: "stash", // ETH reserved token range 60000-60999 60000: "dextt.eth", // DEX test token + 60001: "usdc.eth", // END ETH reserved token range 65536: "keth", 88888: "ryo[c0ban]", diff --git a/dex/networks/erc20/params.go b/dex/networks/erc20/params.go deleted file mode 100644 index df4caffcf7..0000000000 --- a/dex/networks/erc20/params.go +++ /dev/null @@ -1,21 +0,0 @@ -// This code is available on the terms of the project LICENSE.md file, -// also available online at https://blueoakcouncil.org/license/1.0.0. - -//go:build lgpl - -package erc20 - -import ( - "decred.org/dcrdex/dex" - "github.com/ethereum/go-ethereum/common" -) - -var ( - ContractAddresses = map[uint32]map[dex.Network]common.Address{ - 0: { - dex.Mainnet: common.Address{}, - dex.Simnet: common.HexToAddress("0x6b4368d3E41a60e20FF8539C843B3CDB38C8A507"), - dex.Testnet: common.Address{}, - }, - } -) diff --git a/dex/networks/eth/params.go b/dex/networks/eth/params.go index 0248eb6e1b..c45af9b585 100644 --- a/dex/networks/eth/params.go +++ b/dex/networks/eth/params.go @@ -239,6 +239,7 @@ type Redemption struct { } var testTokenID, _ = dex.BipSymbolID("dextt.eth") +var usdcTokenID, _ = dex.BipSymbolID("usdc.eth") // Gases lists the expected gas required for various DEX and wallet operations. // ★★ IMPORTANT ★★ By policy, clients should allow servers to adjust Swap and diff --git a/dex/networks/eth/tokens.go b/dex/networks/eth/tokens.go index a4e52c28cf..a5fd01d84f 100644 --- a/dex/networks/eth/tokens.go +++ b/dex/networks/eth/tokens.go @@ -105,11 +105,82 @@ var Tokens = map[uint32]*Token{ // address to file. Live tests must populate this field. Address: common.Address{}, Gas: Gases{ + // Results from client's GetGasEstimates. + // + // First swap used 171756 gas + // 4 additional swaps averaged 112607 gas each + // [171756 284366 396976 509586 622184] + // First redeem used 63214 gas + // 4 additional redeems averaged 31641 gas each + // [63214 94858 126502 158135 189779] + // Average of 5 refunds: 48127 + // [48127 48127 48127 48127 48127] + // + // Approve is the gas used to call the approve + // method of the contract. For Approve transactions, + // the very first approval for an account-spender + // pair takes more than subsequent approvals. The + // results are repeated for a different account's + // first approvals on the same contract, so it's not + // just the global first. + // Average of 5 approvals: 27365 + // [44465 27365 27365 27365 27365] + // + // The first transfer to an address the contract has + // not seen before will insert a new key into the + // contract's token map. The amount of extra gas + // this consumes seems to depend on the size of the + // map and is not noticeable on simnet. + // Average of 5 transfers: 32540 + // [32540 32540 32540 32540 32540] Swap: 174_000, SwapAdd: 115_000, Redeem: 70_000, RedeemAdd: 33_000, - Refund: 50_000, // [48149 48149 48137 48149 48137] + Refund: 50_000, + Approve: 46_000, + Transfer: 35_000, + }, + }, + }, + }, + }, + }, + usdcTokenID: { + EVMFactor: new(int64), + Token: &dex.Token{ + ParentID: EthBipID, + Name: "USDC", + UnitInfo: dex.UnitInfo{ + AtomicUnit: "microUSD", + Conventional: dex.Denomination{ + Unit: "USDC", + ConversionFactor: 1e6, + }, + }, + }, + NetTokens: map[dex.Network]*NetToken{ + dex.Mainnet: { + Address: common.Address{}, + SwapContracts: map[uint32]*SwapContract{}, + }, + dex.Testnet: { + Address: common.HexToAddress("0x07865c6e87b9f70255377e024ace6630c1eaa37f"), + SwapContracts: map[uint32]*SwapContract{ + 0: { + Address: common.HexToAddress("0x9E493d3766989e701797b9371682B7b94fD8Af9c"), + Gas: Gases{ + // Results from client's GetGasEstimates. + // + // First swap used 170853 gas + // 4 additional swaps averaged 112583 gas each + // [170853 283439 396025 508563 621185] + // First redeem used 83874 gas + // 4 additional redeems averaged 31641 gas each + // [83874 115518 147138 178747 210439] + // Average of 5 refunds: 64334 + // [64337 64337 64337 64337 64325] + // // Approve is the gas used to call the approve // method of the contract. For Approve transactions, // the very first approval for an account-spender @@ -117,12 +188,32 @@ var Tokens = map[uint32]*Token{ // results are repeated for a different account's // first approvals on the same contract, so it's not // just the global first. - Approve: 46_000, // [44465 27365 27365 27365 27365] - Transfer: 27_000, // [24964 24964 24964 24964 24964] + // Average of 5 approvals: 46222 + // [59902 42802 42802 42802 42802] + // + // + // The first transfer to an address the contract has + // not seen before will insert a new key into the + // contract's token map. The amount of extra gas + // this consumes seems to depend on the size of the + // map. + // Average of 5 transfers: 51820 + // [65500 48400 48400 48400 48400] + Swap: 175_000, + SwapAdd: 115_000, + Redeem: 87_000, + RedeemAdd: 33_000, + Refund: 67_000, + Approve: 65_000, + Transfer: 70_000, }, }, }, }, + dex.Simnet: { + Address: common.Address{}, + SwapContracts: map[uint32]*SwapContract{}, + }, }, }, } diff --git a/server/asset/eth/eth.go b/server/asset/eth/eth.go index 99d875787c..8cf0d73ef9 100644 --- a/server/asset/eth/eth.go +++ b/server/asset/eth/eth.go @@ -81,6 +81,7 @@ func init() { }) registerToken(testTokenID, 0) + registerToken(usdcID, 0) } const ( @@ -101,6 +102,7 @@ var ( } testTokenID, _ = dex.BipSymbolID("dextt.eth") + usdcID, _ = dex.BipSymbolID("usdc.eth") ) type driverBase struct { @@ -622,7 +624,7 @@ func (be *AssetBackend) AccountBalance(addrStr string) (uint64, error) { if err != nil { return 0, fmt.Errorf("accountBalance error: %w", err) } - return dexeth.WeiToGweiUint64(bigBal) + return be.atomize(bigBal), nil } // ValidateSignature checks that the pubkey is correct for the address and diff --git a/server/market/balancer.go b/server/market/balancer.go index 9b7e0d8e07..63aa535821 100644 --- a/server/market/balancer.go +++ b/server/market/balancer.go @@ -79,7 +79,7 @@ func NewDEXBalancer(tunnels map[string]PendingAccounter, assets map[uint32]*asse } bb.feeFamily[parentID] = parent.assetInfo for tokenID := range asset.Tokens(parentID) { - if tokenID == assetID { // Don't double count + if tokenID == assetID || assets[tokenID] == nil { // Don't double count continue } bb.feeFamily[tokenID] = &assets[tokenID].Asset diff --git a/server/market/orderrouter.go b/server/market/orderrouter.go index cdd9cb6cfb..c6b96d3cd6 100644 --- a/server/market/orderrouter.go +++ b/server/market/orderrouter.go @@ -421,7 +421,7 @@ func (r *OrderRouter) processTrade(oRecord *orderRecord, tunnel MarketTunnel, as return msgjson.NewError(msgjson.SignatureError, "redeem signature validation failed") } - if !r.sufficientAccountBalance(acctAddr, oRecord.order, &assets.receiving.Asset, assets.receiving.ID, tunnel) { + if !r.sufficientAccountBalance(acctAddr, oRecord.order, assets.receiving.Asset.ID, assets.receiving.ID, tunnel) { return msgjson.NewError(msgjson.FundingError, "insufficient balance") } } @@ -452,7 +452,7 @@ func (r *OrderRouter) processTrade(oRecord *orderRecord, tunnel MarketTunnel, as return msgjson.NewError(msgjson.SignatureError, "signature validation failed") } - if !r.sufficientAccountBalance(acctAddr, oRecord.order, &assets.funding.Asset, assets.receiving.ID, tunnel) { + if !r.sufficientAccountBalance(acctAddr, oRecord.order, assets.funding.Asset.ID, assets.receiving.ID, tunnel) { return msgjson.NewError(msgjson.FundingError, "insufficient balance") } return r.submitOrderToMarket(tunnel, oRecord) @@ -618,22 +618,31 @@ func (r *OrderRouter) processTrade(oRecord *orderRecord, tunnel MarketTunnel, as // sufficientAccountBalance checks that the user's account-based asset balance // is sufficient to support the order, considering the user's other orders and // active matches across all DEX markets. -func (r *OrderRouter) sufficientAccountBalance(accountAddr string, ord order.Order, assetInfo *dex.Asset, redeemAssetID uint32, tunnel MarketTunnel) bool { - assetID := assetInfo.ID +func (r *OrderRouter) sufficientAccountBalance(accountAddr string, ord order.Order, + assetID, redeemAssetID uint32, tunnel MarketTunnel) bool { trade := ord.Trade() - var fundingQty, fundingLots uint64 - var redeems int + // This asset is funding an order when it is either: + // - base asset in a sell order e.g. selling ETH in a ETH-LTC market + // - quote asset in a buy order e.g. buying BTC in a BTC-ETH market + // This asset will be redeemed when it is either: + // - base asset in a buy order e.g. buying ETH in a ETH-LTC market + // - quote asset in a sell order e.g. selling in a BTC-ETH market + + var fundingQty, fundingLots uint64 // when the asset is base in sell order, or quote in buy order + var redeems int // when the asset is base in buy order, or quote in sell order if ord.Base() == assetID { if trade.Sell { fundingQty = trade.Quantity fundingLots = trade.Quantity / tunnel.LotSize() - } else { - if lo, ok := ord.(*order.LimitOrder); ok { - redeems = int(calc.QuoteToBase(lo.Rate, trade.Quantity) / tunnel.LotSize()) - } else { - redeems = int(calc.QuoteToBase(safeMidGap(tunnel), trade.Quantity) / tunnel.LotSize()) + } else { // buying base asset + baseQty := trade.Quantity + if _, ok := ord.(*order.MarketOrder); ok { + // Market buy Quantity is in units of quote asset, so estimate + // how much of base asset that might be based on mid-gap rate. + baseQty = calc.QuoteToBase(safeMidGap(tunnel), trade.Quantity) } + redeems = int(baseQty / tunnel.LotSize()) } } else { if trade.Sell {