diff --git a/.gitignore b/.gitignore index 0819d08c3c..fc8006a250 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ client/tor/build server/cmd/geogame/geogame internal/cmd/xmrswap/xmrswap internal/cmd/xmrswap/config.json +internal/libsecp256k1/secp256k1 +server/cmd/dcrdex/evm-protocol-overrides.json diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index b7d27b3404..7d348b93cc 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -2197,7 +2197,7 @@ func testAvailableFund(t *testing.T, segwit bool, walletType string) { } ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: 0, MaxSwapCount: 1, MaxFeeRate: tBTC.MaxFeeRate, @@ -2762,7 +2762,7 @@ func TestFundEdges(t *testing.T) { unspents := []*ListUnspentResult{p2pkhUnspent} node.listUnspent = unspents ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: swapVal, MaxSwapCount: lots, MaxFeeRate: tBTC.MaxFeeRate, @@ -2986,7 +2986,7 @@ func TestFundEdgesSegwit(t *testing.T) { unspents := []*ListUnspentResult{p2wpkhUnspent} node.listUnspent = unspents ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: swapVal, MaxSwapCount: lots, MaxFeeRate: tBTC.MaxFeeRate, @@ -4365,7 +4365,7 @@ func testPreSwap(t *testing.T, segwit bool, walletType string) { } form := &asset.PreSwapForm{ - Version: version, + AssetVersion: version, LotSize: tLotSize, Lots: lots, MaxFeeRate: tBTC.MaxFeeRate, @@ -4411,8 +4411,8 @@ func testPreRedeem(t *testing.T, segwit bool, walletType string) { defer shutdown() preRedeem, err := wallet.PreRedeem(&asset.PreRedeemForm{ - Version: version, - Lots: 5, + AssetVersion: version, + Lots: 5, }) // Shouldn't actually be any path to error. if err != nil { diff --git a/client/asset/btc/livetest/livetest.go b/client/asset/btc/livetest/livetest.go index ba1859afcc..a2e37fd616 100644 --- a/client/asset/btc/livetest/livetest.go +++ b/client/asset/btc/livetest/livetest.go @@ -266,7 +266,7 @@ func Run(t *testing.T, cfg *Config) { checkAmt("second", rig.secondWallet) ord := &asset.Order{ - Version: 0, + AssetVersion: 0, Value: contractValue * 3, MaxSwapCount: lots * 3, MaxFeeRate: cfg.Asset.MaxFeeRate, diff --git a/client/asset/btc/simnet_test.go b/client/asset/btc/simnet_test.go index 7e70260892..a87058d054 100644 --- a/client/asset/btc/simnet_test.go +++ b/client/asset/btc/simnet_test.go @@ -367,7 +367,7 @@ func testWalletTxBalanceSync(t *testing.T, fromWallet, toWallet *ExchangeWalletF } order := &asset.Order{ - Value: toSatoshi(1), + AssetVersion: toSatoshi(1), FeeSuggestion: 10, MaxSwapCount: 1, MaxFeeRate: 20, diff --git a/client/asset/dcr/dcr_test.go b/client/asset/dcr/dcr_test.go index 6634a7e117..c01ad4a836 100644 --- a/client/asset/dcr/dcr_test.go +++ b/client/asset/dcr/dcr_test.go @@ -876,7 +876,7 @@ func TestAvailableFund(t *testing.T) { } ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: 0, MaxSwapCount: 1, MaxFeeRate: tDCR.MaxFeeRate, @@ -2595,7 +2595,7 @@ func TestFundEdges(t *testing.T) { node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent} ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: swapVal, MaxSwapCount: lots, MaxFeeRate: tDCR.MaxFeeRate, @@ -3871,7 +3871,7 @@ func TestPreSwap(t *testing.T) { node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent} form := &asset.PreSwapForm{ - Version: version, + AssetVersion: version, LotSize: tLotSize, Lots: lots, MaxFeeRate: tDCR.MaxFeeRate, @@ -3913,8 +3913,8 @@ func TestPreRedeem(t *testing.T) { defer shutdown() preRedeem, err := wallet.PreRedeem(&asset.PreRedeemForm{ - Version: version, - Lots: 5, + AssetVersion: version, + Lots: 5, }) // Shouldn't actually be any path to error. if err != nil { diff --git a/client/asset/dcr/simnet_test.go b/client/asset/dcr/simnet_test.go index e2cceb3cb9..25f8fc9eb5 100644 --- a/client/asset/dcr/simnet_test.go +++ b/client/asset/dcr/simnet_test.go @@ -348,7 +348,7 @@ func runTest(t *testing.T, splitTx bool) { } ord := &asset.Order{ - Version: tDCR.Version, + AssetVersion: tDCR.Version, Value: contractValue * 3, MaxSwapCount: lots * 3, MaxFeeRate: tDCR.MaxFeeRate, diff --git a/client/asset/driver.go b/client/asset/driver.go index 2f589c21f9..2600a80801 100644 --- a/client/asset/driver.go +++ b/client/asset/driver.go @@ -16,7 +16,8 @@ import ( // networks enables filtering out tokens via the package's SetNetwork. type nettedToken struct { *Token - addrs map[dex.Network]string + erc20NetAddrs map[dex.Network]string + netSupportedAssetVersions map[dex.Network][]uint32 } var ( @@ -85,7 +86,13 @@ func Register(assetID uint32, driver Driver) { // RegisterToken should be called to register tokens. If no nets are specified // the token will be registered for all networks. The user must invoke // SetNetwork to enable net-based filtering of package function output. -func RegisterToken(tokenID uint32, token *dex.Token, walletDef *WalletDefinition, addrs map[dex.Network]string) { +func RegisterToken( + tokenID uint32, + token *dex.Token, + walletDef *WalletDefinition, + erc20NetAddrs map[dex.Network]string, + netSupportedAssetVersions map[dex.Network][]uint32, +) { driversMtx.Lock() defer driversMtx.Unlock() if _, exists := tokens[tokenID]; exists { @@ -101,7 +108,8 @@ func RegisterToken(tokenID uint32, token *dex.Token, walletDef *WalletDefinition Definition: walletDef, // ContractAddress specified in SetNetwork. }, - addrs: addrs, + erc20NetAddrs: erc20NetAddrs, + netSupportedAssetVersions: netSupportedAssetVersions, } } @@ -262,12 +270,13 @@ func UnitInfo(assetID uint32) (dex.UnitInfo, error) { // network. SetNetwork need only be called once during initialization. func SetNetwork(net dex.Network) { for assetID, nt := range tokens { - addr, exists := nt.addrs[net] + addr, exists := nt.erc20NetAddrs[net] if !exists { delete(tokens, assetID) continue } nt.Token.ContractAddress = addr + nt.Token.SupportedAssetVersions = nt.netSupportedAssetVersions[net] } } diff --git a/client/asset/estimation.go b/client/asset/estimation.go index ad53c5e305..0d0c664eeb 100644 --- a/client/asset/estimation.go +++ b/client/asset/estimation.go @@ -38,8 +38,9 @@ type RedeemEstimate struct { // PreSwapForm can be used to get a swap fees estimate. type PreSwapForm struct { - // Version is the asset version. Most backends only support one version. - Version uint32 + // AssetVersion is the server's asset version. Most backends only support + // one version. + AssetVersion uint32 // LotSize is the lot size for the calculation. For quote assets, LotSize // should be based on either the user's limit order rate, or some measure // of the current market rate. @@ -83,8 +84,9 @@ type PreSwap struct { // PreRedeemForm can be used to get a redemption estimate. type PreRedeemForm struct { - // Version is the asset version. Most backends only support one version. - Version uint32 + // AssetVersion is the asset version. Most backends only support one + // version. + AssetVersion uint32 // Lots is the number of lots in the order. Lots uint64 // FeeSuggestion is a suggested fee from the server. diff --git a/client/asset/eth/cmd/getgas/main.go b/client/asset/eth/cmd/getgas/main.go index d02f3bb64e..c9b9722701 100644 --- a/client/asset/eth/cmd/getgas/main.go +++ b/client/asset/eth/cmd/getgas/main.go @@ -48,8 +48,8 @@ func mainErr() error { flag.BoolVar(&useMainnet, "mainnet", false, "use mainnet") flag.BoolVar(&useTestnet, "testnet", false, "use testnet") flag.BoolVar(&useSimnet, "simnet", false, "use simnet") - flag.BoolVar(&trace, "trace", false, "use simnet") - flag.BoolVar(&debug, "debug", false, "use simnet") + flag.BoolVar(&trace, "trace", false, "use trace logging") + flag.BoolVar(&debug, "debug", false, "use debug logging") flag.IntVar(&maxSwaps, "n", 5, "max number of swaps per transaction. minimum is 2. test will run from 2 swap up to n swaps.") flag.StringVar(&chain, "chain", "eth", "symbol of the base chain") flag.StringVar(&token, "token", "", "symbol of the token. if token is not specified, will check gas for base chain") @@ -125,7 +125,8 @@ func mainErr() error { wParams := new(eth.GetGasWalletParams) wParams.BaseUnitInfo = bui - if token != chain { + isToken := token != chain + if isToken { var exists bool tkn, exists := tokens[assetID] if !exists { @@ -139,16 +140,18 @@ func mainErr() error { } swapContract, exists := netToken.SwapContracts[contractVer] if !exists { - return nil, fmt.Errorf("no verion %d contract for %s token on %s network %s", contractVer, tkn.Name, chain, net) + return nil, fmt.Errorf("no version %d contract for %s token on %s network %s", contractVer, tkn.Name, chain, net) } wParams.Gas = &swapContract.Gas } else { wParams.UnitInfo = bui g, exists := gases[contractVer] if !exists { - return nil, fmt.Errorf("no verion %d contract for %s network %s", contractVer, chain, net) + return nil, fmt.Errorf("no version %d contract for %s network %s", contractVer, chain, net) } wParams.Gas = g + } + if !isToken || contractVer == 1 { cs, exists := contracts[contractVer] if !exists { return nil, fmt.Errorf("no version %d base chain swap contract on %s", contractVer, chain) diff --git a/client/asset/eth/contractor.go b/client/asset/eth/contractor.go index 6fa30124c3..b663644b39 100644 --- a/client/asset/eth/contractor.go +++ b/client/asset/eth/contractor.go @@ -4,8 +4,10 @@ package eth import ( + "bytes" "context" "crypto/sha256" + "errors" "fmt" "math/big" "time" @@ -17,6 +19,7 @@ import ( erc20v0 "decred.org/dcrdex/dex/networks/erc20/contracts/v0" dexeth "decred.org/dcrdex/dex/networks/eth" swapv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0" + swapv1 "decred.org/dcrdex/dex/networks/eth/contracts/v1" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -28,19 +31,21 @@ import ( // The intention is that if a new contract is implemented, the contractor // interface itself will not require any updates. type contractor interface { - swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) + status(ctx context.Context, locator []byte) (*dexeth.SwapStatus, error) + vector(ctx context.Context, locator []byte) (*dexeth.SwapVector, error) + statusAndVector(ctx context.Context, locator []byte) (*dexeth.SwapStatus, *dexeth.SwapVector, error) initiate(*bind.TransactOpts, []*asset.Contract) (*types.Transaction, error) redeem(txOpts *bind.TransactOpts, redeems []*asset.Redemption) (*types.Transaction, error) - refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) + refund(opts *bind.TransactOpts, locator []byte) (*types.Transaction, error) estimateInitGas(ctx context.Context, n int) (uint64, error) - estimateRedeemGas(ctx context.Context, secrets [][32]byte) (uint64, error) - estimateRefundGas(ctx context.Context, secretHash [32]byte) (uint64, error) + estimateRedeemGas(ctx context.Context, secrets [][32]byte, locators [][]byte) (uint64, error) + estimateRefundGas(ctx context.Context, locator []byte) (uint64, error) // value checks the incoming or outgoing contract value. This is just the // one of redeem, refund, or initiate values. It is not an error if the // transaction does not pay to the contract, and the values returned in that // case will always be zero. value(context.Context, *types.Transaction) (incoming, outgoing uint64, err error) - isRefundable(secretHash [32]byte) (bool, error) + isRefundable(locator []byte) (bool, error) } // tokenContractor interacts with an ERC20 token contract and a token swap @@ -57,8 +62,11 @@ type tokenContractor interface { estimateTransferGas(context.Context, *big.Int) (uint64, error) } -type contractorConstructor func(contractAddr, addr common.Address, ec bind.ContractBackend) (contractor, error) -type tokenContractorConstructor func(net dex.Network, token *dexeth.Token, acctAddr common.Address, ec bind.ContractBackend) (tokenContractor, error) +type unifiedContractor interface { + tokenContractor(token *dexeth.Token) (tokenContractor, error) +} + +type contractorConstructor func(net dex.Network, contractAddr, acctAddr common.Address, ec bind.ContractBackend) (contractor, error) // contractV0 is the interface common to a version 0 swap contract or version 0 // token swap contract. @@ -70,6 +78,21 @@ type contractV0 interface { IsRefundable(opts *bind.CallOpts, secretHash [32]byte) (bool, error) } +var _ contractV0 = (*swapv0.ETHSwap)(nil) + +type contractV1 interface { + Initiate(opts *bind.TransactOpts, token common.Address, contracts []swapv1.ETHSwapVector) (*types.Transaction, error) + Redeem(opts *bind.TransactOpts, token common.Address, redemptions []swapv1.ETHSwapRedemption) (*types.Transaction, error) + Status(opts *bind.CallOpts, token common.Address, c swapv1.ETHSwapVector) (swapv1.ETHSwapStatus, error) + Refund(opts *bind.TransactOpts, token common.Address, c swapv1.ETHSwapVector) (*types.Transaction, error) + IsRedeemable(opts *bind.CallOpts, token common.Address, c swapv1.ETHSwapVector) (bool, error) + + ContractKey(opts *bind.CallOpts, token common.Address, v swapv1.ETHSwapVector) ([32]byte, error) + Swaps(opts *bind.CallOpts, arg0 [32]byte) ([32]byte, error) +} + +var _ contractV1 = (*swapv1.ETHSwap)(nil) + // contractorV0 is the contractor for contract version 0. // Redeem and Refund methods of swapv0.ETHSwap already have suitable return types. type contractorV0 struct { @@ -88,7 +111,7 @@ var _ contractor = (*contractorV0)(nil) // newV0Contractor is the constructor for a version 0 ETH swap contract. For // token swap contracts, use newV0TokenContractor to construct a // tokenContractorV0. -func newV0Contractor(contractAddr, acctAddr common.Address, cb bind.ContractBackend) (contractor, error) { +func newV0Contractor(_ dex.Network, contractAddr, acctAddr common.Address, cb bind.ContractBackend) (contractor, error) { c, err := swapv0.NewETHSwap(contractAddr, cb) if err != nil { return nil, err @@ -154,6 +177,11 @@ func (c *contractorV0) redeem(txOpts *bind.TransactOpts, redemptions []*asset.Re if secretHashes[secretHash] { return nil, fmt.Errorf("duplicate secret hash %x", secretHash[:]) } + checkHash := sha256.Sum256(secretB) + if checkHash != secretHash { + return nil, errors.New("wrong secret") + } + secretHashes[secretHash] = true redemps = append(redemps, swapv0.ETHSwapRedemption{ @@ -164,7 +192,73 @@ func (c *contractorV0) redeem(txOpts *bind.TransactOpts, redemptions []*asset.Re return c.contractV0.Redeem(txOpts, redemps) } -// swap retrieves the swap info from the read-only swap method. +// status fetches the SwapStatus, which specifies the current state of mutable +// swap data. +func (c *contractorV0) status(ctx context.Context, locator []byte) (*dexeth.SwapStatus, error) { + secretHash, err := dexeth.ParseV0Locator(locator) + if err != nil { + return nil, err + } + swap, err := c.swap(ctx, secretHash) + if err != nil { + return nil, err + } + status := &dexeth.SwapStatus{ + Step: swap.State, + Secret: swap.Secret, + BlockHeight: swap.BlockHeight, + } + return status, nil +} + +// vector generates a SwapVector, containing the immutable data that defines +// the swap. +func (c *contractorV0) vector(ctx context.Context, locator []byte) (*dexeth.SwapVector, error) { + secretHash, err := dexeth.ParseV0Locator(locator) + if err != nil { + return nil, err + } + swap, err := c.swap(ctx, secretHash) + if err != nil { + return nil, err + } + vector := &dexeth.SwapVector{ + From: swap.Initiator, + To: swap.Participant, + Value: swap.Value, + SecretHash: secretHash, + LockTime: uint64(swap.LockTime.Unix()), + } + return vector, nil +} + +// statusAndVector generates both the status and the vector simultaneously. For +// version 0, this is better than calling status and vector separately, since +// each makes an identical call to c.swap. +func (c *contractorV0) statusAndVector(ctx context.Context, locator []byte) (*dexeth.SwapStatus, *dexeth.SwapVector, error) { + secretHash, err := dexeth.ParseV0Locator(locator) + if err != nil { + return nil, nil, err + } + swap, err := c.swap(ctx, secretHash) + if err != nil { + return nil, nil, err + } + vector := &dexeth.SwapVector{ + From: swap.Initiator, + To: swap.Participant, + Value: swap.Value, + SecretHash: secretHash, + LockTime: uint64(swap.LockTime.Unix()), + } + status := &dexeth.SwapStatus{ + Step: swap.State, + Secret: swap.Secret, + BlockHeight: swap.BlockHeight, + } + return status, vector, nil +} + func (c *contractorV0) swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) { callOpts := &bind.CallOpts{ From: c.acctAddr, @@ -188,19 +282,35 @@ 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) (tx *types.Transaction, err error) { +func (c *contractorV0) refund(txOpts *bind.TransactOpts, locator []byte) (tx *types.Transaction, err error) { + secretHash, err := dexeth.ParseV0Locator(locator) + if err != nil { + return nil, err + } + return c.refundImpl(txOpts, secretHash) +} + +func (c *contractorV0) refundImpl(txOpts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { return c.contractV0.Refund(txOpts, secretHash) } // isRefundable exposes the isRefundable method of the swap contract. -func (c *contractorV0) isRefundable(secretHash [32]byte) (bool, error) { +func (c *contractorV0) isRefundable(locator []byte) (bool, error) { + secretHash, err := dexeth.ParseV0Locator(locator) + if err != nil { + return false, err + } + return c.isRefundableImpl(secretHash) +} + +func (c *contractorV0) isRefundableImpl(secretHash [32]byte) (bool, error) { return c.contractV0.IsRefundable(&bind.CallOpts{From: c.acctAddr}, secretHash) } // estimateRedeemGas estimates the gas used to redeem. The secret hashes // supplied must reference existing swaps, so this method can't be used until // the swap is initiated. -func (c *contractorV0) estimateRedeemGas(ctx context.Context, secrets [][32]byte) (uint64, error) { +func (c *contractorV0) estimateRedeemGas(ctx context.Context, secrets [][32]byte, _ [][]byte) (uint64, error) { redemps := make([]swapv0.ETHSwapRedemption, 0, len(secrets)) for _, secret := range secrets { redemps = append(redemps, swapv0.ETHSwapRedemption{ @@ -214,7 +324,15 @@ func (c *contractorV0) estimateRedeemGas(ctx context.Context, secrets [][32]byte // estimateRefundGas estimates the gas used to refund. The secret hashes // supplied must reference existing swaps that are refundable, so this method // can't be used until the swap is initiated and the lock time has expired. -func (c *contractorV0) estimateRefundGas(ctx context.Context, secretHash [32]byte) (uint64, error) { +func (c *contractorV0) estimateRefundGas(ctx context.Context, locator []byte) (uint64, error) { + secretHash, err := dexeth.ParseV0Locator(locator) + if err != nil { + return 0, err + } + return c.estimateRefundGasImpl(ctx, secretHash) +} + +func (c *contractorV0) estimateRefundGasImpl(ctx context.Context, secretHash [32]byte) (uint64, error) { return c.estimateGas(ctx, nil, "refund", secretHash) } @@ -246,7 +364,7 @@ func (c *contractorV0) estimateInitGas(ctx context.Context, n int) (uint64, erro func (c *contractorV0) estimateGas(ctx context.Context, value *big.Int, method string, args ...any) (uint64, error) { data, err := c.abi.Pack(method, args...) if err != nil { - return 0, fmt.Errorf("Pack error: %v", err) + return 0, fmt.Errorf("pack error: %v", err) } return c.cb.EstimateGas(ctx, ethereum.CallMsg{ @@ -275,7 +393,7 @@ func (c *contractorV0) value(ctx context.Context, tx *types.Transaction) (in, ou // incomingValue calculates the value being redeemed for refunded in the tx. func (c *contractorV0) incomingValue(ctx context.Context, tx *types.Transaction) (uint64, error) { - if redeems, err := dexeth.ParseRedeemData(tx.Data(), 0); err == nil { + if redeems, err := dexeth.ParseRedeemDataV0(tx.Data()); err == nil { var redeemed uint64 for _, redeem := range redeems { swap, err := c.swap(ctx, redeem.SecretHash) @@ -286,7 +404,7 @@ func (c *contractorV0) incomingValue(ctx context.Context, tx *types.Transaction) } return redeemed, nil } - secretHash, err := dexeth.ParseRefundData(tx.Data(), 0) + secretHash, err := dexeth.ParseRefundDataV0(tx.Data()) if err != nil { return 0, nil } @@ -299,7 +417,7 @@ func (c *contractorV0) incomingValue(ctx context.Context, tx *types.Transaction) // outgoingValue calculates the value sent in swaps in the tx. func (c *contractorV0) outgoingValue(tx *types.Transaction) (swapped uint64) { - if inits, err := dexeth.ParseInitiateData(tx.Data(), 0); err == nil { + if inits, err := dexeth.ParseInitiateDataV0(tx.Data()); err == nil { for _, init := range inits { swapped += c.atomize(init.Value) } @@ -307,12 +425,84 @@ func (c *contractorV0) outgoingValue(tx *types.Transaction) (swapped uint64) { return } +// erc20Contractor supports the ERC20 ABI. Embedded in token contractors. +type erc20Contractor struct { + cb bind.ContractBackend + tokenContract *erc20.IERC20 + tokenContractAddr common.Address + acctAddr common.Address + swapAddr common.Address +} + +// balance exposes the read-only balanceOf method of the erc20 token contract. +func (c *erc20Contractor) balance(ctx context.Context) (*big.Int, error) { + callOpts := &bind.CallOpts{ + From: c.acctAddr, + Context: ctx, + } + + return c.tokenContract.BalanceOf(callOpts, c.acctAddr) +} + +// allowance exposes the read-only allowance method of the erc20 token contract. +func (c *erc20Contractor) allowance(ctx context.Context) (*big.Int, error) { + callOpts := &bind.CallOpts{ + From: c.acctAddr, + Context: ctx, + } + return c.tokenContract.Allowance(callOpts, c.acctAddr, c.swapAddr) +} + +// approve sends an approve transaction approving the linked contract to call +// transferFrom for the specified amount. +func (c *erc20Contractor) approve(txOpts *bind.TransactOpts, amount *big.Int) (*types.Transaction, error) { + return c.tokenContract.Approve(txOpts, c.swapAddr, amount) +} + +// transfer calls the transfer method of the erc20 token contract. Used for +// sends or withdrawals. +func (c *erc20Contractor) transfer(txOpts *bind.TransactOpts, addr common.Address, amount *big.Int) (*types.Transaction, error) { + return c.tokenContract.Transfer(txOpts, addr, amount) +} + +// estimateApproveGas estimates the gas needed to send an approve tx. +func (c *erc20Contractor) estimateApproveGas(ctx context.Context, amount *big.Int) (uint64, error) { + return estimateGas(ctx, c.acctAddr, c.tokenContractAddr, erc20.ERC20ABI, c.cb, new(big.Int), "approve", c.swapAddr, amount) +} + +// estimateTransferGas estimates the gas needed for a transfer tx. The account +// needs to have > amount tokens to use this method. +func (c *erc20Contractor) estimateTransferGas(ctx context.Context, amount *big.Int) (uint64, error) { + return estimateGas(ctx, c.acctAddr, c.swapAddr, erc20.ERC20ABI, c.cb, new(big.Int), "transfer", c.acctAddr, amount) +} + +func (c *erc20Contractor) parseTransfer(receipt *types.Receipt) (uint64, error) { + var transferredAmt uint64 + for _, log := range receipt.Logs { + if log.Address != c.tokenContractAddr { + continue + } + transfer, err := c.tokenContract.ParseTransfer(*log) + if err != nil { + continue + } + if transfer.To == c.acctAddr { + transferredAmt += transfer.Value.Uint64() + } + } + + if transferredAmt > 0 { + return transferredAmt, nil + } + + return 0, fmt.Errorf("transfer log to %s not found", c.tokenContractAddr) +} + // tokenContractorV0 is a contractor that implements the tokenContractor // methods, providing access to the methods of the token's ERC20 contract. type tokenContractorV0 struct { *contractorV0 - tokenAddr common.Address - tokenContract *erc20.IERC20 + *erc20Contractor } var _ contractor = (*tokenContractorV0)(nil) @@ -361,101 +551,352 @@ func newV0TokenContractor(net dex.Network, token *dexeth.Token, acctAddr common. evmify: token.AtomicToEVM, atomize: token.EVMToAtomic, }, - tokenAddr: tokenAddr, - tokenContract: tokenContract, + + erc20Contractor: &erc20Contractor{ + cb: cb, + tokenContract: tokenContract, + tokenContractAddr: tokenAddr, + acctAddr: acctAddr, + swapAddr: swapContractAddr, + }, }, nil } -// balance exposes the read-only balanceOf method of the erc20 token contract. -func (c *tokenContractorV0) balance(ctx context.Context) (*big.Int, error) { - callOpts := &bind.CallOpts{ - From: c.acctAddr, - Context: ctx, +// value finds incoming or outgoing value for the tx to either the swap contract +// or the erc20 token contract. For the token contract, only transfer and +// transferFrom are parsed. It is not an error if this tx is a call to another +// method of the token contract, but no values will be parsed. +func (c *tokenContractorV0) value(ctx context.Context, tx *types.Transaction) (in, out uint64, err error) { + to := *tx.To() + if to == c.contractAddr { + return c.contractorV0.value(ctx, tx) + } + if to != c.tokenContractAddr { + return 0, 0, nil } - return c.tokenContract.BalanceOf(callOpts, c.acctAddr) + // Consider removing. We'll never be sending transferFrom transactions + // directly. + if sender, _, value, err := erc20.ParseTransferFromData(tx.Data()); err == nil && sender == c.contractorV0.acctAddr { + return 0, c.atomize(value), nil + } + + if _, value, err := erc20.ParseTransferData(tx.Data()); err == nil { + return 0, c.atomize(value), nil + } + + return 0, 0, nil } -// allowance exposes the read-only allowance method of the erc20 token contract. -func (c *tokenContractorV0) allowance(ctx context.Context) (*big.Int, error) { - // See if we support the pending state. - _, pendingUnavailable := c.cb.(*multiRPCClient) - callOpts := &bind.CallOpts{ - Pending: !pendingUnavailable, - From: c.acctAddr, - Context: ctx, +// tokenAddress exposes the token_address immutable address of the token-bound +// swap contract. +func (c *tokenContractorV0) tokenAddress() common.Address { + return c.tokenContractAddr +} + +type contractorV1 struct { + contractV1 + net dex.Network + abi *abi.ABI + tokenAddr common.Address // zero-address for base-chain asset, e.g. ETH, POL + swapContractAddr common.Address + acctAddr common.Address + cb bind.ContractBackend + isToken bool + evmify func(uint64) *big.Int + atomize func(*big.Int) uint64 +} + +var _ contractor = (*contractorV1)(nil) + +func newV1Contractor(net dex.Network, swapContractAddr, acctAddr common.Address, cb bind.ContractBackend) (contractor, error) { + c, err := swapv1.NewETHSwap(swapContractAddr, cb) + if err != nil { + return nil, err } - return c.tokenContract.Allowance(callOpts, c.acctAddr, c.contractAddr) + return &contractorV1{ + contractV1: c, + net: net, + abi: dexeth.ABIs[1], + swapContractAddr: swapContractAddr, + acctAddr: acctAddr, + cb: cb, + atomize: dexeth.WeiToGwei, + evmify: dexeth.GweiToWei, + }, nil } -// 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) (tx *types.Transaction, err error) { - return c.tokenContract.Approve(txOpts, c.contractAddr, amount) +func (c *contractorV1) status(ctx context.Context, locator []byte) (*dexeth.SwapStatus, error) { + v, err := dexeth.ParseV1Locator(locator) + if err != nil { + return nil, err + } + rec, err := c.Status(&bind.CallOpts{From: c.acctAddr}, c.tokenAddr, dexeth.SwapVectorToAbigen(v)) + if err != nil { + return nil, err + } + return &dexeth.SwapStatus{ + Step: dexeth.SwapStep(rec.Step), + Secret: rec.Secret, + BlockHeight: rec.BlockNumber.Uint64(), + }, err } -// 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) (tx *types.Transaction, err error) { - return c.tokenContract.Transfer(txOpts, addr, amount) +func (c *contractorV1) vector(ctx context.Context, locator []byte) (*dexeth.SwapVector, error) { + return dexeth.ParseV1Locator(locator) } -func (c *tokenContractorV0) parseTransfer(receipt *types.Receipt) (uint64, error) { - var transferredAmt uint64 - for _, log := range receipt.Logs { - if log.Address != c.tokenAddr { - continue +func (c *contractorV1) statusAndVector(ctx context.Context, locator []byte) (*dexeth.SwapStatus, *dexeth.SwapVector, error) { + v, err := dexeth.ParseV1Locator(locator) + if err != nil { + return nil, nil, err + } + + rec, err := c.Status(&bind.CallOpts{From: c.acctAddr}, c.tokenAddr, dexeth.SwapVectorToAbigen(v)) + if err != nil { + return nil, nil, err + } + return &dexeth.SwapStatus{ + Step: dexeth.SwapStep(rec.Step), + Secret: rec.Secret, + BlockHeight: rec.BlockNumber.Uint64(), + }, v, err +} + +// func (c *contractorV1) record(v *dexeth.SwapVector) (r [32]byte, err error) { +// abiVec := dexeth.SwapVectorToAbigen(v) +// ck, err := c.ContractKey(&bind.CallOpts{From: c.acctAddr}, abiVec) +// if err != nil { +// return r, fmt.Errorf("ContractKey error: %v", err) +// } +// return c.Swaps(&bind.CallOpts{From: c.acctAddr}, ck) +// } + +func (c *contractorV1) initiate(txOpts *bind.TransactOpts, contracts []*asset.Contract) (*types.Transaction, error) { + versionedContracts := make([]swapv1.ETHSwapVector, 0, len(contracts)) + for _, ac := range contracts { + v := &dexeth.SwapVector{ + From: c.acctAddr, + To: common.HexToAddress(ac.Address), + Value: c.evmify(ac.Value), + LockTime: ac.LockTime, } - transfer, err := c.tokenContract.ParseTransfer(*log) + copy(v.SecretHash[:], ac.SecretHash) + versionedContracts = append(versionedContracts, dexeth.SwapVectorToAbigen(v)) + } + return c.Initiate(txOpts, c.tokenAddr, versionedContracts) +} + +func (c *contractorV1) redeem(txOpts *bind.TransactOpts, redeems []*asset.Redemption) (*types.Transaction, error) { + versionedRedemptions := make([]swapv1.ETHSwapRedemption, 0, len(redeems)) + secretHashes := make(map[[32]byte]bool, len(redeems)) + for _, r := range redeems { + var secret [32]byte + copy(secret[:], r.Secret) + secretHash := sha256.Sum256(r.Secret) + if !bytes.Equal(secretHash[:], r.Spends.SecretHash) { + return nil, errors.New("wrong secret") + } + if secretHashes[secretHash] { + return nil, fmt.Errorf("duplicate secret hash %x", secretHash[:]) + } + secretHashes[secretHash] = true + + // Not checking version from DecodeLocator because it was already + // audited and incorrect version locator would err below anyway. + _, locator, err := dexeth.DecodeContractData(r.Spends.Contract) if err != nil { - continue + return nil, fmt.Errorf("error parsing locator redeem: %w", err) } - if transfer.To == c.acctAddr { - transferredAmt += transfer.Value.Uint64() + v, err := dexeth.ParseV1Locator(locator) + if err != nil { + return nil, fmt.Errorf("error parsing locator: %w", err) } + versionedRedemptions = append(versionedRedemptions, swapv1.ETHSwapRedemption{ + V: dexeth.SwapVectorToAbigen(v), + Secret: secret, + }) } + return c.Redeem(txOpts, c.tokenAddr, versionedRedemptions) +} - if transferredAmt > 0 { - return transferredAmt, nil +func (c *contractorV1) refund(txOpts *bind.TransactOpts, locator []byte) (*types.Transaction, error) { + v, err := dexeth.ParseV1Locator(locator) + if err != nil { + return nil, err } + return c.Refund(txOpts, c.tokenAddr, dexeth.SwapVectorToAbigen(v)) +} - return 0, fmt.Errorf("transfer log to %s not found", c.acctAddr) +func (c *contractorV1) estimateInitGas(ctx context.Context, n int) (uint64, error) { + initiations := make([]swapv1.ETHSwapVector, 0, n) + for j := 0; j < n; j++ { + var secretHash [32]byte + copy(secretHash[:], encode.RandomBytes(32)) + initiations = append(initiations, swapv1.ETHSwapVector{ + RefundTimestamp: 1, + SecretHash: secretHash, + Initiator: c.acctAddr, + Participant: common.BytesToAddress(encode.RandomBytes(20)), + Value: big.NewInt(dexeth.GweiFactor), + }) + } + + var value *big.Int + if !c.isToken { + value = dexeth.GweiToWei(uint64(n)) + } + + return c.estimateGas(ctx, value, "initiate", c.tokenAddr, initiations) } -// estimateApproveGas estimates the gas needed to send an approve tx. -func (c *tokenContractorV0) estimateApproveGas(ctx context.Context, amount *big.Int) (uint64, error) { - return c.estimateGas(ctx, "approve", c.contractAddr, amount) +func (c *contractorV1) estimateGas(ctx context.Context, value *big.Int, method string, args ...interface{}) (uint64, error) { + return estimateGas(ctx, c.acctAddr, c.swapContractAddr, c.abi, c.cb, value, method, args...) } -// estimateTransferGas estimates the gas needed for a transfer tx. The account -// needs to have > amount tokens to use this method. -func (c *tokenContractorV0) estimateTransferGas(ctx context.Context, amount *big.Int) (uint64, error) { - return c.estimateGas(ctx, "transfer", c.acctAddr, amount) +func (c *contractorV1) estimateRedeemGas(ctx context.Context, secrets [][32]byte, locators [][]byte) (uint64, error) { + if len(secrets) != len(locators) { + return 0, fmt.Errorf("number of secrets (%d) does not match number of contracts (%d)", len(secrets), len(locators)) + } + + vectors := make([]*dexeth.SwapVector, len(locators)) + for i, loc := range locators { + v, err := dexeth.ParseV1Locator(loc) + if err != nil { + return 0, fmt.Errorf("unable to parse locator # %d (%x): %v", i, loc, err) + } + vectors[i] = v + } + + redemps := make([]swapv1.ETHSwapRedemption, 0, len(secrets)) + for i, secret := range secrets { + redemps = append(redemps, swapv1.ETHSwapRedemption{ + Secret: secret, + V: dexeth.SwapVectorToAbigen(vectors[i]), + }) + } + return c.estimateGas(ctx, nil, "redeem", c.tokenAddr, redemps) } -// estimateGas estimates the gas needed for methods on the ERC20 token contract. -// For estimating methods on the swap contract, use (contractorV0).estimateGas. -func (c *tokenContractorV0) estimateGas(ctx context.Context, method string, args ...any) (uint64, error) { - data, err := erc20.ERC20ABI.Pack(method, args...) +func (c *contractorV1) estimateRefundGas(ctx context.Context, locator []byte) (uint64, error) { + v, err := dexeth.ParseV1Locator(locator) if err != nil { - return 0, fmt.Errorf("token estimateGas Pack error: %v", err) + return 0, err } + return c.estimateGas(ctx, nil, "refund", c.tokenAddr, dexeth.SwapVectorToAbigen(v)) +} - return c.cb.EstimateGas(ctx, ethereum.CallMsg{ - From: c.acctAddr, - To: &c.tokenAddr, - Data: data, - }) +func (c *contractorV1) isRedeemable(locator []byte, secret [32]byte) (bool, error) { + v, err := dexeth.ParseV1Locator(locator) + if err != nil { + return false, err + } + if v.To != c.acctAddr { + return false, nil + } + if is, err := c.IsRedeemable(&bind.CallOpts{From: c.acctAddr}, c.tokenAddr, dexeth.SwapVectorToAbigen(v)); err != nil || !is { + return is, err + } + return sha256.Sum256(secret[:]) == v.SecretHash, nil +} + +func (c *contractorV1) isRefundable(locator []byte) (bool, error) { + v, err := dexeth.ParseV1Locator(locator) + if err != nil { + return false, err + } + if is, err := c.IsRedeemable(&bind.CallOpts{From: c.acctAddr}, c.tokenAddr, dexeth.SwapVectorToAbigen(v)); err != nil || !is { + return is, err + } + return time.Now().Unix() >= int64(v.LockTime), nil +} + +func (c *contractorV1) incomingValue(ctx context.Context, tx *types.Transaction) (uint64, error) { + if _, redeems, err := dexeth.ParseRedeemDataV1(tx.Data()); err == nil { + var redeemed *big.Int + for _, r := range redeems { + redeemed.Add(redeemed, r.Contract.Value) + } + return c.atomize(redeemed), nil + } + refund, err := dexeth.ParseRefundDataV1(tx.Data()) + if err != nil { + return 0, nil + } + return c.atomize(refund.Value), nil +} + +func (c *contractorV1) outgoingValue(tx *types.Transaction) (swapped uint64) { + if _, inits, err := dexeth.ParseInitiateDataV1(tx.Data()); err == nil { + for _, init := range inits { + swapped += c.atomize(init.Value) + } + } + return +} + +func (c *contractorV1) value(ctx context.Context, tx *types.Transaction) (in, out uint64, err error) { + if *tx.To() != c.swapContractAddr { + return 0, 0, nil + } + + if v, err := c.incomingValue(ctx, tx); err != nil { + return 0, 0, fmt.Errorf("incomingValue error: %w", err) + } else if v > 0 { + return v, 0, nil + } + + return 0, c.outgoingValue(tx), nil +} + +type tokenContractorV1 struct { + *contractorV1 + *erc20Contractor +} + +func (c *contractorV1) tokenContractor(token *dexeth.Token) (tokenContractor, error) { + netToken, found := token.NetTokens[c.net] + if !found { + return nil, fmt.Errorf("token %s has no network %s", token.Name, c.net) + } + tokenAddr := netToken.Address + + tokenContract, err := erc20.NewIERC20(tokenAddr, c.cb) + if err != nil { + return nil, err + } + + return &tokenContractorV1{ + contractorV1: &contractorV1{ + contractV1: c.contractV1, + net: c.net, + abi: c.abi, + tokenAddr: tokenAddr, + swapContractAddr: c.swapContractAddr, + acctAddr: c.acctAddr, + cb: c.cb, + isToken: true, + evmify: token.AtomicToEVM, + atomize: token.EVMToAtomic, + }, + erc20Contractor: &erc20Contractor{ + cb: c.cb, + tokenContract: tokenContract, + tokenContractAddr: tokenAddr, + acctAddr: c.acctAddr, + swapAddr: c.swapContractAddr, + }, + }, nil } // value finds incoming or outgoing value for the tx to either the swap contract // or the erc20 token contract. For the token contract, only transfer and // transferFrom are parsed. It is not an error if this tx is a call to another // method of the token contract, but no values will be parsed. -func (c *tokenContractorV0) value(ctx context.Context, tx *types.Transaction) (in, out uint64, err error) { +func (c *tokenContractorV1) value(ctx context.Context, tx *types.Transaction) (in, out uint64, err error) { to := *tx.To() - if to == c.contractAddr { - return c.contractorV0.value(ctx, tx) + if to == c.swapContractAddr { + return c.contractorV1.value(ctx, tx) } if to != c.tokenAddr { return 0, 0, nil @@ -463,7 +904,7 @@ func (c *tokenContractorV0) value(ctx context.Context, tx *types.Transaction) (i // Consider removing. We'll never be sending transferFrom transactions // directly. - if sender, _, value, err := erc20.ParseTransferFromData(tx.Data()); err == nil && sender == c.acctAddr { + if sender, _, value, err := erc20.ParseTransferFromData(tx.Data()); err == nil && sender == c.contractorV1.acctAddr { return 0, c.atomize(value), nil } @@ -476,14 +917,28 @@ func (c *tokenContractorV0) value(ctx context.Context, tx *types.Transaction) (i // tokenAddress exposes the token_address immutable address of the token-bound // swap contract. -func (c *tokenContractorV0) tokenAddress() common.Address { +func (c *tokenContractorV1) tokenAddress() common.Address { return c.tokenAddr } -var contractorConstructors = map[uint32]contractorConstructor{ - 0: newV0Contractor, +var _ contractor = (*tokenContractorV1)(nil) +var _ tokenContractor = (*tokenContractorV1)(nil) + +func estimateGas(ctx context.Context, from, to common.Address, abi *abi.ABI, cb bind.ContractBackend, value *big.Int, method string, args ...interface{}) (uint64, error) { + data, err := abi.Pack(method, args...) + if err != nil { + return 0, fmt.Errorf("Pack error: %v", err) + } + + return cb.EstimateGas(ctx, ethereum.CallMsg{ + From: from, + To: &to, + Data: data, + Value: value, + }) } -var tokenContractorConstructors = map[uint32]tokenContractorConstructor{ - 0: newV0TokenContractor, +var contractorConstructors = map[uint32]contractorConstructor{ + 0: newV0Contractor, + 1: newV1Contractor, } diff --git a/client/asset/eth/contractor_test.go b/client/asset/eth/contractor_test.go index c7f4175238..3033bee3a6 100644 --- a/client/asset/eth/contractor_test.go +++ b/client/asset/eth/contractor_test.go @@ -2,6 +2,7 @@ package eth import ( "bytes" + "crypto/sha256" "fmt" "math/big" "testing" @@ -128,7 +129,8 @@ func TestRedeemV0(t *testing.T) { c := contractorV0{contractV0: abiContract, evmify: dexeth.GweiToWei} secretB := encode.RandomBytes(32) - secretHashB := encode.RandomBytes(32) + secretHash := sha256.Sum256(secretB) + secretHashB := secretHash[:] redemption := &asset.Redemption{ Secret: secretB, @@ -160,12 +162,12 @@ func TestRedeemV0(t *testing.T) { // bad secret hash length redemption.Spends.SecretHash = encode.RandomBytes(20) checkResult("bad secret hash length", true) - redemption.Spends.SecretHash = encode.RandomBytes(32) + redemption.Spends.SecretHash = secretHashB // bad secret length redemption.Secret = encode.RandomBytes(20) checkResult("bad secret length", true) - redemption.Secret = encode.RandomBytes(32) + redemption.Secret = secretB // Redeem error abiContract.redeemErr = fmt.Errorf("test error") @@ -177,9 +179,11 @@ func TestRedeemV0(t *testing.T) { checkResult("dupe error", true) // two OK + secretB2 := encode.RandomBytes(32) + secretHash2 := sha256.Sum256(secretB2) redemption2 := &asset.Redemption{ - Secret: encode.RandomBytes(32), - Spends: &asset.AuditInfo{SecretHash: encode.RandomBytes(32)}, + Secret: secretB2, + Spends: &asset.AuditInfo{SecretHash: secretHash2[:]}, } redemptions = []*asset.Redemption{redemption, redemption2} checkResult("two ok", false) diff --git a/client/asset/eth/deploy.go b/client/asset/eth/deploy.go index 975f155ec2..f043a019ae 100644 --- a/client/asset/eth/deploy.go +++ b/client/asset/eth/deploy.go @@ -16,6 +16,7 @@ import ( dexeth "decred.org/dcrdex/dex/networks/eth" multibal "decred.org/dcrdex/dex/networks/eth/contracts/multibalance" ethv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0" + ethv1 "decred.org/dcrdex/dex/networks/eth/contracts/v1" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -140,37 +141,32 @@ func (contractDeployer) EstimateMultiBalanceDeployFunding( } func (contractDeployer) txData(contractVer uint32, tokenAddr common.Address) (txData []byte, err error) { - var abi *abi.ABI - var bytecode []byte - isToken := tokenAddr != (common.Address{}) - if isToken { + if tokenAddr == (common.Address{}) { switch contractVer { case 0: - bytecode = common.FromHex(erc20v0.ERC20SwapBin) - abi, err = erc20v0.ERC20SwapMetaData.GetAbi() - } - } else { - switch contractVer { - case 0: - bytecode = common.FromHex(ethv0.ETHSwapBin) - abi, err = ethv0.ETHSwapMetaData.GetAbi() + return common.FromHex(ethv0.ETHSwapBin), nil + case 1: + return common.FromHex(ethv1.ETHSwapBin), nil } } + var abi *abi.ABI + var bytecode []byte + switch contractVer { + case 0: + bytecode = common.FromHex(erc20v0.ERC20SwapBin) + abi, err = erc20v0.ERC20SwapMetaData.GetAbi() + } if err != nil { return nil, fmt.Errorf("error parsing ABI: %w", err) } if abi == nil { return nil, fmt.Errorf("no abi data for version %d", contractVer) } - txData = bytecode - if isToken { - argData, err := abi.Pack("", tokenAddr) - if err != nil { - return nil, fmt.Errorf("error packing token address: %w", err) - } - txData = append(txData, argData...) + argData, err := abi.Pack("", tokenAddr) + if err != nil { + return nil, fmt.Errorf("error packing token address: %w", err) } - return + return append(bytecode, argData...), nil } // DeployContract deployes a dcrdex swap contract. @@ -199,7 +195,6 @@ func (contractDeployer) DeployContract( contractAddr, tx, _, err := erc20v0.DeployERC20Swap(txOpts, cb, tokenAddress) return contractAddr, tx, err } - } } else { switch contractVer { @@ -208,6 +203,11 @@ func (contractDeployer) DeployContract( contractAddr, tx, _, err := ethv0.DeployETHSwap(txOpts, cb) return contractAddr, tx, err } + case 1: + deployer = func(txOpts *bind.TransactOpts, cb bind.ContractBackend) (common.Address, *types.Transaction, error) { + contractAddr, tx, _, err := ethv1.DeployETHSwap(txOpts, cb) + return contractAddr, tx, err + } } } if deployer == nil { @@ -264,7 +264,7 @@ func (contractDeployer) deployContract( } feeRate := dexeth.WeiToGweiCeil(maxFeeRate) - log.Infof("Estimated fees: %s gwei / gas", ui.ConventionalString(feeRate*gas)) + log.Infof("Estimated fees: %s gwei", ui.ConventionalString(feeRate*gas)) gas *= 5 / 4 // Add 20% buffer feesWithBuffer := feeRate * gas @@ -287,7 +287,13 @@ func (contractDeployer) deployContract( return err } - log.Infof("👍 Contract %s launched with tx %s", contractAddr, tx.Hash()) + log.Infof("Contract %s launched with tx %s", contractAddr, tx.Hash()) + + if err = waitForConfirmation(ctx, "deploy", cl, tx.Hash(), log); err != nil { + return fmt.Errorf("error waiting for deployment transaction status: %w", err) + } + + log.Info("👍 Transaction confirmed") return nil } diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index 8eef685728..36cf08be46 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -40,32 +40,33 @@ import ( ethmath "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/params" "github.com/tyler-smith/go-bip39" ) -func init() { - dexeth.MaybeReadSimnetAddrs() -} - func registerToken(tokenID uint32, desc string) { token, found := dexeth.Tokens[tokenID] if !found { panic("token " + strconv.Itoa(int(tokenID)) + " not known") } netAddrs := make(map[dex.Network]string) + netAssetVersions := make(map[dex.Network][]uint32, 3) for net, netToken := range token.NetTokens { netAddrs[net] = netToken.Address.String() + netAssetVersions[net] = make([]uint32, 0, 1) + for ver := range netToken.SwapContracts { + netAssetVersions[net] = append(netAssetVersions[net], ver) + } } asset.RegisterToken(tokenID, token.Token, &asset.WalletDefinition{ Type: walletTypeToken, Tab: "Ethereum token", Description: desc, - }, netAddrs) + }, netAddrs, netAssetVersions) } func init() { + dexeth.MaybeReadSimnetAddrs() asset.Register(BipID, &Driver{}) registerToken(usdcTokenID, "The USDC Ethereum ERC20 token.") registerToken(usdtTokenID, "The USDT Ethereum ERC20 token.") @@ -94,15 +95,6 @@ const ( // see DecodeCoinID func for details. coinIDTakerFoundMakerRedemption = "TakerFoundMakerRedemption:" - // maxTxFeeGwei is the default max amount of eth that can be used in one - // transaction. This is set by the host in the case of providers. The - // internal node currently has no max but also cannot be used since the - // merge. - // - // TODO: Find a way to ask the host about their config set max fee and - // gas values. - maxTxFeeGwei = 1_000_000_000 - LiveEstimateFailedError = dex.ErrorKind("live gas estimate failed") // txAgeOut is the amount of time after which we forego any tx @@ -119,9 +111,8 @@ const ( stateUpdateTick = time.Second * 5 // maxUnindexedTxs is the number of pending txs we will allow to be // unverified on-chain before we halt broadcasting of new txs. - maxUnindexedTxs = 10 - peerCountTicker = 5 * time.Second // no rpc calls here - contractVersionNewest = ^uint32(0) + maxUnindexedTxs = 10 + peerCountTicker = 5 * time.Second // no rpc calls here ) var ( @@ -161,7 +152,7 @@ var ( // exposed though any Driver methods or assets/driver functions. Use the // parent wallet's WalletInfo via (*Driver).Info if you need a token's // supported versions before a wallet is available. - SupportedVersions: []uint32{0}, + SupportedVersions: []uint32{0, 1}, UnitInfo: dexeth.UnitInfo, AvailableWallets: []*asset.WalletDefinition{ // { @@ -179,8 +170,6 @@ var ( Seeded: true, GuideLink: "https://github.com/decred/dcrdex/blob/master/docs/wiki/Ethereum.md", }, - // MaxSwapsInTx and MaxRedeemsInTx are set in (Wallet).Info, since - // the value cannot be known until we connect and get network info. }, IsAccountBased: true, } @@ -203,31 +192,6 @@ var ( } ) -// perTxGasLimit is the most gas we can use on a transaction. It is the lower of -// either the per tx or per block gas limit. -func perTxGasLimit(gasFeeLimit uint64) uint64 { - // maxProportionOfBlockGasLimitToUse sets the maximum proportion of a - // block's gas limit that a swap and redeem transaction will use. Since it - // is set to 4, the max that will be used is 25% (1/4) of the block's gas - // limit. - const maxProportionOfBlockGasLimitToUse = 4 - - // blockGasLimit is the amount of gas we can use in one transaction - // according to the block gas limit. - - // Ethereum GasCeil: 30_000_000, Polygon: 8_000_000 - blockGasLimit := ethconfig.Defaults.Miner.GasCeil / maxProportionOfBlockGasLimitToUse - - // txGasLimit is the amount of gas we can use in one transaction - // according to the default transaction gas fee limit. - txGasLimit := maxTxFeeGwei / gasFeeLimit - - if blockGasLimit > txGasLimit { - return txGasLimit - } - return blockGasLimit -} - // safeConfs returns the confirmations for a given tip and block number, // returning 0 if the block number is zero or if the tip is lower than the // block number. @@ -360,7 +324,7 @@ type Balance struct { } // ethFetcher represents a blockchain information fetcher. In practice, it is -// satisfied by *nodeClient. For testing, it can be satisfied by a stub. +// satisfied by *multiRPCClient. For testing, it can be satisfied by a stub. type ethFetcher interface { address() common.Address addressBalance(ctx context.Context, addr common.Address) (*big.Int, error) @@ -439,11 +403,12 @@ type baseWallet struct { multiBalanceAddress common.Address multiBalanceContract *multibal.MultiBalanceV0 - baseChainID uint32 - chainCfg *params.ChainConfig - chainID int64 - compat *CompatibilityData - tokens map[uint32]*dexeth.Token + baseChainID uint32 + chainCfg *params.ChainConfig + chainID int64 + compat *CompatibilityData + tokens map[uint32]*dexeth.Token + maxTxFeeGwei uint64 startingBlocks atomic.Uint64 @@ -490,13 +455,11 @@ type assetWallet struct { ui dex.UnitInfo connected atomic.Bool wi asset.WalletInfo + tokenAddr common.Address // empty address for base chain asset versionedContracts map[uint32]common.Address versionedGases map[uint32]*dexeth.Gases - maxSwapGas uint64 - maxRedeemGas uint64 - lockedFunds struct { mtx sync.RWMutex initiateReserves uint64 @@ -505,7 +468,7 @@ type assetWallet struct { } findRedemptionMtx sync.RWMutex - findRedemptionReqs map[[32]byte]*findRedemptionRequest + findRedemptionReqs map[string]*findRedemptionRequest approvalsMtx sync.RWMutex pendingApprovals map[uint32]*pendingApproval @@ -514,7 +477,8 @@ type assetWallet struct { lastPeerCount uint32 peersChange func(uint32, error) - contractors map[uint32]contractor // version -> contractor + contractorV0 contractor + contractorV1 contractor evmify func(uint64) *big.Int atomize func(*big.Int) uint64 @@ -547,17 +511,54 @@ type TokenWallet struct { netToken *dexeth.NetToken } -func (w *assetWallet) maxSwapsAndRedeems() (maxSwaps, maxRedeems uint64) { - txGasLimit := perTxGasLimit(atomic.LoadUint64(&w.gasFeeLimitV)) - return txGasLimit / w.maxSwapGas, txGasLimit / w.maxRedeemGas +// perTxGasLimit is the most gas we can use on a transaction. It is the lower of +// either the block's gas limit or the limit based on our maximum allowable +// fees. +func (w *assetWallet) perTxGasLimit(feeRateGwei uint64) uint64 { + blockGasLimit := w.tip().GasLimit + maxFeeBasedGasLimit := w.maxTxFeeGwei / feeRateGwei + if maxFeeBasedGasLimit < blockGasLimit { + return maxFeeBasedGasLimit + } + return blockGasLimit +} + +// maxSwapsOrRedeems calculates the maximum number of swaps or redemptions that +// can go in a single transaction. If feeRateGwei is not provided, the +// prevailing fee rate will be used. +func (w *assetWallet) maxSwapsOrRedeems(oneGas, gasAdd uint64, feeRateGwei uint64) (n int, err error) { + feeRate := dexeth.GweiToWei(feeRateGwei) + if feeRateGwei == 0 { + feeRate, err = w.currentFeeRate(w.ctx) + if err != nil { + return 0, err + } + } + + txGasLimit := w.perTxGasLimit(dexeth.WeiToGweiCeil(feeRate)) + if oneGas > txGasLimit { + return 0, fmt.Errorf("tx limit zero") + } + return 1 + int((txGasLimit-oneGas)/gasAdd), nil +} + +var _ asset.MaxMatchesCounter = (*assetWallet)(nil) + +// MaxSwaps is the maximum matches than can go in a swap tx. +func (w *assetWallet) MaxSwaps(assetVer uint32, feeRateGwei uint64) (int, error) { + g := w.gases(contractVersion(assetVer)) + return w.maxSwapsOrRedeems(g.Swap, g.SwapAdd, feeRateGwei) +} + +// MaxRedeems is the maximum matches than can go in a redeem tx. +func (w *assetWallet) MaxRedeems(assetVer uint32) (int, error) { + g := w.gases(contractVersion(assetVer)) + return w.maxSwapsOrRedeems(g.Redeem, g.RedeemAdd, 0) } // Info returns basic information about the wallet and asset. func (w *assetWallet) Info() *asset.WalletInfo { wi := w.wi - maxSwaps, maxRedeems := w.maxSwapsAndRedeems() - wi.MaxSwapsInTx = maxSwaps - wi.MaxRedeemsInTx = maxRedeems return &wi } @@ -595,6 +596,16 @@ func privKeyFromSeed(seed []byte) (pk []byte, zero func(), err error) { return pk, extKey.Zero, nil } +// contractVersion converts a server's asset protocol version to a swap contract +// version. It applies to both tokens and eth right now, but that may not always +// be the case. +func contractVersion(assetVer uint32) uint32 { + if assetVer == asset.VersionNewest { + return dexeth.ContractVersionNewest + } + return dexeth.ProtocolVersion(assetVer).ContractVersion() +} + func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams, compat *CompatibilityData, skipConnect bool) error { switch createWalletParams.Type { case walletTypeGeth: @@ -613,16 +624,6 @@ func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams defer zero() switch createWalletParams.Type { - // case walletTypeGeth: - // node, err := prepareNode(&nodeConfig{ - // net: createWalletParams.Net, - // appDir: walletDir, - // }) - // if err != nil { - // return err - // } - // defer node.Close() - // return importKeyToNode(node, privateKey, createWalletParams.Pass) case walletTypeRPC: // Make the wallet dir if it does not exist, otherwise we may fail to // write the compliant-providers.json file. Create the keystore @@ -716,6 +717,7 @@ func newWallet(assetCFG *asset.WalletConfig, logger dex.Logger, net dex.Network) WalletInfo: WalletInfo, Net: net, DefaultProviders: defaultProviders, + MaxTxFeeGwei: dexeth.GweiFactor, // 1 ETH }) } @@ -734,6 +736,9 @@ type EVMWalletConfig struct { MultiBalAddress common.Address // If empty, separate calls for N tokens + 1 WalletInfo asset.WalletInfo Net dex.Network + // MaxTxFeeGwei is the absolute maximum fees we will allow for a single tx. + // It should be set to a relatively large value. + MaxTxFeeGwei uint64 } func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) { @@ -776,6 +781,7 @@ func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) { gasFeeLimitV: gasFeeLimit, wallets: make(map[uint32]*assetWallet), multiBalanceAddress: cfg.MultiBalAddress, + maxTxFeeGwei: cfg.MaxTxFeeGwei, } var maxSwapGas, maxRedeemGas uint64 @@ -788,29 +794,17 @@ func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) { } } - txGasLimit := perTxGasLimit(gasFeeLimit) - - if maxSwapGas == 0 || txGasLimit < maxSwapGas { - return nil, errors.New("max swaps cannot be zero or undefined") - } - if maxRedeemGas == 0 || txGasLimit < maxRedeemGas { - return nil, errors.New("max redeems cannot be zero or undefined") - } - aw := &assetWallet{ baseWallet: eth, log: cfg.Logger, assetID: assetID, versionedContracts: cfg.BaseChainContracts, versionedGases: cfg.VersionedGases, - maxSwapGas: maxSwapGas, - maxRedeemGas: maxRedeemGas, emit: cfg.AssetCfg.Emit, - findRedemptionReqs: make(map[[32]byte]*findRedemptionRequest), + findRedemptionReqs: make(map[string]*findRedemptionRequest), pendingApprovals: make(map[uint32]*pendingApproval), approvalCache: make(map[uint32]bool), peersChange: cfg.AssetCfg.PeersChange, - contractors: make(map[uint32]contractor), evmify: dexeth.GweiToWei, atomize: dexeth.WeiToGwei, ui: dexeth.UnitInfo, @@ -818,11 +812,6 @@ func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) { wi: cfg.WalletInfo, } - maxSwaps, maxRedeems := aw.maxSwapsAndRedeems() - - cfg.Logger.Debugf("ETH wallet will support a maximum of %d swaps and %d redeems per transaction.", - maxSwaps, maxRedeems) - aw.wallets = map[uint32]*assetWallet{ assetID: aw, } @@ -876,13 +865,19 @@ func (w *ETHWallet) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) for ver, constructor := range contractorConstructors { contractAddr, exists := w.versionedContracts[ver] if !exists || contractAddr == (common.Address{}) { - return nil, fmt.Errorf("no contract address for version %d, net %s", ver, w.net) + w.log.Debugf("no eth swap contract address for version %d, net %s", ver, w.net) + continue } - c, err := constructor(contractAddr, w.addr, w.node.contractBackend()) + c, err := constructor(w.net, contractAddr, w.addr, w.node.contractBackend()) if err != nil { return nil, fmt.Errorf("error constructor version %d contractor: %v", ver, err) } - w.contractors[ver] = c + switch ver { + case 0: + w.contractorV0 = c + case 1: + w.contractorV1 = c + } } if w.multiBalanceAddress != (common.Address{}) { @@ -946,7 +941,7 @@ func (w *ETHWallet) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) bestHdr.Number, confirmedNonce, nextNonce, len(pendingTxs), highestPendingNonce, lowestPendingNonce) } - height := w.currentTip.Number + height := bestHdr.Number // NOTE: We should be using the tipAtConnect to set Progress in SyncStatus. atomic.StoreInt64(&w.tipAtConnect, height.Int64()) w.log.Infof("Connected to eth (%s), at height %d", w.walletType, height) @@ -983,7 +978,7 @@ func (w *TokenWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) { return nil, fmt.Errorf("parent wallet not connected") } - err := w.loadContractors() + err := w.loadContractors(w.parent) if err != nil { return nil, err } @@ -1004,11 +999,15 @@ func (w *TokenWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) { return &wg, nil } -// tipHeight gets the current best header's tip height. -func (w *baseWallet) tipHeight() uint64 { +func (w *baseWallet) tip() *types.Header { w.tipMtx.RLock() defer w.tipMtx.RUnlock() - return w.currentTip.Number.Uint64() + return w.currentTip +} + +// tipHeight gets the current best header's tip height. +func (w *baseWallet) tipHeight() uint64 { + return w.tip().Number.Uint64() } // Reconfigure attempts to reconfigure the wallet. @@ -1237,30 +1236,13 @@ func (w *ETHWallet) OpenTokenWallet(tokenCfg *asset.TokenConfig) (asset.Wallet, return nil, fmt.Errorf("could not find token with ID %d on network %s", w.assetID, w.net) } - var maxSwapGas, maxRedeemGas uint64 - for _, contract := range netToken.SwapContracts { - if contract.Gas.Swap > maxSwapGas { - maxSwapGas = contract.Gas.Swap - } - if contract.Gas.Redeem > maxRedeemGas { - maxRedeemGas = contract.Gas.Redeem - } - } - - txGasLimit := perTxGasLimit(atomic.LoadUint64(&w.gasFeeLimitV)) - - if maxSwapGas == 0 || txGasLimit < maxSwapGas { - return nil, errors.New("max swaps cannot be zero or undefined") - } - if maxRedeemGas == 0 || txGasLimit < maxRedeemGas { - return nil, errors.New("max redeems cannot be zero or undefined") - } - + supportedAssetVersions := make([]uint32, 0, 1) contracts := make(map[uint32]common.Address) gases := make(map[uint32]*dexeth.Gases) for ver, c := range netToken.SwapContracts { contracts[ver] = c.Address gases[ver] = &c.Gas + supportedAssetVersions = append(supportedAssetVersions, ver) } aw := &assetWallet{ @@ -1269,22 +1251,20 @@ func (w *ETHWallet) OpenTokenWallet(tokenCfg *asset.TokenConfig) (asset.Wallet, assetID: tokenCfg.AssetID, versionedContracts: contracts, versionedGases: gases, - maxSwapGas: maxSwapGas, - maxRedeemGas: maxRedeemGas, emit: tokenCfg.Emit, peersChange: tokenCfg.PeersChange, - findRedemptionReqs: make(map[[32]byte]*findRedemptionRequest), + findRedemptionReqs: make(map[string]*findRedemptionRequest), pendingApprovals: make(map[uint32]*pendingApproval), approvalCache: make(map[uint32]bool), - contractors: make(map[uint32]contractor), evmify: token.AtomicToEVM, atomize: token.EVMToAtomic, ui: token.UnitInfo, wi: asset.WalletInfo{ Name: token.Name, - SupportedVersions: w.wi.SupportedVersions, + SupportedVersions: supportedAssetVersions, UnitInfo: token.UnitInfo, }, + tokenAddr: netToken.Address, pendingTxCheckBal: new(big.Int), } @@ -1441,18 +1421,19 @@ func (w *TokenWallet) MaxOrder(ord *asset.MaxOrderForm) (*asset.SwapEstimate, er ord.RedeemVersion, ord.RedeemAssetID, w.parent) } -func (w *assetWallet) maxOrder(lotSize uint64, maxFeeRate uint64, ver uint32, - redeemVer, redeemAssetID uint32, feeWallet *assetWallet) (*asset.SwapEstimate, error) { +func (w *assetWallet) maxOrder(lotSize uint64, maxFeeRate uint64, initAssetVer, + redeemAssetVer, redeemAssetID uint32, feeWallet *assetWallet) (*asset.SwapEstimate, error) { balance, err := w.Balance() if err != nil { return nil, err } + initContractVer := contractVersion(initAssetVer) // Get the refund gas. - if g := w.gases(ver); g == nil { + if g := w.gases(initContractVer); g == nil { return nil, fmt.Errorf("no gas table") } - g, err := w.initGasEstimate(1, ver, redeemVer, redeemAssetID) + g, err := w.initGasEstimate(1, initContractVer, contractVersion(redeemAssetVer), redeemAssetID, maxFeeRate) liveEstimateFailed := errors.Is(err, LiveEstimateFailedError) if err != nil && !liveEstimateFailed { return nil, fmt.Errorf("gasEstimate error: %w", err) @@ -1482,7 +1463,7 @@ func (w *assetWallet) maxOrder(lotSize uint64, maxFeeRate uint64, ver uint32, FeeReservesPerLot: feeReservesPerLot, }, nil } - return w.estimateSwap(lots, lotSize, maxFeeRate, ver, feeReservesPerLot) + return w.estimateSwap(lots, lotSize, maxFeeRate, initContractVer, feeReservesPerLot) } // PreSwap gets order estimates based on the available funds and the wallet @@ -1498,7 +1479,7 @@ func (w *TokenWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) { } func (w *assetWallet) preSwap(req *asset.PreSwapForm, feeWallet *assetWallet) (*asset.PreSwap, error) { - maxEst, err := w.maxOrder(req.LotSize, req.MaxFeeRate, req.Version, + maxEst, err := w.maxOrder(req.LotSize, req.MaxFeeRate, req.AssetVersion, req.RedeemVersion, req.RedeemAssetID, feeWallet) if err != nil { return nil, err @@ -1509,7 +1490,7 @@ func (w *assetWallet) preSwap(req *asset.PreSwapForm, feeWallet *assetWallet) (* } est, err := w.estimateSwap(req.Lots, req.LotSize, req.MaxFeeRate, - req.Version, maxEst.FeeReservesPerLot) + contractVersion(req.AssetVersion), maxEst.FeeReservesPerLot) if err != nil { return nil, err } @@ -1525,13 +1506,11 @@ func (w *baseWallet) MaxFundingFees(_ uint32, _ uint64, _ map[string]string) uin } // SingleLotSwapRefundFees returns the fees for a swap transaction for a single lot. -func (w *assetWallet) SingleLotSwapRefundFees(version uint32, feeSuggestion uint64, _ bool) (swapFees uint64, refundFees uint64, err error) { - if version == asset.VersionNewest { - version = contractVersionNewest - } - g := w.gases(version) +func (w *assetWallet) SingleLotSwapRefundFees(assetVer uint32, feeSuggestion uint64, _ bool) (swapFees uint64, refundFees uint64, err error) { + contractVer := contractVersion(assetVer) + g := w.gases(contractVer) if g == nil { - return 0, 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version) + return 0, 0, fmt.Errorf("no gases known for %d contract version %d", w.assetID, contractVersion(assetVer)) } return g.Swap * feeSuggestion, g.Refund * feeSuggestion, nil } @@ -1539,7 +1518,7 @@ func (w *assetWallet) SingleLotSwapRefundFees(version uint32, feeSuggestion uint // estimateSwap prepares an *asset.SwapEstimate. The estimate does not include // funds that might be locked for refunds. func (w *assetWallet) estimateSwap( - lots, lotSize uint64, maxFeeRate uint64, ver uint32, feeReservesPerLot uint64, + lots, lotSize uint64, maxFeeRate uint64, contractVer uint32, feeReservesPerLot uint64, ) (*asset.SwapEstimate, error) { if lots == 0 { @@ -1554,7 +1533,7 @@ func (w *assetWallet) estimateSwap( } feeRateGwei := dexeth.WeiToGweiCeil(feeRate) // This is an estimate, so we use the (lower) live gas estimates. - oneSwap, err := w.estimateInitGas(w.ctx, 1, ver) + oneSwap, err := w.estimateInitGas(w.ctx, 1, contractVer) if err != nil { return nil, fmt.Errorf("(%d) error estimating swap gas: %v", w.assetID, err) } @@ -1584,7 +1563,7 @@ func (w *assetWallet) gases(contractVer uint32) *dexeth.Gases { // PreRedeem generates an estimate of the range of redemption fees that could // be assessed. func (w *assetWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, error) { - oneRedeem, nRedeem, err := w.redeemGas(int(req.Lots), req.Version) + oneRedeem, nRedeem, err := w.redeemGas(int(req.Lots), contractVersion(req.AssetVersion)) if err != nil { return nil, err } @@ -1598,15 +1577,11 @@ func (w *assetWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, err } // SingleLotRedeemFees returns the fees for a redeem transaction for a single lot. -func (w *assetWallet) SingleLotRedeemFees(version uint32, feeSuggestion uint64) (fees uint64, err error) { - if version == asset.VersionNewest { - version = contractVersionNewest - } - g := w.gases(version) +func (w *assetWallet) SingleLotRedeemFees(assetVer uint32, feeSuggestion uint64) (fees uint64, err error) { + g := w.gases(contractVersion(assetVer)) if g == nil { - return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version) + return 0, fmt.Errorf("no gases known for %d, constract version %d", w.assetID, contractVersion(assetVer)) } - return g.Redeem * feeSuggestion, nil } @@ -1663,8 +1638,10 @@ func (w *ETHWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint6 dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) } - g, err := w.initGasEstimate(int(ord.MaxSwapCount), ord.Version, - ord.RedeemVersion, ord.RedeemAssetID) + contractVer := contractVersion(ord.AssetVersion) + + g, err := w.initGasEstimate(int(ord.MaxSwapCount), contractVer, + ord.RedeemVersion, ord.RedeemAssetID, ord.MaxFeeRate) if err != nil { return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) } @@ -1701,7 +1678,7 @@ func (w *TokenWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uin dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) } - approvalStatus, err := w.approvalStatus(ord.Version) + approvalStatus, err := w.approvalStatus(ord.AssetVersion) if err != nil { return nil, nil, 0, fmt.Errorf("error getting approval status: %v", err) } @@ -1715,8 +1692,8 @@ func (w *TokenWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uin return nil, nil, 0, fmt.Errorf("unknown approval status %d", approvalStatus) } - g, err := w.initGasEstimate(int(ord.MaxSwapCount), ord.Version, - ord.RedeemVersion, ord.RedeemAssetID) + g, err := w.initGasEstimate(int(ord.MaxSwapCount), contractVersion(ord.AssetVersion), + ord.RedeemVersion, ord.RedeemAssetID, ord.MaxFeeRate) if err != nil { return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) } @@ -1753,7 +1730,7 @@ func (w *ETHWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]ass dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) } - g, err := w.initGasEstimate(1, ord.Version, ord.RedeemVersion, ord.RedeemAssetID) + g, err := w.initGasEstimate(1, ord.AssetVersion, ord.RedeemVersion, ord.RedeemAssetID, ord.MaxFeeRate) if err != nil { return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) } @@ -1791,7 +1768,7 @@ func (w *TokenWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]a dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) } - approvalStatus, err := w.approvalStatus(ord.Version) + approvalStatus, err := w.approvalStatus(ord.AssetVersion) if err != nil { return nil, nil, 0, fmt.Errorf("error getting approval status: %v", err) } @@ -1805,8 +1782,8 @@ func (w *TokenWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]a return nil, nil, 0, fmt.Errorf("unknown approval status %d", approvalStatus) } - g, err := w.initGasEstimate(1, ord.Version, - ord.RedeemVersion, ord.RedeemAssetID) + g, err := w.initGasEstimate(1, ord.AssetVersion, + ord.RedeemVersion, ord.RedeemAssetID, ord.MaxFeeRate) if err != nil { return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) } @@ -1860,17 +1837,17 @@ type gasEstimate struct { // initGasEstimate gets the best available gas estimate for n initiations. A // live estimate is checked against the server's configured values and our own // known values and errors or logs generated in certain cases. -func (w *assetWallet) initGasEstimate(n int, initVer, redeemVer, redeemAssetID uint32) (est *gasEstimate, err error) { +func (w *assetWallet) initGasEstimate(n int, initContractVer, redeemContractVer, redeemAssetID uint32, feeRateGwei uint64) (est *gasEstimate, err error) { est = new(gasEstimate) // Get the refund gas. - if g := w.gases(initVer); g == nil { + if g := w.gases(initContractVer); g == nil { return nil, fmt.Errorf("no gas table") } else { // scoping g est.Refund = g.Refund } - est.Swap, est.nSwap, err = w.swapGas(n, initVer) + est.Swap, est.nSwap, err = w.swapGas(n, initContractVer, feeRateGwei) if err != nil && !errors.Is(err, LiveEstimateFailedError) { return nil, err } @@ -1881,7 +1858,7 @@ func (w *assetWallet) initGasEstimate(n int, initVer, redeemVer, redeemAssetID u if redeemW := w.wallet(redeemAssetID); redeemW != nil { var er error - est.Redeem, est.nRedeem, er = redeemW.redeemGas(n, redeemVer) + est.Redeem, est.nRedeem, er = redeemW.redeemGas(n, redeemContractVer) if err != nil { return nil, fmt.Errorf("error calculating fee-family redeem gas: %w", er) } @@ -1896,99 +1873,76 @@ func (w *assetWallet) initGasEstimate(n int, initVer, redeemVer, redeemAssetID u // cannot get a live estimate from the contractor, which will happen if the // wallet has no balance. A live gas estimate will always be attempted, and used // if our expected gas values are lower (anomalous). -func (w *assetWallet) swapGas(n int, ver uint32) (oneSwap, nSwap uint64, err error) { - g := w.gases(ver) +func (w *assetWallet) swapGas(n int, contractVer uint32, feeRateGwei uint64) (oneSwapGas, nSwapGas uint64, err error) { + g := w.gases(contractVer) if g == nil { - return 0, 0, fmt.Errorf("no gases known for %d version %d", w.assetID, ver) + return 0, 0, fmt.Errorf("no gases known for %d contract version %d", w.assetID, contractVer) } - oneSwap = g.Swap - - // We have no way of updating the value of SwapAdd without a version change, - // but we're not gonna worry about accuracy for nSwap, since it's only used - // for estimates and never for dex-validated values like order funding. - nSwap = oneSwap + uint64(n-1)*g.SwapAdd + oneSwapGas = g.Swap // The amount we can estimate and ultimately the amount we can use in a // single transaction is limited by the block gas limit or the tx gas // limit. Core will use the largest gas among all versions when // determining the maximum number of swaps that can be in one - // transaction. Limit our gas estimate to the same number of swaps. - nMax := n - maxSwaps, _ := w.maxSwapsAndRedeems() - var nRemain, nFull int - if uint64(n) > maxSwaps { - nMax = int(maxSwaps) - nFull = n / nMax - nSwap = (oneSwap + uint64(nMax-1)*g.SwapAdd) * uint64(nFull) - nRemain = n % nMax - if nRemain != 0 { - nSwap += oneSwap + uint64(nRemain-1)*g.SwapAdd - } + // transaction. Limit our gas estimate to the same number of swaps/redeems.. + maxSwapsPerTx, err := w.maxSwapsOrRedeems(g.Swap, g.SwapAdd, feeRateGwei) + if err != nil { + return 0, 0, fmt.Errorf("error calculating max swaps: %w", err) } - // If a live estimate is greater than our estimate from configured values, - // use the live estimate with a warning. - gasEst, err := w.estimateInitGas(w.ctx, nMax, ver) - if err != nil { - err = errors.Join(err, LiveEstimateFailedError) - return - // Or we could go with what we know? But this estimate error could be a - // hint that the transaction would fail, and we don't have a way to - // recover from that. Play it safe and allow caller to retry assuming - // the error is transient with the provider. - // w.log.Errorf("(%d) error estimating swap gas (using expected gas cap instead): %v", w.assetID, err) - // return oneSwap, nSwap, true, nil - } - if nMax != n { - // If we needed to adjust the max earlier, and the estimate did - // not error, multiply the estimate by the number of full - // transactions and add the estimate of the remainder. - gasEst *= uint64(nFull) - if nRemain > 0 { - remainEst, err := w.estimateInitGas(w.ctx, nRemain, ver) - if err != nil { - w.log.Errorf("(%d) error estimating swap gas for remainder: %v", w.assetID, err) - return 0, 0, err - } - gasEst += remainEst + if nFull := n / maxSwapsPerTx; nFull > 0 { + fullGas := g.SwapN(maxSwapsPerTx) + if fullGasEst, err := w.estimateInitGas(w.ctx, maxSwapsPerTx, contractVer); err != nil { + w.log.Errorf("(%d) error estimating swap gas for full txs: %v", w.assetID, err) + return 0, 0, errors.Join(err, LiveEstimateFailedError) + } else if fullGasEst > fullGas { + w.log.Warnf("%d-tx (full) swap gas estimate %d is greater than the server's configured value %d. Using live estimate + 10%%.", + nFull, fullGasEst, fullGas) + fullGas = fullGasEst * 11 / 10 } + nSwapGas = uint64(nFull) * fullGas } - if gasEst > nSwap { - w.log.Warnf("Swap gas estimate %d is greater than the server's configured value %d. Using live estimate + 10%%.", gasEst, nSwap) - nSwap = gasEst * 11 / 10 // 10% buffer - if n == 1 && nSwap > oneSwap { - oneSwap = nSwap + if nRemain := n & maxSwapsPerTx; nRemain > 0 { + remainGas := g.SwapN(nRemain) + if remainGasEst, err := w.estimateInitGas(w.ctx, nRemain, contractVer); err != nil { + w.log.Errorf("(%d) error estimating swap gas for remainder: %v", w.assetID, err) + return 0, 0, errors.Join(err, LiveEstimateFailedError) + } else if remainGasEst > remainGas { + w.log.Warnf("%d-tx swap gas estimate %d is greater than the server's configured value %d. Using live estimate + 10%%.", + nRemain, remainGasEst, remainGas) + remainGas = remainGasEst * 11 / 10 } + nSwapGas += remainGas } return } // redeemGas gets an accurate estimate for redemption gas. We allow a DEX server // some latitude in adjusting the redemption gas, up to 2x our local estimate. -func (w *assetWallet) redeemGas(n int, ver uint32) (oneGas, nGas uint64, err error) { - g := w.gases(ver) +func (w *assetWallet) redeemGas(n int, contractVer uint32) (oneGas, nGas uint64, err error) { + g := w.gases(contractVer) if g == nil { return 0, 0, fmt.Errorf("no gas table for redemption asset %d", w.assetID) } redeemGas := g.Redeem // Not concerned with the accuracy of nGas. It's never used outside of // best case estimates. - return redeemGas, redeemGas + (uint64(n)-1)*g.RedeemAdd, nil + return redeemGas, g.RedeemN(n), nil } // approvalGas gets the best available estimate for an approval tx, which is // the greater of the asset's registered value and a live estimate. It is an // error if a live estimate cannot be retrieved, which will be the case if the // user's eth balance is insufficient to cover tx fees for the approval. -func (w *assetWallet) approvalGas(newGas *big.Int, ver uint32) (uint64, error) { - ourGas := w.gases(ver) +func (w *assetWallet) approvalGas(newGas *big.Int, contractVer uint32) (uint64, error) { + ourGas := w.gases(contractVer) if ourGas == nil { - return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, ver) + return 0, fmt.Errorf("no gases known for %d contract version %d", w.assetID, contractVer) } approveGas := ourGas.Approve - if approveEst, err := w.estimateApproveGas(newGas); err != nil { + if approveEst, err := w.estimateApproveGas(contractVer, 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%%.", approveEst, approveGas) @@ -2101,13 +2055,13 @@ func (w *TokenWallet) FundingCoins(ids []dex.Bytes) (asset.Coins, error) { // swapReceipt implements the asset.Receipt interface for ETH. type swapReceipt struct { - txHash common.Hash - secretHash [dexeth.SecretHashSize]byte + txHash common.Hash + locator []byte // expiration and value can be determined with a blockchain // lookup, but we cache these values to avoid this. expiration time.Time value uint64 - ver uint32 + contractVer uint32 contractAddr string // specified by ver, here for naive consumers } @@ -2128,7 +2082,7 @@ func (r *swapReceipt) Coin() asset.Coin { // Contract returns the swap's identifying data, which the concatenation of the // contract version and the secret hash. func (r *swapReceipt) Contract() dex.Bytes { - return dexeth.EncodeContractData(r.ver, r.secretHash) + return dexeth.EncodeContractData(r.contractVer, r.locator) } // String returns a string representation of the swapReceipt. The secret hash @@ -2137,8 +2091,8 @@ func (r *swapReceipt) Contract() dex.Bytes { // the user can pick this information from the transaction's "to" address and // the calldata, this simplifies the process. func (r *swapReceipt) String() string { - return fmt.Sprintf("{ tx hash: %s, contract address: %s, secret hash: %x }", - r.txHash, r.contractAddr, r.secretHash) + return fmt.Sprintf("{ tx hash: %s, contract address: %s, locator: %x }", + r.txHash, r.contractAddr, r.locator) } // SignedRefund returns an empty byte array. ETH does not support a pre-signed @@ -2176,9 +2130,9 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 swapVal += contract.Value } - // Set the gas limit as high as reserves will allow. + contractVer := contractVersion(swaps.AssetVersion) n := len(swaps.Contracts) - oneSwap, nSwap, err := w.swapGas(n, swaps.Version) + oneSwap, nSwap, err := w.swapGas(n, contractVer, swaps.FeeRate) if err != nil { return fail("error getting gas fees: %v", err) } @@ -2210,7 +2164,7 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 return fail("Swap: failed to get network tip cap: %w", err) } - tx, err := w.initiate(w.ctx, w.assetID, swaps.Contracts, gasLimit, maxFeeRate, tipRate, swaps.Version) + tx, err := w.initiate(w.ctx, w.assetID, swaps.Contracts, gasLimit, maxFeeRate, tipRate, contractVer) if err != nil { return fail("Swap: initiate error: %w", err) } @@ -2218,15 +2172,13 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 txHash := tx.Hash() receipts := make([]asset.Receipt, 0, n) for _, swap := range swaps.Contracts { - var secretHash [dexeth.SecretHashSize]byte - copy(secretHash[:], swap.SecretHash) receipts = append(receipts, &swapReceipt{ expiration: time.Unix(int64(swap.LockTime), 0), value: swap.Value, txHash: txHash, - secretHash: secretHash, - ver: swaps.Version, - contractAddr: w.versionedContracts[swaps.Version].String(), + locator: acToLocator(contractVer, swap, dexeth.GweiToWei(swap.Value), w.addr), + contractVer: contractVer, + contractAddr: w.versionedContracts[contractVer].String(), }) } @@ -2241,6 +2193,26 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 return receipts, change, fees, nil } +// acToLocator converts the asset.Contract to a version-specific locator. +func acToLocator(contractVer uint32, swap *asset.Contract, evmValue *big.Int, from common.Address) []byte { + switch contractVer { + case 0: + return swap.SecretHash + case 1: + var secretHash [32]byte + copy(secretHash[:], swap.SecretHash) + return (&dexeth.SwapVector{ + From: from, + To: common.HexToAddress(swap.Address), + Value: evmValue, + SecretHash: secretHash, + LockTime: swap.LockTime, + }).Locator() + default: + panic("need to add a version in acToLocator") + } +} + // Swap sends the swaps in a single transaction. The fees used returned are the // max fees that will possibly be used, since in ethereum with EIP-1559 we cannot // know exactly how much fees will be used. @@ -2273,7 +2245,8 @@ func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uin } n := len(swaps.Contracts) - oneSwap, nSwap, err := w.swapGas(n, swaps.Version) + contractVer := contractVersion(swaps.AssetVersion) + oneSwap, nSwap, err := w.swapGas(n, contractVer, swaps.FeeRate) if err != nil { return fail("error getting gas fees: %v", err) } @@ -2299,28 +2272,26 @@ func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uin return fail("Swap: failed to get network tip cap: %w", err) } - tx, err := w.initiate(w.ctx, w.assetID, swaps.Contracts, gasLimit, maxFeeRate, tipRate, swaps.Version) + tx, err := w.initiate(w.ctx, w.assetID, swaps.Contracts, gasLimit, maxFeeRate, tipRate, contractVer) if err != nil { return fail("Swap: initiate error: %w", err) } - if w.netToken.SwapContracts[swaps.Version] == nil { - return fail("unable to find contract address for asset %d contract version %d", w.assetID, swaps.Version) + if w.netToken.SwapContracts[swaps.AssetVersion] == nil { + return fail("unable to find contract address for asset %d contract version %d", w.assetID, swaps.AssetVersion) } - contractAddr := w.netToken.SwapContracts[swaps.Version].Address.String() + contractAddr := w.netToken.SwapContracts[contractVer].Address.String() txHash := tx.Hash() receipts := make([]asset.Receipt, 0, n) for _, swap := range swaps.Contracts { - var secretHash [dexeth.SecretHashSize]byte - copy(secretHash[:], swap.SecretHash) receipts = append(receipts, &swapReceipt{ expiration: time.Unix(int64(swap.LockTime), 0), value: swap.Value, txHash: txHash, - secretHash: secretHash, - ver: swaps.Version, + locator: acToLocator(contractVer, swap, w.evmify(swap.Value), w.addr), + contractVer: contractVer, contractAddr: contractAddr, }) } @@ -2371,16 +2342,19 @@ func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, non var contractVer uint32 // require a consistent version since this is a single transaction secrets := make([][32]byte, 0, n) + locators := make([][]byte, 0, n) var redeemedValue uint64 for i, redemption := range form.Redemptions { // NOTE: redemption.Spends.SecretHash is a dup of the hash extracted // from redemption.Spends.Contract. Even for scriptable UTXO assets, the // redeem script in this Contract field is redundant with the SecretHash // field as ExtractSwapDetails can be applied to extract the hash. - ver, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract) + ver, locator, err := dexeth.DecodeContractData(redemption.Spends.Contract) if err != nil { return fail(fmt.Errorf("Redeem: invalid versioned swap contract data: %w", err)) } + + locators = append(locators, locator) if i == 0 { contractVer = ver } else if contractVer != ver { @@ -2395,23 +2369,23 @@ func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, non var secret [32]byte copy(secret[:], redemption.Secret) secrets = append(secrets, secret) - redeemable, err := w.isRedeemable(secretHash, secret, ver) + redeemable, err := w.isRedeemable(locator, secret, ver) if err != nil { return fail(fmt.Errorf("Redeem: failed to check if swap is redeemable: %w", err)) } if !redeemable { - return fail(fmt.Errorf("Redeem: secretHash %x not redeemable with secret %x", - secretHash, secret)) + return fail(fmt.Errorf("Redeem: version %d locator %x not redeemable with secret %x", + ver, locator, secret)) } - swapData, err := w.swap(w.ctx, secretHash, ver) + status, vector, err := w.statusAndVector(w.ctx, locator, contractVer) if err != nil { return nil, nil, 0, fmt.Errorf("error finding swap state: %w", err) } - if swapData.State != dexeth.SSInitiated { + if status.Step != dexeth.SSInitiated { return nil, nil, 0, asset.ErrSwapNotInitiated } - redeemedValue += w.atomize(swapData.Value) + redeemedValue += w.atomize(vector.Value) } g := w.gases(contractVer) @@ -2519,7 +2493,7 @@ func recoverPubkey(msgHash, sig []byte) ([]byte, error) { // tokenBalance checks the token balance of the account handled by the wallet. func (w *assetWallet) tokenBalance() (bal *big.Int, err error) { // We don't care about the version. - return bal, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error { + return bal, w.withTokenContractor(w.assetID, dexeth.ContractVersionERC20, func(c tokenContractor) error { bal, err = c.balance(w.ctx) return err }) @@ -2527,8 +2501,8 @@ func (w *assetWallet) tokenBalance() (bal *big.Int, err error) { // tokenAllowance checks the amount of tokens that the swap contract is approved // to spend on behalf of the account handled by the wallet. -func (w *assetWallet) tokenAllowance(version uint32) (allowance *big.Int, err error) { - return allowance, w.withTokenContractor(w.assetID, version, func(c tokenContractor) error { +func (w *assetWallet) tokenAllowance(contractVer uint32) (allowance *big.Int, err error) { + return allowance, w.withTokenContractor(w.assetID, contractVer, func(c tokenContractor) error { allowance, err = c.allowance(w.ctx) return err }) @@ -2555,16 +2529,20 @@ func (w *assetWallet) approveToken(ctx context.Context, amount *big.Int, gasLimi }) } -func (w *assetWallet) approvalStatus(version uint32) (asset.ApprovalStatus, error) { +func (w *assetWallet) approvalStatus(assetVer uint32) (asset.ApprovalStatus, error) { if w.assetID == w.baseChainID { return asset.Approved, nil } + contractVer := contractVersion(assetVer) + // If the result has been cached, return what is in the cache. // The cache is cleared if an approval/unapproval tx is done. w.approvalsMtx.RLock() - if approved, cached := w.approvalCache[version]; cached { - w.approvalsMtx.RUnlock() + approved, cached := w.approvalCache[contractVer] + _, pending := w.pendingApprovals[contractVer] + w.approvalsMtx.RUnlock() + if cached { if approved { return asset.Approved, nil } else { @@ -2572,24 +2550,22 @@ func (w *assetWallet) approvalStatus(version uint32) (asset.ApprovalStatus, erro } } - if _, pending := w.pendingApprovals[version]; pending { - w.approvalsMtx.RUnlock() + if pending { return asset.Pending, nil } - w.approvalsMtx.RUnlock() w.approvalsMtx.Lock() defer w.approvalsMtx.Unlock() - currentAllowance, err := w.tokenAllowance(version) + currentAllowance, err := w.tokenAllowance(contractVer) if err != nil { return asset.NotApproved, fmt.Errorf("error retrieving current allowance: %w", err) } if currentAllowance.Cmp(unlimitedAllowanceReplenishThreshold) >= 0 { - w.approvalCache[version] = true + w.approvalCache[contractVer] = true return asset.Approved, nil } - w.approvalCache[version] = false + w.approvalCache[contractVer] = false return asset.NotApproved, nil } @@ -2719,16 +2695,14 @@ func (w *TokenWallet) ApprovalFee(assetVer uint32, approve bool) (uint64, error) // ApprovalStatus returns the approval status for each version of the // token's swap contract. func (w *TokenWallet) ApprovalStatus() map[uint32]asset.ApprovalStatus { - versions := w.Info().SupportedVersions - statuses := map[uint32]asset.ApprovalStatus{} - for _, version := range versions { - status, err := w.approvalStatus(version) + for _, assetVer := range w.wi.SupportedVersions { + status, err := w.approvalStatus(assetVer) if err != nil { - w.log.Errorf("error checking approval status for version %d: %w", version, err) + w.log.Errorf("error checking approval status for swap contract version %d: %w", assetVer, err) continue } - statuses[version] = status + statuses[assetVer] = status } return statuses @@ -2754,8 +2728,8 @@ func (w *ETHWallet) ReserveNRedemptions(n uint64, ver uint32, maxFeeRate uint64) // ReserveNRedemptions locks funds for redemption. It is an error if there // is insufficient spendable balance. // Part of the AccountLocker interface. -func (w *TokenWallet) ReserveNRedemptions(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) { - g := w.gases(ver) +func (w *TokenWallet) ReserveNRedemptions(n uint64, assetVer uint32, maxFeeRate uint64) (uint64, error) { + g := w.gases(contractVersion(assetVer)) if g == nil { return 0, fmt.Errorf("no gas table") } @@ -2800,8 +2774,8 @@ func (w *TokenWallet) ReReserveRedemption(req uint64) error { // ReserveNRefunds locks funds for doing refunds. It is an error if there // is insufficient spendable balance. Part of the AccountLocker interface. -func (w *ETHWallet) ReserveNRefunds(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) { - g := w.gases(ver) +func (w *ETHWallet) ReserveNRefunds(n uint64, assetVer uint32, maxFeeRate uint64) (uint64, error) { + g := w.gases(contractVersion(assetVer)) if g == nil { return 0, errors.New("no gas table") } @@ -2810,8 +2784,8 @@ func (w *ETHWallet) ReserveNRefunds(n uint64, ver uint32, maxFeeRate uint64) (ui // ReserveNRefunds locks funds for doing refunds. It is an error if there // is insufficient spendable balance. Part of the AccountLocker interface. -func (w *TokenWallet) ReserveNRefunds(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) { - g := w.gases(ver) +func (w *TokenWallet) ReserveNRefunds(n uint64, assetVer uint32, maxFeeRate uint64) (uint64, error) { + g := w.gases(contractVersion(assetVer)) if g == nil { return 0, errors.New("no gas table") } @@ -2887,32 +2861,73 @@ func (w *assetWallet) AuditContract(coinID, contract, serializedTx dex.Bytes, re return nil, fmt.Errorf("AuditContract: coin id != txHash - coin id: %x, txHash: %s", coinID, tx.Hash()) } - version, secretHash, err := dexeth.DecodeContractData(contract) + version, locator, err := dexeth.DecodeContractData(contract) if err != nil { return nil, fmt.Errorf("AuditContract: failed to decode contract data: %w", err) } - initiations, err := dexeth.ParseInitiateData(tx.Data(), version) - if err != nil { - return nil, fmt.Errorf("AuditContract: failed to parse initiate data: %w", err) - } + var val uint64 + var participant string + var lockTime time.Time + var secretHashB []byte + switch version { + case 0: + initiations, err := dexeth.ParseInitiateDataV0(tx.Data()) + if err != nil { + return nil, fmt.Errorf("AuditContract: failed to parse initiate data: %w", err) + } + + secretHash, err := dexeth.ParseV0Locator(locator) + if err != nil { + return nil, fmt.Errorf("error parsing v0 locator (%x): %w", locator, err) + } - initiation, ok := initiations[secretHash] - if !ok { - return nil, errors.New("AuditContract: tx does not initiate secret hash") + initiation, ok := initiations[secretHash] + if !ok { + return nil, errors.New("AuditContract: tx does not initiate secret hash") + } + val = w.atomize(initiation.Value) + participant = initiation.Participant.String() + lockTime = initiation.LockTime + secretHashB = secretHash[:] + case 1: + vec, err := dexeth.ParseV1Locator(locator) + if err != nil { + return nil, err + } + tokenAddr, txVectors, err := dexeth.ParseInitiateDataV1(tx.Data()) + if err != nil { + return nil, fmt.Errorf("AuditContract: failed to parse initiate data: %w", err) + } + if tokenAddr != w.tokenAddr { + return nil, fmt.Errorf("address in init tx data is incorrect. %s != %s", tokenAddr, w.tokenAddr) + } + txVec, ok := txVectors[vec.SecretHash] + if !ok { + return nil, errors.New("AuditContract: tx does not initiate secret hash") + } + if !dexeth.CompareVectors(vec, txVec) { + return nil, fmt.Errorf("tx vector doesn't match expectation. %+v != %+v", txVec, vec) + } + val = w.atomize(vec.Value) + participant = vec.To.String() + lockTime = time.Unix(int64(vec.LockTime), 0) + secretHashB = vec.SecretHash[:] + default: + return nil, fmt.Errorf("unknown contract version %d", version) } coin := &coin{ id: txHash, - value: w.atomize(initiation.Value), + value: val, } return &asset.AuditInfo{ - Recipient: initiation.Participant.Hex(), - Expiration: initiation.LockTime, + Recipient: participant, + Expiration: lockTime, Coin: coin, Contract: contract, - SecretHash: secretHash[:], + SecretHash: secretHashB, }, nil } @@ -2930,26 +2945,25 @@ func (w *assetWallet) LockTimeExpired(ctx context.Context, lockTime time.Time) ( // ContractLockTimeExpired returns true if the specified contract's locktime has // expired, making it possible to issue a Refund. func (w *assetWallet) ContractLockTimeExpired(ctx context.Context, contract dex.Bytes) (bool, time.Time, error) { - contractVer, secretHash, err := dexeth.DecodeContractData(contract) + contractVer, locator, err := dexeth.DecodeContractData(contract) if err != nil { return false, time.Time{}, err } - swap, err := w.swap(ctx, secretHash, contractVer) + status, vec, err := w.statusAndVector(ctx, locator, contractVer) if err != nil { return false, time.Time{}, err - } - - // Time is not yet set for uninitiated swaps. - if swap.State == dexeth.SSNone { + } else if status.Step == dexeth.SSNone { return false, time.Time{}, asset.ErrSwapNotInitiated } - expired, err := w.LockTimeExpired(ctx, swap.LockTime) + lockTime := time.Unix(int64(vec.LockTime), 0) + + expired, err := w.LockTimeExpired(ctx, lockTime) if err != nil { return false, time.Time{}, err } - return expired, swap.LockTime, nil + return expired, lockTime, nil } // findRedemptionResult is used internally for queued findRedemptionRequests. @@ -2967,22 +2981,21 @@ type findRedemptionRequest struct { // sendFindRedemptionResult sends the result or logs a message if it cannot be // sent. -func (eth *baseWallet) sendFindRedemptionResult(req *findRedemptionRequest, secretHash [32]byte, - secret []byte, makerAddr string, err error) { +func (eth *baseWallet) sendFindRedemptionResult(req *findRedemptionRequest, locator, secret []byte, makerAddr string, err error) { select { case req.res <- &findRedemptionResult{secret: secret, makerAddr: makerAddr, err: err}: default: - eth.log.Info("findRedemptionResult channel blocking for request %s", secretHash) + eth.log.Info("findRedemptionResult channel blocking for request %x", locator) } } // findRedemptionRequests creates a copy of the findRedemptionReqs map. -func (w *assetWallet) findRedemptionRequests() map[[32]byte]*findRedemptionRequest { +func (w *assetWallet) findRedemptionRequests() map[string]*findRedemptionRequest { w.findRedemptionMtx.RLock() defer w.findRedemptionMtx.RUnlock() - reqs := make(map[[32]byte]*findRedemptionRequest, len(w.findRedemptionReqs)) - for secretHash, req := range w.findRedemptionReqs { - reqs[secretHash] = req + reqs := make(map[string]*findRedemptionRequest, len(w.findRedemptionReqs)) + for loc, req := range w.findRedemptionReqs { + reqs[loc] = req } return reqs } @@ -2999,13 +3012,13 @@ func (w *assetWallet) FindRedemption(ctx context.Context, _, contract dex.Bytes) // contract, so we are basically doing the next best thing here. const coinIDTmpl = coinIDTakerFoundMakerRedemption + "%s" - contractVer, secretHash, err := dexeth.DecodeContractData(contract) + contractVer, locator, err := dexeth.DecodeContractData(contract) if err != nil { return nil, nil, err } // See if it's ready right away. - secret, makerAddr, err := w.findSecret(secretHash, contractVer) + secret, makerAddr, err := w.findSecret(locator, contractVer) if err != nil { return nil, nil, err } @@ -3020,14 +3033,16 @@ func (w *assetWallet) FindRedemption(ctx context.Context, _, contract dex.Bytes) res: make(chan *findRedemptionResult, 1), } + locatorKey := string(locator) + w.findRedemptionMtx.Lock() - if w.findRedemptionReqs[secretHash] != nil { + if w.findRedemptionReqs[locatorKey] != nil { w.findRedemptionMtx.Unlock() - return nil, nil, fmt.Errorf("duplicate find redemption request for %x", secretHash) + return nil, nil, fmt.Errorf("duplicate find redemption request for %x", locator) } - w.findRedemptionReqs[secretHash] = req + w.findRedemptionReqs[locatorKey] = req w.findRedemptionMtx.Unlock() @@ -3038,11 +3053,11 @@ func (w *assetWallet) FindRedemption(ctx context.Context, _, contract dex.Bytes) } w.findRedemptionMtx.Lock() - delete(w.findRedemptionReqs, secretHash) + delete(w.findRedemptionReqs, locatorKey) w.findRedemptionMtx.Unlock() if res == nil { - return nil, nil, fmt.Errorf("context cancelled for find redemption request %x", secretHash) + return nil, nil, fmt.Errorf("context cancelled for find redemption request %x", locator) } if res.err != nil { @@ -3052,64 +3067,61 @@ func (w *assetWallet) FindRedemption(ctx context.Context, _, contract dex.Bytes) return dex.Bytes(fmt.Sprintf(coinIDTmpl, res.makerAddr)), res.secret[:], nil } -// findSecret returns redemption secret from smart contract that Maker put there -// redeeming Taker swap along with Maker Ethereum account address. Returns empty -// values if Maker hasn't redeemed yet. -func (w *assetWallet) findSecret(secretHash [32]byte, contractVer uint32) ([]byte, string, error) { +func (w *assetWallet) findSecret(locator []byte, contractVer uint32) ([]byte, string, error) { ctx, cancel := context.WithTimeout(w.ctx, 10*time.Second) defer cancel() - swap, err := w.swap(ctx, secretHash, contractVer) + status, vector, err := w.statusAndVector(ctx, locator, contractVer) if err != nil { return nil, "", err } - switch swap.State { + switch status.Step { case dexeth.SSInitiated: return nil, "", nil // no Maker redeem yet, but keep checking case dexeth.SSRedeemed: - return swap.Secret[:], swap.Initiator.String(), nil + return status.Secret[:], vector.From.String(), nil case dexeth.SSNone: - return nil, "", fmt.Errorf("swap %x does not exist", secretHash) + return nil, "", fmt.Errorf("swap %x does not exist", locator) case dexeth.SSRefunded: - return nil, "", fmt.Errorf("swap %x is already refunded", secretHash) + return nil, "", fmt.Errorf("swap %x is already refunded", locator) } - return nil, "", fmt.Errorf("unrecognized swap state %v", swap.State) + return nil, "", fmt.Errorf("unrecognized swap state %v", status.Step) } // Refund refunds a contract. This can only be used after the time lock has // expired. func (w *assetWallet) Refund(_, contract dex.Bytes, feeRate uint64) (dex.Bytes, error) { - version, secretHash, err := dexeth.DecodeContractData(contract) + contractVer, locator, err := dexeth.DecodeContractData(contract) if err != nil { return nil, fmt.Errorf("Refund: failed to decode contract: %w", err) } - swap, err := w.swap(w.ctx, secretHash, version) + status, vector, err := w.statusAndVector(w.ctx, locator, contractVer) if err != nil { return nil, err } // It's possible the swap was refunded by someone else. In that case we // cannot know the refunding tx hash. - switch swap.State { + switch status.Step { case dexeth.SSInitiated: // good, check refundability case dexeth.SSNone: return nil, asset.ErrSwapNotInitiated case dexeth.SSRefunded: - w.log.Infof("Swap with secret hash %x already refunded.", secretHash) + w.log.Infof("Swap with locator %x already refunded.", locator) zeroHash := common.Hash{} return zeroHash[:], nil case dexeth.SSRedeemed: - w.log.Infof("Swap with secret hash %x already redeemed with secret key %x.", - secretHash, swap.Secret) + w.log.Infof("Swap with locator %x already redeemed with secret key %x.", + locator, status.Secret) return nil, asset.CoinNotFoundError // so caller knows to FindRedemption } - refundable, err := w.isRefundable(secretHash, version) + refundable, err := w.isRefundable(locator, contractVer) if err != nil { return nil, fmt.Errorf("Refund: failed to check isRefundable: %w", err) } if !refundable { - return nil, fmt.Errorf("Refund: swap with secret hash %x is not refundable", secretHash) + return nil, fmt.Errorf("Refund: swap with locator %x is not refundable", locator) } maxFeeRate := dexeth.GweiToWei(feeRate) @@ -3118,7 +3130,7 @@ func (w *assetWallet) Refund(_, contract dex.Bytes, feeRate uint64) (dex.Bytes, return nil, fmt.Errorf("Refund: failed to get network tip cap: %w", err) } - tx, err := w.refund(secretHash, w.atomize(swap.Value), maxFeeRate, tipRate, version) + tx, err := w.refund(locator, w.atomize(vector.Value), maxFeeRate, tipRate, contractVer) if err != nil { return nil, fmt.Errorf("Refund: failed to call refund: %w", err) } @@ -3236,7 +3248,7 @@ func (w *TokenWallet) canSend(value uint64, verifyBalance, isPreEstimate bool) ( } maxFeeRateGwei := dexeth.WeiToGweiCeil(maxFeeRate) - g := w.gases(contractVersionNewest) + g := w.gases(dexeth.ContractVersionERC20) if g == nil { return 0, nil, nil, fmt.Errorf("gas table not found") } @@ -3307,7 +3319,7 @@ func (w *TokenWallet) EstimateSendTxFee(addr string, value, _ uint64, _, maxWith // StandardSendFees returns the fees for a simple send tx. func (w *TokenWallet) StandardSendFee(feeRate uint64) uint64 { - g := w.gases(contractVersionNewest) + g := w.gases(dexeth.ContractVersionNewest) if g == nil { w.log.Errorf("error getting gases for token %s", w.token.Name) return 0 @@ -3350,24 +3362,34 @@ func (w *assetWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, c ctx, cancel := context.WithTimeout(ctx, confCheckTimeout) defer cancel() - swapData, err := w.swap(ctx, secretHash, contractVer) + tip := w.tipHeight() + + status, err := w.status(ctx, secretHash, contractVer) if err != nil { return 0, false, fmt.Errorf("error finding swap state: %w", err) } - if swapData.State == dexeth.SSNone { - // Check if we know about the tx ourselves. If it's not in pendingTxs - // or the database, assume it's lost. + if status.Step == dexeth.SSNone { return 0, false, asset.ErrSwapNotInitiated } - spent = swapData.State >= dexeth.SSRedeemed - tip := w.tipHeight() + spent = status.Step >= dexeth.SSRedeemed + if spent && contractVer == 1 { + // Gotta get the confirimations directly. + var txHash common.Hash + copy(txHash[:], coinID) + confs, err = w.node.transactionConfirmations(ctx, txHash) + if err != nil { + return 0, false, fmt.Errorf("error finding swap state: %w", err) + } + return + } + // TODO: If tip < swapData.BlockHeight (which has been observed), what does // that mean? Are we using the wrong provider in a multi-provider setup? How // do we resolve provider relevance? - if tip >= swapData.BlockHeight { - confs = uint32(tip - swapData.BlockHeight + 1) + if tip >= status.BlockHeight { + confs = uint32(w.tipHeight() - status.BlockHeight + 1) } return } @@ -3469,37 +3491,6 @@ func (eth *assetWallet) DynamicRedemptionFeesPaid(ctx context.Context, coinID, c return eth.swapOrRedemptionFeesPaid(ctx, coinID, contractData, false) } -// extractSecretHashes extracts the secret hashes from the reedeem or swap tx -// data. The returned hashes are sorted lexicographically. -func extractSecretHashes(isInit bool, txData []byte, contractVer uint32) (secretHashes [][]byte, _ error) { - defer func() { - sort.Slice(secretHashes, func(i, j int) bool { return bytes.Compare(secretHashes[i], secretHashes[j]) < 0 }) - }() - if isInit { - inits, err := dexeth.ParseInitiateData(txData, contractVer) - if err != nil { - return nil, fmt.Errorf("invalid initiate data: %v", err) - } - secretHashes = make([][]byte, 0, len(inits)) - for k := range inits { - copyK := k - secretHashes = append(secretHashes, copyK[:]) - } - return secretHashes, nil - } - // redeem - redeems, err := dexeth.ParseRedeemData(txData, contractVer) - if err != nil { - return nil, fmt.Errorf("invalid redeem data: %v", err) - } - secretHashes = make([][]byte, 0, len(redeems)) - for k := range redeems { - copyK := k - secretHashes = append(secretHashes, copyK[:]) - } - return secretHashes, nil -} - // swapOrRedemptionFeesPaid returns exactly how much gwei was used to send an // initiation or redemption transaction. It also returns the secret hashes // included with this init or redeem. Secret hashes are sorted so returns are @@ -3514,16 +3505,15 @@ func (w *baseWallet) swapOrRedemptionFeesPaid( coinID dex.Bytes, contractData dex.Bytes, isInit bool, -) (fee uint64, secretHashes [][]byte, err error) { - - var txHash common.Hash - copy(txHash[:], coinID) - - contractVer, secretHash, err := dexeth.DecodeContractData(contractData) +) (fee uint64, locators [][]byte, err error) { + contractVer, locator, err := dexeth.DecodeContractData(contractData) if err != nil { return 0, nil, err } + var txHash common.Hash + copy(txHash[:], coinID) + tip := w.tipHeight() var blockNum uint64 @@ -3539,7 +3529,7 @@ func (w *baseWallet) swapOrRedemptionFeesPaid( if confs := safeConfs(tip, blockNum); confs < w.finalizeConfs { return 0, nil, asset.ErrNotEnoughConfirms } - secretHashes, err = extractSecretHashes(isInit, tx.Data(), contractVer) + locators, _, err = extractSecretHashes(tx, contractVer, isInit) return } @@ -3557,21 +3547,84 @@ func (w *baseWallet) swapOrRedemptionFeesPaid( bigFees := new(big.Int).Mul(receipt.EffectiveGasPrice, big.NewInt(int64(receipt.GasUsed))) fee = dexeth.WeiToGweiCeil(bigFees) - secretHashes, err = extractSecretHashes(isInit, tx.Data(), contractVer) + locators, _, err = extractSecretHashes(tx, contractVer, isInit) if err != nil { return 0, nil, err } + + sort.Slice(locators, func(i, j int) bool { return bytes.Compare(locators[i], locators[j]) < 0 }) var found bool - for i := range secretHashes { - if bytes.Equal(secretHash[:], secretHashes[i]) { + for i := range locators { + if bytes.Equal(locator, locators[i]) { found = true break } } if !found { - return 0, nil, fmt.Errorf("secret hash %x not found in transaction", secretHash) + return 0, nil, fmt.Errorf("locator %x not found in transaction", locator) + } + return dexeth.WeiToGweiCeil(bigFees), locators, nil +} + +// extractSecretHashes extracts the secret hashes from the reedeem or swap tx +// data. The returned hashes are sorted lexicographically. +func extractSecretHashes(tx *types.Transaction, contractVer uint32, isInit bool) (locators, secretHashes [][]byte, err error) { + defer func() { + sort.Slice(secretHashes, func(i, j int) bool { return bytes.Compare(secretHashes[i], secretHashes[j]) < 0 }) + }() + + switch contractVer { + case 0: + if isInit { + inits, err := dexeth.ParseInitiateDataV0(tx.Data()) + if err != nil { + return nil, nil, fmt.Errorf("invalid initiate data: %v", err) + } + locators = make([][]byte, 0, len(inits)) + for k := range inits { + copyK := k + locators = append(locators, copyK[:]) + } + } else { + redeems, err := dexeth.ParseRedeemDataV0(tx.Data()) + if err != nil { + return nil, nil, fmt.Errorf("invalid redeem data: %v", err) + } + locators = make([][]byte, 0, len(redeems)) + for k := range redeems { + copyK := k + locators = append(locators, copyK[:]) + } + } + return locators, locators, nil + case 1: + if isInit { + _, vectors, err := dexeth.ParseInitiateDataV1(tx.Data()) + if err != nil { + return nil, nil, fmt.Errorf("invalid initiate data: %v", err) + } + locators = make([][]byte, 0, len(vectors)) + secretHashes = make([][]byte, 0, len(vectors)) + for _, vec := range vectors { + locators = append(locators, vec.Locator()) + secretHashes = append(secretHashes, vec.SecretHash[:]) + } + } else { + _, redeems, err := dexeth.ParseRedeemDataV1(tx.Data()) + if err != nil { + return nil, nil, fmt.Errorf("invalid redeem data: %v", err) + } + locators = make([][]byte, 0, len(redeems)) + secretHashes = make([][]byte, 0, len(redeems)) + for secretHash, r := range redeems { + locators = append(locators, r.Contract.Locator()) + secretHashes = append(secretHashes, secretHash[:]) + } + } + return locators, secretHashes, nil + default: + return nil, nil, fmt.Errorf("unknown server version %d", contractVer) } - return dexeth.WeiToGweiCeil(bigFees), secretHashes, nil } // RegFeeConfirmations gets the number of confirmations for the specified @@ -3710,9 +3763,7 @@ func (eth *ETHWallet) checkForNewBlocks(ctx context.Context) { bestHash := bestHdr.Hash() // This method is called frequently. Don't hold write lock // unless tip has changed. - eth.tipMtx.RLock() - currentTipHash := eth.currentTip.Hash() - eth.tipMtx.RUnlock() + currentTipHash := eth.tip().Hash() if currentTipHash == bestHash { return } @@ -3768,7 +3819,7 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede var txHash common.Hash copy(txHash[:], coinID) - contractVer, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract) + contractVer, locator, err := dexeth.DecodeContractData(redemption.Spends.Contract) if err != nil { return nil, fmt.Errorf("failed to decode contract data: %w", err) } @@ -3823,11 +3874,11 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede } // We weren't able to redeem. Perhaps fees were too low, but we'll // check the status in the contract for a couple of other conditions. - swap, err := w.swap(w.ctx, secretHash, contractVer) + status, err := w.status(w.ctx, locator, contractVer) if err != nil { return nil, fmt.Errorf("error pulling swap data from contract: %v", err) } - switch swap.State { + switch status.Step { case dexeth.SSRedeemed: w.log.Infof("Redemption in tx %s was apparently redeemed by another tx. OK.", txHash) return confStatus(w.finalizeConfs, w.finalizeConfs, txHash), nil @@ -3901,15 +3952,16 @@ func (w *baseWallet) localTxStatus(txHash common.Hash) (_ bool, s *walletTxStatu // checkFindRedemptions checks queued findRedemptionRequests. func (w *assetWallet) checkFindRedemptions() { - for secretHash, req := range w.findRedemptionRequests() { + for loc, req := range w.findRedemptionRequests() { if w.ctx.Err() != nil { return } - secret, makerAddr, err := w.findSecret(secretHash, req.contractVer) + locator := []byte(loc) + secret, makerAddr, err := w.findSecret(locator, req.contractVer) if err != nil { - w.sendFindRedemptionResult(req, secretHash, nil, "", err) + w.sendFindRedemptionResult(req, locator, nil, "", err) } else if len(secret) > 0 { - w.sendFindRedemptionResult(req, secretHash, secret, makerAddr, nil) + w.sendFindRedemptionResult(req, locator, secret, makerAddr, nil) } } } @@ -4060,6 +4112,10 @@ func (w *assetWallet) getConfirmedBalance() (*big.Int, error) { return reqBal, nil } +func (w *assetWallet) contractors() map[uint32]contractor { + return map[uint32]contractor{0: w.contractorV0, 1: w.contractorV1} +} + func (w *assetWallet) balanceWithTxPool() (*Balance, error) { isToken := w.assetID != w.baseChainID confirmed, err := w.getConfirmedBalance() @@ -4129,7 +4185,7 @@ func (w *assetWallet) balanceWithTxPool() (*Balance, error) { } var contractOut uint64 - for ver, c := range w.contractors { + for ver, c := range w.contractors() { in, out, err := c.value(w.ctx, tx) if err != nil { w.log.Errorf("version %d contractor incomingValue error: %v", ver, err) @@ -4228,7 +4284,7 @@ func (w *ETHWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate, tipR // sendToAddr sends funds to the address. func (w *TokenWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate, tipRate *big.Int) (tx *types.Transaction, err error) { - g := w.gases(contractVersionNewest) + g := w.gases(dexeth.ContractVersionERC20) if g == nil { return nil, fmt.Errorf("no gas table") } @@ -4242,7 +4298,7 @@ func (w *TokenWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate, ti txType = asset.SelfSend } recipient := addr.Hex() - return tx, txType, amt, &recipient, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error { + return tx, txType, amt, &recipient, w.withTokenContractor(w.assetID, dexeth.ContractVersionERC20, func(c tokenContractor) error { tx, err = c.transfer(txOpts, addr, w.evmify(amt)) if err != nil { return err @@ -4253,10 +4309,27 @@ func (w *TokenWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate, ti } -// swap gets a swap keyed by secretHash in the contract. -func (w *assetWallet) swap(ctx context.Context, secretHash [32]byte, contractVer uint32) (swap *dexeth.SwapState, err error) { - return swap, w.withContractor(contractVer, func(c contractor) error { - swap, err = c.swap(ctx, secretHash) +// status fetches the SwapStatus for the locator and contract version. +func (w *assetWallet) status(ctx context.Context, locator []byte, contractVer uint32) (s *dexeth.SwapStatus, err error) { + return s, w.withContractor(contractVer, func(c contractor) error { + s, err = c.status(ctx, locator) + return err + }) +} + +// vector fetches the SwapVector for the locator and contract version. +func (w *assetWallet) vector(ctx context.Context, locator []byte, contractVer uint32) (v *dexeth.SwapVector, err error) { + return v, w.withContractor(contractVer, func(c contractor) error { + v, err = c.vector(ctx, locator) + return err + }) +} + +// statusAndVector fetches the SwapStatus and SwapVector for the locator and +// contract version. +func (w *assetWallet) statusAndVector(ctx context.Context, locator []byte, contractVer uint32) (s *dexeth.SwapStatus, v *dexeth.SwapVector, err error) { + return s, v, w.withContractor(contractVer, func(c contractor) error { + s, v, err = c.statusAndVector(ctx, locator) return err }) } @@ -4300,23 +4373,23 @@ func (w *assetWallet) estimateInitGas(ctx context.Context, numSwaps int, contrac // nodeclient_harness_test.go suite (GetGasEstimates, testRedeemGas, etc.). // Never use this with a public RPC provider, especially as maker, since it // reveals the secret keys. -func (w *assetWallet) estimateRedeemGas(ctx context.Context, secrets [][32]byte, contractVer uint32) (gas uint64, err error) { +func (w *assetWallet) estimateRedeemGas(ctx context.Context, secrets [][32]byte, locators [][]byte, contractVer uint32) (gas uint64, err error) { return gas, w.withContractor(contractVer, func(c contractor) error { - gas, err = c.estimateRedeemGas(ctx, secrets) + gas, err = c.estimateRedeemGas(ctx, secrets, locators) return err }) } // estimateRefundGas checks the amount of gas that is used for a refund. -func (w *assetWallet) estimateRefundGas(ctx context.Context, secretHash [32]byte, contractVer uint32) (gas uint64, err error) { +func (w *assetWallet) estimateRefundGas(ctx context.Context, locator []byte, contractVer uint32) (gas uint64, err error) { return gas, w.withContractor(contractVer, func(c contractor) error { - gas, err = c.estimateRefundGas(ctx, secretHash) + gas, err = c.estimateRefundGas(ctx, locator) return err }) } // loadContractors prepares the token contractors and add them to the map. -func (w *assetWallet) loadContractors() error { +func (w *assetWallet) loadContractors(parent *assetWallet) error { token, found := w.tokens[w.assetID] if !found { return fmt.Errorf("token %d not found", w.assetID) @@ -4326,32 +4399,41 @@ func (w *assetWallet) loadContractors() error { return fmt.Errorf("token %d not found", w.assetID) } - for ver := range netToken.SwapContracts { - constructor, found := tokenContractorConstructors[ver] - if !found { - w.log.Errorf("contractor constructor not found for token %s, version %d", token.Name, ver) - continue - } - c, err := constructor(w.net, token, w.addr, w.node.contractBackend()) + if _, found := netToken.SwapContracts[0]; found { + c, err := newV0TokenContractor(w.net, token, w.addr, w.node.contractBackend()) if err != nil { - return fmt.Errorf("error constructing token %s contractor version %d: %w", token.Name, ver, err) + return fmt.Errorf("error constructing token %s contractor version 0: %w", token.Name, err) } - if netToken.Address != c.tokenAddress() { return fmt.Errorf("wrong %s token address. expected %s, got %s", token.Name, netToken.Address, c.tokenAddress()) } + w.contractorV0 = c + } - w.contractors[ver] = c + if _, found := netToken.SwapContracts[1]; found { + if parent.contractorV1 == nil { + return errors.New("can't construct version 1 contractor if parent doesn't have the unified contractor") + } + cgen, ok := parent.contractorV1.(unifiedContractor) + if !ok { + return errors.New("parent contractor ain't unified") + } + c, err := cgen.tokenContractor(token) + if err != nil { + return fmt.Errorf("error constructing version 1 token %s contractor: %w", token.Name, err) + } + w.contractorV1 = c } return nil } // withContractor runs the provided function with the versioned contractor. func (w *assetWallet) withContractor(contractVer uint32, f func(contractor) error) error { - if contractVer == contractVersionNewest { + if contractVer == dexeth.ContractVersionERC20 { + // For ERC20 methods, use the most recent contractor version. var bestVer uint32 var bestContractor contractor - for ver, c := range w.contractors { + for ver, c := range w.contractors() { if ver >= bestVer { bestContractor = c bestVer = ver @@ -4359,19 +4441,28 @@ func (w *assetWallet) withContractor(contractVer uint32, f func(contractor) erro } return f(bestContractor) } - contractor, found := w.contractors[contractVer] - if !found { - return fmt.Errorf("no version %d contractor for asset %d", contractVer, w.assetID) + var c contractor + switch contractVer { + case 0: + if w.contractorV0 == nil { + return errors.New("no version 0 contractor") + } + c = w.contractorV0 + case 1: + if w.contractorV1 == nil { + return errors.New("no version 1 contractor") + } + c = w.contractorV1 } - return f(contractor) + return f(c) } // withTokenContractor runs the provided function with the tokenContractor. -func (w *assetWallet) withTokenContractor(assetID, ver uint32, f func(tokenContractor) error) error { - return w.withContractor(ver, func(c contractor) error { +func (w *assetWallet) withTokenContractor(assetID, contractVer uint32, f func(tokenContractor) error) error { + return w.withContractor(contractVer, func(c contractor) error { tc, is := c.(tokenContractor) if !is { - return fmt.Errorf("contractor for %d %T is not a tokenContractor", assetID, c) + return fmt.Errorf("contractor for %s version %d is not a tokenContractor. type = %T", w.ui.Conventional.Unit, contractVer, c) } return f(tc) }) @@ -4379,22 +4470,13 @@ func (w *assetWallet) withTokenContractor(assetID, ver uint32, f func(tokenContr // estimateApproveGas estimates the gas required for a transaction approving a // spender for an ERC20 contract. -func (w *assetWallet) estimateApproveGas(newGas *big.Int) (gas uint64, err error) { - return gas, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error { +func (w *assetWallet) estimateApproveGas(contractVer uint32, newGas *big.Int) (gas uint64, err error) { + return gas, w.withTokenContractor(w.assetID, contractVer, func(c tokenContractor) error { gas, err = c.estimateApproveGas(w.ctx, newGas) return err }) } -// estimateTransferGas estimates the gas needed for a token transfer call to an -// ERC20 contract. -func (w *assetWallet) estimateTransferGas(val uint64) (gas uint64, err error) { - return gas, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error { - gas, err = c.estimateTransferGas(w.ctx, w.evmify(val)) - return err - }) -} - // Can uncomment here and in redeem to test rejected redemption reauthorization. // var firstRedemptionBorked atomic.Bool @@ -4443,7 +4525,7 @@ func (w *assetWallet) redeem( // refund refunds a swap contract using the account controlled by the wallet. // Any on-chain failure, such as the locktime not being past, will not cause // this to error. -func (w *assetWallet) refund(secretHash [32]byte, amt uint64, maxFeeRate, tipRate *big.Int, contractVer uint32) (tx *types.Transaction, err error) { +func (w *assetWallet) refund(locator []byte, amt uint64, maxFeeRate, tipRate *big.Int, contractVer uint32) (tx *types.Transaction, err error) { gas := w.gases(contractVer) if gas == nil { return nil, fmt.Errorf("no gas table for asset %d, version %d", w.assetID, contractVer) @@ -4454,30 +4536,34 @@ func (w *assetWallet) refund(secretHash [32]byte, amt uint64, maxFeeRate, tipRat return nil, 0, 0, nil, err } return tx, asset.Refund, amt, nil, w.withContractor(contractVer, func(c contractor) error { - tx, err = c.refund(txOpts, secretHash) + tx, err = c.refund(txOpts, locator) return err }) }) } -// isRedeemable checks if the swap identified by secretHash is redeemable using -// secret. This must NOT be a contractor call. -func (w *assetWallet) isRedeemable(secretHash [32]byte, secret [32]byte, contractVer uint32) (redeemable bool, err error) { - swap, err := w.swap(w.ctx, secretHash, contractVer) +// isRedeemable checks if the swap identified by secretHash is redeemable using secret. +func (w *assetWallet) isRedeemable(locator []byte, secret [32]byte, contractVer uint32) (redeemable bool, err error) { + status, err := w.status(w.ctx, locator, contractVer) if err != nil { return false, err } - if swap.State != dexeth.SSInitiated { + if status.Step != dexeth.SSInitiated { return false, nil } - return w.ValidateSecret(secret[:], secretHash[:]), nil + vector, err := w.vector(w.ctx, locator, contractVer) + if err != nil { + return false, err + } + + return w.ValidateSecret(secret[:], vector.SecretHash[:]), nil } -func (w *assetWallet) isRefundable(secretHash [32]byte, contractVer uint32) (refundable bool, err error) { +func (w *assetWallet) isRefundable(locator []byte, contractVer uint32) (refundable bool, err error) { return refundable, w.withContractor(contractVer, func(c contractor) error { - refundable, err = c.isRefundable(secretHash) + refundable, err = c.isRefundable(locator) return err }) } @@ -4485,7 +4571,6 @@ func (w *assetWallet) isRefundable(secretHash [32]byte, contractVer uint32) (ref func checkTxStatus(receipt *types.Receipt, gasLimit uint64) error { if receipt.Status != types.ReceiptStatusSuccessful { return fmt.Errorf("transaction status failed") - } if receipt.GasUsed > gasLimit { @@ -5221,7 +5306,7 @@ func (w *ETHWallet) WalletTransaction(ctx context.Context, txID string) (*asset. // transaction, finds the log that sends tokens to the wallet's address, // and returns the value of the transfer. func (w *TokenWallet) extractValueFromTransferLog(receipt *types.Receipt) (v uint64, err error) { - return v, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error { + return v, w.withTokenContractor(w.assetID, dexeth.ContractVersionERC20, func(c tokenContractor) error { v, err = c.parseTransfer(receipt) return err }) @@ -5367,18 +5452,26 @@ func quickNode(ctx context.Context, walletDir string, contractVer uint32, if ctor == nil { return nil, nil, fmt.Errorf("no contractor constructor for eth contract version %d", contractVer) } - c, err = ctor(wParams.ContractAddr, cl.address(), cl.contractBackend()) + c, err = ctor(net, wParams.ContractAddr, cl.address(), cl.contractBackend()) if err != nil { return nil, nil, fmt.Errorf("contractor constructor error: %v", err) } } else { - ctor := tokenContractorConstructors[contractVer] - if ctor == nil { - return nil, nil, fmt.Errorf("no token contractor constructor for eth contract version %d", contractVer) - } - c, err = ctor(net, wParams.Token, cl.address(), cl.contractBackend()) - if err != nil { - return nil, nil, fmt.Errorf("token contractor constructor error: %v", err) + switch contractVer { + case 0: + c, err = newV0TokenContractor(net, wParams.Token, cl.address(), cl.contractBackend()) + if err != nil { + return nil, nil, fmt.Errorf("token contractor constructor error: %v", err) + } + case 1: + bc, err := newV1Contractor(net, wParams.ContractAddr, cl.address(), cl.contractBackend()) + if err != nil { + return nil, nil, fmt.Errorf("base contractor constructor error: %v", err) + } + c, err = bc.(unifiedContractor).tokenContractor(wParams.Token) + if err != nil { + return nil, nil, fmt.Errorf("tokenContractor error: %v", err) + } } } success = true @@ -5491,9 +5584,26 @@ func (getGas) ReadCredentials(chain, credentialsPath string, net dex.Network) (a return } -func getGetGasClientWithEstimatesAndBalances(ctx context.Context, net dex.Network, contractVer uint32, maxSwaps int, - walletDir string, providers []string, seed []byte, wParams *GetGasWalletParams, log dex.Logger) (cl *multiRPCClient, c contractor, - ethReq, swapReq, feeRate uint64, ethBal, tokenBal *big.Int, err error) { +func getGetGasClientWithEstimatesAndBalances( + ctx context.Context, + net dex.Network, + contractVer uint32, + maxSwaps int, + walletDir string, + providers []string, + seed []byte, + wParams *GetGasWalletParams, + log dex.Logger, +) ( + cl *multiRPCClient, + c contractor, + ethReq, + swapReq, + feeRate uint64, + ethBal, + tokenBal *big.Int, + err error, +) { cl, c, err = quickNode(ctx, walletDir, contractVer, seed, providers, wParams, net, log) if err != nil { @@ -5744,6 +5854,7 @@ func (getGas) returnFunds( } remainder := ethBal - fees + txOpts, err := cl.txOpts(ctx, remainder, defaultSendGasLimit, maxFeeRate, tipRate, nil) if err != nil { return fmt.Errorf("error generating tx opts: %w", err) @@ -5768,6 +5879,10 @@ func (getGas) returnFunds( func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVer uint32, maxSwaps int, credentialsPath string, wParams *GetGasWalletParams, log dex.Logger) error { + if *wParams.Gas == (dexeth.Gases{}) { + return fmt.Errorf("empty gas table. put some estimates in VersionedGases or Tokens for this contract") + } + symbol := dex.BipIDSymbol(assetID) log.Infof("Getting gas estimates for up to %d swaps of asset %s, contract version %d on %s", maxSwaps, symbol, contractVer, symbol) @@ -5813,8 +5928,9 @@ func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVe var approvalClient *multiRPCClient var approvalContractor tokenContractor + evmify := dexeth.GweiToWei if isToken { - + evmify = wParams.Token.AtomicToEVM atomicBal := wParams.Token.EVMToAtomic(tokenBal) convUnit := ui.Conventional.Unit @@ -5857,7 +5973,7 @@ func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVe } log.Debugf("Getting gas estimates") - return getGasEstimates(ctx, cl, approvalClient, c, approvalContractor, maxSwaps, wParams.Gas, log) + return getGasEstimates(ctx, cl, approvalClient, c, approvalContractor, maxSwaps, contractVer, wParams.Gas, evmify, log) } // getGasEstimate is used to get a gas table for an asset's contract(s). The @@ -5872,7 +5988,7 @@ func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVe // gas estimate. These are only needed when the asset is a token. For eth, they // can be nil. func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac tokenContractor, - maxSwaps int, g *dexeth.Gases, log dex.Logger) (err error) { + maxSwaps int, contractVer uint32, g *dexeth.Gases, evmify func(v uint64) *big.Int, log dex.Logger) (err error) { tc, isToken := c.(tokenContractor) @@ -5909,6 +6025,8 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t return fmt.Errorf("error getting network fees: %v", err) } + maxFeeRate := new(big.Int).Add(tipRate, new(big.Int).Mul(baseRate, big.NewInt(2))) + defer func() { if len(stats.swaps) == 0 { return @@ -5949,10 +6067,15 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t fmt.Printf(" %+v \n", stats.transfers) }() + logTx := func(tag string, n int, tx *types.Transaction) { + log.Infof("%s %d tx, hash = %s, nonce = %d, maxFeeRate = %s, tip cap = %s", + tag, n, tx.Hash(), tx.Nonce(), tx.GasFeeCap(), tx.GasTipCap()) + } + // Estimate approve for tokens. if isToken { sendApprove := func(cl ethFetcher, c tokenContractor) error { - txOpts, err := cl.txOpts(ctx, 0, g.Approve*2, baseRate, tipRate, nil) + txOpts, err := cl.txOpts(ctx, 0, g.Approve*2, maxFeeRate, tipRate, nil) if err != nil { return fmt.Errorf("error constructing signed tx opts for approve: %w", err) } @@ -5960,6 +6083,7 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t if err != nil { return fmt.Errorf("error estimating approve gas: %w", err) } + logTx("Approve", 1, tx) if err = waitForConfirmation(ctx, "approval", cl, tx.Hash(), log); err != nil { return fmt.Errorf("error waiting for approve transaction: %w", err) } @@ -5986,7 +6110,7 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t return fmt.Errorf("error sending approve transaction for the initiator: %w", err) } - txOpts, err := cl.txOpts(ctx, 0, g.Transfer*2, baseRate, tipRate, nil) + txOpts, err := cl.txOpts(ctx, 0, g.Transfer*2, maxFeeRate, tipRate, nil) if err != nil { return fmt.Errorf("error constructing signed tx opts for transfer: %w", err) } @@ -5999,6 +6123,7 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t if err != nil { return fmt.Errorf("transfer error: %w", err) } + logTx("Transfer", 1, transferTx) if err = waitForConfirmation(ctx, "transfer", cl, transferTx.Hash(), log); err != nil { return fmt.Errorf("error waiting for transfer tx: %w", err) } @@ -6013,9 +6138,11 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t stats.transfers = append(stats.transfers, receipt.GasUsed) } + var v uint64 = 1 for n := 1; n <= maxSwaps; n++ { contracts := make([]*asset.Contract, 0, n) secrets := make([][32]byte, 0, n) + lockTime := time.Now().Add(-time.Hour) for i := 0; i < n; i++ { secretB := encode.RandomBytes(32) var secret [32]byte @@ -6023,9 +6150,9 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t secretHash := sha256.Sum256(secretB) contracts = append(contracts, &asset.Contract{ Address: cl.address().String(), // trading with self - Value: 1, + Value: v, SecretHash: secretHash[:], - LockTime: uint64(time.Now().Add(-time.Hour).Unix()), + LockTime: uint64(lockTime.Unix()), }) secrets = append(secrets, secret) } @@ -6036,7 +6163,7 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t } // Send the inits - txOpts, err := cl.txOpts(ctx, optsVal, g.SwapN(n)*2, baseRate, tipRate, nil) + txOpts, err := cl.txOpts(ctx, optsVal, g.SwapN(n)*2, maxFeeRate, tipRate, nil) if err != nil { return fmt.Errorf("error constructing signed tx opts for %d swaps: %v", n, err) } @@ -6045,6 +6172,7 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t if err != nil { return fmt.Errorf("initiate error for %d swaps: %v", n, err) } + logTx("Initiate", n, tx) if err = waitForConfirmation(ctx, "init", cl, tx.Hash(), log); err != nil { return fmt.Errorf("error waiting for init tx to be mined: %w", err) } @@ -6055,13 +6183,11 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t if err = checkTxStatus(receipt, txOpts.GasLimit); err != nil { return fmt.Errorf("init tx failed status check: %w", err) } - log.Infof("%d gas used for %d initiation txs", receipt.GasUsed, n) + log.Infof("%d gas used for %d initiations in tx %s", receipt.GasUsed, n, tx.Hash()) stats.swaps = append(stats.swaps, receipt.GasUsed) // Estimate a refund - var firstSecretHash [32]byte - copy(firstSecretHash[:], contracts[0].SecretHash) - refundGas, err := c.estimateRefundGas(ctx, firstSecretHash) + refundGas, err := c.estimateRefundGas(ctx, acToLocator(contractVer, contracts[0], evmify(v), cl.address())) if err != nil { return fmt.Errorf("error estimate refund gas: %w", err) } @@ -6072,21 +6198,25 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t for i, contract := range contracts { redemptions = append(redemptions, &asset.Redemption{ Spends: &asset.AuditInfo{ + Recipient: cl.address().String(), + Expiration: lockTime, + Contract: dexeth.EncodeContractData(contractVer, acToLocator(contractVer, contract, evmify(v), cl.address())), SecretHash: contract.SecretHash, }, Secret: secrets[i][:], }) } - txOpts, err = cl.txOpts(ctx, 0, g.RedeemN(n)*2, baseRate, tipRate, nil) + txOpts, err = cl.txOpts(ctx, 0, g.RedeemN(n)*2, maxFeeRate, tipRate, nil) if err != nil { return fmt.Errorf("error constructing signed tx opts for %d redeems: %v", n, err) } - log.Debugf("Sending %d redemption txs", n) + log.Debugf("Sending %d redemptions", n) tx, err = c.redeem(txOpts, redemptions) if err != nil { return fmt.Errorf("redeem error for %d swaps: %v", n, err) } + logTx("Redeem", n, tx) if err = waitForConfirmation(ctx, "redeem", cl, tx.Hash(), log); err != nil { return fmt.Errorf("error waiting for redeem tx to be mined: %w", err) } @@ -6097,7 +6227,7 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t if err = checkTxStatus(receipt, txOpts.GasLimit); err != nil { return fmt.Errorf("redeem tx failed status check: %w", err) } - log.Infof("%d gas used for %d redemptions", receipt.GasUsed, n) + log.Infof("%d gas used for %d redemptions in tx %s", receipt.GasUsed, n, tx.Hash()) stats.redeems = append(stats.redeems, receipt.GasUsed) } @@ -6128,7 +6258,7 @@ func newTxOpts(ctx context.Context, from common.Address, val, maxGas uint64, max } func gases(contractVer uint32, versionedGases map[uint32]*dexeth.Gases) *dexeth.Gases { - if contractVer != contractVersionNewest { + if contractVer != dexeth.ContractVersionNewest { return versionedGases[contractVer] } var bestVer uint32 diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index 36e7752fe6..c3af569bfb 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -47,11 +47,21 @@ var ( testAddressB = common.HexToAddress("8d83B207674bfd53B418a6E47DA148F5bFeCc652") testAddressC = common.HexToAddress("2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27") - ethGases = dexeth.VersionedGases[0] - tokenGases = dexeth.Tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas + ethGasesV0 = dexeth.VersionedGases[0] + tokenGasesV0 = dexeth.Tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas + ethGasesV1 = dexeth.VersionedGases[1] + tokenGasesV1 = dexeth.Tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts[1].Gas - tETH = &dex.Asset{ - // Version meaning? + tETHV0 = &dex.Asset{ + Version: 0, + ID: 60, + Symbol: "ETH", + MaxFeeRate: 100, + SwapConf: 1, + } + + tETHV1 = &dex.Asset{ + Version: 1, ID: 60, Symbol: "ETH", MaxFeeRate: 100, @@ -66,7 +76,7 @@ var ( SwapConf: 1, } - tToken = &dex.Asset{ + tTokenV0 = &dex.Asset{ ID: usdcTokenID, Symbol: "usdc.eth", Version: 0, @@ -74,6 +84,14 @@ var ( SwapConf: 1, } + tTokenV1 = &dex.Asset{ + ID: usdcTokenID, + Symbol: "dextt.eth", + Version: 1, + MaxFeeRate: 20, + SwapConf: 1, + } + signer = types.LatestSigner(params.AllEthashProtocolChanges) // simBackend = backends.NewSimulatedBackend(core.GenesisAlloc{ @@ -330,6 +348,9 @@ type tContractor struct { redeemGasErr error refundGasErr error redeemGasOverride *uint64 + redeemable bool + redeemableErr error + redeemableMap map[string]bool valueIn map[common.Hash]uint64 valueOut map[common.Hash]uint64 valueErr error @@ -345,6 +366,76 @@ type tContractor struct { } } +func (c *tContractor) status(ctx context.Context, locator []byte) (*dexeth.SwapStatus, error) { + if c.swapErr != nil { + return nil, c.swapErr + } + vector, err := c.vector(ctx, locator) + if err != nil { + return nil, err + } + swap, ok := c.swapMap[vector.SecretHash] + if !ok { + return nil, errors.New("swap not in map") + } + s := &dexeth.SwapStatus{ + Step: swap.State, + Secret: swap.Secret, + BlockHeight: swap.BlockHeight, + } + return s, nil +} + +func (c *tContractor) vector(ctx context.Context, locator []byte) (*dexeth.SwapVector, error) { + if c.swapErr != nil { + return nil, c.swapErr + } + if len(locator) == dexeth.LocatorV1Length { + return dexeth.ParseV1Locator(locator) + } + var secretHash [32]byte + copy(secretHash[:], locator) + swap, ok := c.swapMap[secretHash] + if !ok { + return nil, errors.New("swap not in map") + } + v := &dexeth.SwapVector{ + From: swap.Initiator, + To: swap.Participant, + Value: swap.Value, + SecretHash: secretHash, + LockTime: uint64(swap.LockTime.Unix()), + } + return v, nil +} + +func (c *tContractor) statusAndVector(ctx context.Context, locator []byte) (*dexeth.SwapStatus, *dexeth.SwapVector, error) { + if c.swapErr != nil { + return nil, nil, c.swapErr + } + vector, err := c.vector(ctx, locator) + if err != nil { + return nil, nil, err + } + swap, ok := c.swapMap[vector.SecretHash] + if !ok { + return nil, nil, errors.New("swap not in map") + } + v := &dexeth.SwapVector{ + From: swap.Initiator, + To: swap.Participant, + Value: swap.Value, + SecretHash: vector.SecretHash, + LockTime: uint64(swap.LockTime.Unix()), + } + s := &dexeth.SwapStatus{ + Step: swap.State, + Secret: swap.Secret, + BlockHeight: swap.BlockHeight, + } + return s, v, nil +} + func (c *tContractor) swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) { if c.swapErr != nil { return nil, c.swapErr @@ -366,8 +457,12 @@ func (c *tContractor) redeem(txOpts *bind.TransactOpts, redeems []*asset.Redempt return c.redeemTx, c.redeemErr } -func (c *tContractor) refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { - c.lastRefund.secretHash = secretHash +func (c *tContractor) refund(opts *bind.TransactOpts, locator []byte) (*types.Transaction, error) { + vector, err := c.vector(context.Background(), locator) + if err != nil { + return nil, err + } + c.lastRefund.secretHash = vector.SecretHash c.lastRefund.maxFeeRate = opts.GasFeeCap return c.refundTx, c.refundErr } @@ -376,22 +471,50 @@ func (c *tContractor) estimateInitGas(ctx context.Context, n int) (uint64, error return c.gasEstimates.SwapN(n), c.initGasErr } -func (c *tContractor) estimateRedeemGas(ctx context.Context, secrets [][32]byte) (uint64, error) { +func (c *tContractor) estimateRedeemGas(ctx context.Context, secrets [][32]byte, locators [][]byte) (uint64, error) { if c.redeemGasOverride != nil { return *c.redeemGasOverride, nil } return c.gasEstimates.RedeemN(len(secrets)), c.redeemGasErr } -func (c *tContractor) estimateRefundGas(ctx context.Context, secretHash [32]byte) (uint64, error) { +func (c *tContractor) estimateRefundGas(ctx context.Context, locator []byte) (uint64, error) { return c.gasEstimates.Refund, c.refundGasErr } +func (c *tContractor) isRedeemable(locator []byte, secret [32]byte) (bool, error) { + if c.redeemableErr != nil { + return false, c.redeemableErr + } + + vector, err := c.vector(context.Background(), locator) + if err != nil { + return false, err + } + + if c.swapMap != nil && c.swapMap[vector.SecretHash] == nil { + return false, fmt.Errorf("test error: no swap in swap map") + } + + if c.redeemableMap != nil { + return c.redeemableMap[string(locator)], nil + } + + return c.redeemable, c.redeemableErr +} + func (c *tContractor) value(_ context.Context, tx *types.Transaction) (incoming, outgoing uint64, err error) { - return c.valueIn[tx.Hash()], c.valueOut[tx.Hash()], c.valueErr + incoming, outgoing = c.valueIn[tx.Hash()], c.valueOut[tx.Hash()] + if incoming > 0 { + delete(c.valueIn, tx.Hash()) + } + if outgoing > 0 { + delete(c.valueOut, tx.Hash()) + } + return incoming, outgoing, c.valueErr } -func (c *tContractor) isRefundable(secretHash [32]byte) (bool, error) { +func (c *tContractor) isRefundable(locator []byte) (bool, error) { return c.refundable, c.refundableErr } @@ -1090,7 +1213,7 @@ func newTestNode(assetID uint32) *tMempoolNode { } tc := &tContractor{ - gasEstimates: ethGases, + gasEstimates: ethGasesV0, swapMap: make(map[[32]byte]*dexeth.SwapState), valueIn: make(map[common.Hash]uint64), valueOut: make(map[common.Hash]uint64), @@ -1103,7 +1226,7 @@ func newTestNode(assetID uint32) *tMempoolNode { allow: new(big.Int), } if assetID != BipID { - ttc.tContractor.gasEstimates = &tokenGases + ttc.tContractor.gasEstimates = &tokenGasesV0 c = ttc } @@ -1173,16 +1296,16 @@ func tassetWallet(assetID uint32) (asset.Wallet, *assetWallet, *tMempoolNode, co confirmedNonceAt: new(big.Int), pendingTxs: make([]*extendedWalletTx, 0), txDB: &tTxDB{}, - currentTip: &types.Header{Number: new(big.Int)}, + currentTip: &types.Header{Number: new(big.Int), GasLimit: 30_000_000}, finalizeConfs: txConfsNeededToConfirm, + maxTxFeeGwei: dexeth.GweiFactor, // 1 ETH }, versionedGases: versionedGases, - maxSwapGas: versionedGases[0].Swap, - maxRedeemGas: versionedGases[0].Redeem, log: tLogger.SubLogger(strings.ToUpper(dex.BipIDSymbol(assetID))), assetID: assetID, - contractors: map[uint32]contractor{0: c}, - findRedemptionReqs: make(map[[32]byte]*findRedemptionRequest), + contractorV0: c, + contractorV1: c, + findRedemptionReqs: make(map[string]*findRedemptionRequest), evmify: dexeth.GweiToWei, atomize: dexeth.WeiToGwei, pendingTxCheckBal: new(big.Int), @@ -1206,7 +1329,8 @@ func tassetWallet(assetID uint32) (asset.Wallet, *assetWallet, *tMempoolNode, co node.tokenParent = &assetWallet{ baseWallet: aw.baseWallet, log: tLogger.SubLogger("ETH"), - contractors: map[uint32]contractor{0: node.tContractor}, + contractorV0: node.tContractor, + contractorV1: node.tContractor, assetID: BipID, atomize: dexeth.WeiToGwei, pendingApprovals: make(map[uint32]*pendingApproval), @@ -1362,13 +1486,13 @@ func TestBalanceWithMempool(t *testing.T) { t.Fatalf("unexpected error for test %q: %v", test.name, err) } if bal.Available != test.wantBal { - t.Fatalf("want available balance %v got %v for test %q", test.wantBal, bal.Available, test.name) + t.Fatalf("%s: want available balance %v got %v for test %q", test.name, test.wantBal, bal.Available, test.name) } if bal.Immature != test.wantImmature { - t.Fatalf("want immature balance %v got %v for test %q", test.wantImmature, bal.Immature, test.name) + t.Fatalf("%s: want immature balance %v got %v for test %q", test.name, test.wantImmature, bal.Immature, test.name) } if bal.Locked != test.wantLocked { - t.Fatalf("want locked balance %v got %v for test %q", test.wantLocked, bal.Locked, test.name) + t.Fatalf("%s: want locked balance %v got %v for test %q", test.name, test.wantLocked, bal.Locked, test.name) } } } @@ -1550,31 +1674,45 @@ func testRefund(t *testing.T, assetID uint32) { const gweiBal = 1e9 const ogRefundReserves = 1e8 - v1Contractor := &tContractor{ - swapMap: make(map[[32]byte]*dexeth.SwapState, 1), - gasEstimates: ethGases, - redeemTx: types.NewTx(&types.DynamicFeeTx{}), - } - var v1c contractor = v1Contractor - - gasesV1 := &dexeth.Gases{Refund: 1e5} - if assetID == BipID { - eth.versionedGases[1] = gasesV1 - } else { - eth.versionedGases[1] = &dexeth.Tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas - v1c = &tTokenContractor{tContractor: v1Contractor} - } - - eth.contractors[1] = v1c + // v1Contractor := &tContractor{ + // swapMap: make(map[[32]byte]*dexeth.SwapState, 1), + // gasEstimates: ethGasesV0, + // redeemTx: types.NewTx(&types.DynamicFeeTx{}), + // } + // var v1c contractor = v1Contractor + + // gasesV1 := &dexeth.Gases{Refund: 1e5} + // if assetID == BipID { + // dexeth.VersionedGases[1] = gasesV1 + // defer delete(dexeth.VersionedGases, 1) + // } else { + // tokenContracts := dexeth.Tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts + // tc := *tokenContracts[0] + // tc.Gas = *gasesV1 + // tokenContracts[1] = &tc + // defer delete(tokenContracts, 1) + // v1c = &tTokenContractor{tContractor: v1Contractor} + // } + + // eth.contractors[1] = v1c var secretHash [32]byte copy(secretHash[:], encode.RandomBytes(32)) - v0Contract := dexeth.EncodeContractData(0, secretHash) - ss := &dexeth.SwapState{Value: dexeth.GweiToWei(1)} - v0Contractor := node.tContractor + v0Contract := dexeth.EncodeContractData(0, secretHash[:]) + v1Vector := dexeth.SwapVector{ + From: testAddressA, + To: testAddressB, + Value: dexeth.GweiToWei(1), + SecretHash: secretHash, + LockTime: uint64(time.Now().Unix()), + } + v1Contract := dexeth.EncodeContractData(1, v1Vector.Locator()) - v0Contractor.swapMap[secretHash] = ss - v1Contractor.swapMap[secretHash] = ss + ss := &dexeth.SwapState{ + Value: dexeth.GweiToWei(1), + } + + node.tContractor.swapMap[secretHash] = ss tests := []struct { name string @@ -1587,7 +1725,7 @@ func testRefund(t *testing.T, assetID uint32) { wantZeroHash bool swapStep dexeth.SwapStep swapErr error - useV1Gases bool + v1 bool }{ { name: "ok", @@ -1600,7 +1738,7 @@ func testRefund(t *testing.T, assetID uint32) { swapStep: dexeth.SSInitiated, isRefundable: true, wantLocked: ogRefundReserves - feeSuggestion*dexeth.RefundGas(1), - useV1Gases: true, + v1: true, }, { name: "ok refunded", @@ -1641,11 +1779,10 @@ func testRefund(t *testing.T, assetID uint32) { } for _, test := range tests { + c := node.tContractor contract := v0Contract - c := v0Contractor - if test.useV1Gases { - contract = dexeth.EncodeContractData(1, secretHash) - c = v1Contractor + if test.v1 { + contract = v1Contract } else if test.badContract { contract = []byte{} } @@ -1727,11 +1864,11 @@ func testFundOrderReturnCoinsFundingCoins(t *testing.T, assetID uint32) { w, eth, node, shutdown := tassetWallet(assetID) defer shutdown() walletBalanceGwei := uint64(dexeth.GweiFactor) - fromAsset := tETH + fromAsset := tETHV0 if assetID == BipID { node.bal = dexeth.GweiToWei(walletBalanceGwei) } else { - fromAsset = tToken + fromAsset = tTokenV0 node.tokenContractor.bal = dexeth.GweiToWei(walletBalanceGwei) node.tokenContractor.allow = unlimitedAllowance node.tokenParent.node.(*tMempoolNode).bal = dexeth.GweiToWei(walletBalanceGwei) @@ -1793,7 +1930,7 @@ func testFundOrderReturnCoinsFundingCoins(t *testing.T, assetID uint32) { } order := asset.Order{ - Version: fromAsset.Version, + AssetVersion: fromAsset.Version, Value: walletBalanceGwei / 2, MaxSwapCount: 2, MaxFeeRate: fromAsset.MaxFeeRate, @@ -1905,7 +2042,8 @@ func testFundOrderReturnCoinsFundingCoins(t *testing.T, assetID uint32) { w2, eth2, _, shutdown2 := tassetWallet(assetID) defer shutdown2() eth2.node = node - eth2.contractors[0] = node.tokenContractor + eth2.contractorV0 = node.tokenContractor + eth2.contractorV1 = node.tokenContractor node.tokenContractor.bal = dexeth.GweiToWei(walletBalanceGwei) // Test reloading coins from first order @@ -2021,10 +2159,10 @@ func testFundMultiOrder(t *testing.T, assetID uint32) { defer shutdown() - fromAsset := tETH + fromAsset := tETHV0 swapGas := dexeth.VersionedGases[fromAsset.Version].Swap if assetID != BipID { - fromAsset = tToken + fromAsset = tTokenV0 node.tokenContractor.allow = unlimitedAllowance swapGas = dexeth.Tokens[usdcTokenID].NetTokens[dex.Simnet]. SwapContracts[fromAsset.Version].Gas.Swap @@ -2051,8 +2189,8 @@ func testFundMultiOrder(t *testing.T, assetID uint32) { tokenBal: uint64(dexeth.GweiFactor), parentBal: uint64(dexeth.GweiFactor), multiOrder: &asset.MultiOrder{ - Version: fromAsset.Version, - MaxFeeRate: fromAsset.MaxFeeRate, + AssetVersion: fromAsset.Version, + MaxFeeRate: fromAsset.MaxFeeRate, Values: []*asset.MultiOrderValue{ { Value: uint64(dexeth.GweiFactor) / 2, @@ -2071,8 +2209,8 @@ func testFundMultiOrder(t *testing.T, assetID uint32) { tokenBal: uint64(dexeth.GweiFactor), parentBal: uint64(dexeth.GweiFactor), multiOrder: &asset.MultiOrder{ - Version: fromAsset.Version, - MaxFeeRate: fromAsset.MaxFeeRate, + AssetVersion: fromAsset.Version, + MaxFeeRate: fromAsset.MaxFeeRate, Values: []*asset.MultiOrderValue{ { Value: uint64(dexeth.GweiFactor) / 2, @@ -2093,8 +2231,8 @@ func testFundMultiOrder(t *testing.T, assetID uint32) { tokenBal: uint64(dexeth.GweiFactor), parentBal: uint64(dexeth.GweiFactor), multiOrder: &asset.MultiOrder{ - Version: fromAsset.Version, - MaxFeeRate: fromAsset.MaxFeeRate, + AssetVersion: fromAsset.Version, + MaxFeeRate: fromAsset.MaxFeeRate, Values: []*asset.MultiOrderValue{ { Value: uint64(dexeth.GweiFactor) / 2, @@ -2116,8 +2254,8 @@ func testFundMultiOrder(t *testing.T, assetID uint32) { tokenBal: uint64(dexeth.GweiFactor), parentBal: uint64(dexeth.GweiFactor), multiOrder: &asset.MultiOrder{ - Version: fromAsset.Version, - MaxFeeRate: fromAsset.MaxFeeRate, + AssetVersion: fromAsset.Version, + MaxFeeRate: fromAsset.MaxFeeRate, Values: []*asset.MultiOrderValue{ { Value: uint64(dexeth.GweiFactor) / 2, @@ -2138,8 +2276,8 @@ func testFundMultiOrder(t *testing.T, assetID uint32) { tokenBal: uint64(dexeth.GweiFactor), parentBal: uint64(dexeth.GweiFactor), multiOrder: &asset.MultiOrder{ - Version: fromAsset.Version, - MaxFeeRate: fromAsset.MaxFeeRate, + AssetVersion: fromAsset.Version, + MaxFeeRate: fromAsset.MaxFeeRate, Values: []*asset.MultiOrderValue{ { Value: uint64(dexeth.GweiFactor) / 2, @@ -2160,8 +2298,8 @@ func testFundMultiOrder(t *testing.T, assetID uint32) { tokenBal: uint64(dexeth.GweiFactor) - 1, parentBal: uint64(dexeth.GweiFactor), multiOrder: &asset.MultiOrder{ - Version: fromAsset.Version, - MaxFeeRate: fromAsset.MaxFeeRate, + AssetVersion: fromAsset.Version, + MaxFeeRate: fromAsset.MaxFeeRate, Values: []*asset.MultiOrderValue{ { Value: uint64(dexeth.GweiFactor) / 2, @@ -2181,8 +2319,8 @@ func testFundMultiOrder(t *testing.T, assetID uint32) { tokenBal: uint64(dexeth.GweiFactor), parentBal: swapGas * 4 * fromAsset.MaxFeeRate, multiOrder: &asset.MultiOrder{ - Version: fromAsset.Version, - MaxFeeRate: fromAsset.MaxFeeRate, + AssetVersion: fromAsset.Version, + MaxFeeRate: fromAsset.MaxFeeRate, Values: []*asset.MultiOrderValue{ { Value: uint64(dexeth.GweiFactor) / 2, @@ -2201,8 +2339,8 @@ func testFundMultiOrder(t *testing.T, assetID uint32) { tokenBal: uint64(dexeth.GweiFactor), parentBal: swapGas*4*fromAsset.MaxFeeRate - 1, multiOrder: &asset.MultiOrder{ - Version: fromAsset.Version, - MaxFeeRate: fromAsset.MaxFeeRate, + AssetVersion: fromAsset.Version, + MaxFeeRate: fromAsset.MaxFeeRate, Values: []*asset.MultiOrderValue{ { Value: uint64(dexeth.GweiFactor) / 2, @@ -2268,13 +2406,12 @@ func testFundMultiOrder(t *testing.T, assetID uint32) { func TestPreSwap(t *testing.T) { const baseFee, tip = 42, 2 - const feeSuggestion = 90 // ignored by eth's PreSwap + const currentFees = 44 const lotSize = 10e9 - oneFee := ethGases.Swap * tETH.MaxFeeRate - refund := ethGases.Refund * tETH.MaxFeeRate + oneFee := ethGasesV0.Swap * tETHV0.MaxFeeRate + refund := ethGasesV0.Refund * tETHV0.MaxFeeRate oneLock := lotSize + oneFee + refund - - oneFeeToken := tokenGases.Swap*tToken.MaxFeeRate + tokenGases.Refund*tToken.MaxFeeRate + oneFeeToken := tokenGasesV0.Swap*tTokenV0.MaxFeeRate + tokenGasesV0.Refund*tTokenV0.MaxFeeRate type testData struct { name string @@ -2323,9 +2460,9 @@ func TestPreSwap(t *testing.T) { wantLots: 1, wantValue: lotSize, - wantMaxFees: tETH.MaxFeeRate * ethGases.Swap, - wantBestCase: (baseFee + tip) * ethGases.Swap, - wantWorstCase: (baseFee + tip) * ethGases.Swap, + wantMaxFees: tETHV0.MaxFeeRate * ethGasesV0.Swap, + wantBestCase: currentFees * ethGasesV0.Swap, + wantWorstCase: currentFees * ethGasesV0.Swap, }, { name: "one lot enough for fees - token", @@ -2336,9 +2473,9 @@ func TestPreSwap(t *testing.T) { wantLots: 1, wantValue: lotSize, - wantMaxFees: tToken.MaxFeeRate * tokenGases.Swap, - wantBestCase: (baseFee + tip) * tokenGases.Swap, - wantWorstCase: (baseFee + tip) * tokenGases.Swap, + wantMaxFees: tTokenV0.MaxFeeRate * tokenGasesV0.Swap, + wantBestCase: currentFees * tokenGasesV0.Swap, + wantWorstCase: currentFees * tokenGasesV0.Swap, }, { name: "more lots than max lots", @@ -2363,9 +2500,9 @@ func TestPreSwap(t *testing.T) { wantLots: 4, wantValue: 4 * lotSize, - wantMaxFees: 4 * tETH.MaxFeeRate * ethGases.Swap, - wantBestCase: (baseFee + tip) * ethGases.Swap, - wantWorstCase: 4 * (baseFee + tip) * ethGases.Swap, + wantMaxFees: 4 * tETHV0.MaxFeeRate * ethGasesV0.Swap, + wantBestCase: currentFees * ethGasesV0.Swap, + wantWorstCase: 4 * currentFees * ethGasesV0.Swap, }, { name: "fewer than max lots - token", @@ -2376,9 +2513,9 @@ func TestPreSwap(t *testing.T) { wantLots: 4, wantValue: 4 * lotSize, - wantMaxFees: 4 * tToken.MaxFeeRate * tokenGases.Swap, - wantBestCase: (baseFee + tip) * tokenGases.Swap, - wantWorstCase: 4 * (baseFee + tip) * tokenGases.Swap, + wantMaxFees: 4 * tTokenV0.MaxFeeRate * tokenGasesV0.Swap, + wantBestCase: currentFees * tokenGasesV0.Swap, + wantWorstCase: 4 * currentFees * tokenGasesV0.Swap, }, { name: "balanceError", @@ -2400,11 +2537,12 @@ func TestPreSwap(t *testing.T) { } runTest := func(t *testing.T, test testData) { + var assetID uint32 = BipID - assetCfg := tETH + assetCfg := tETHV0 if test.token { assetID = usdcTokenID - assetCfg = tToken + assetCfg = tTokenV0 } w, _, node, shutdown := tassetWallet(assetID) @@ -2412,7 +2550,7 @@ func TestPreSwap(t *testing.T) { node.baseFee, node.tip = dexeth.GweiToWei(baseFee), dexeth.GweiToWei(tip) if test.token { - node.tContractor.gasEstimates = &tokenGases + node.tContractor.gasEstimates = &tokenGasesV0 node.tokenContractor.bal = dexeth.GweiToWei(test.bal) node.bal = dexeth.GweiToWei(test.parentBal) } else { @@ -2422,11 +2560,11 @@ func TestPreSwap(t *testing.T) { node.balErr = test.balErr preSwap, err := w.PreSwap(&asset.PreSwapForm{ - Version: assetCfg.Version, + AssetVersion: assetCfg.Version, LotSize: lotSize, Lots: test.lots, MaxFeeRate: assetCfg.MaxFeeRate, - FeeSuggestion: feeSuggestion, // ignored + FeeSuggestion: currentFees, // ignored RedeemVersion: tBTC.Version, RedeemAssetID: tBTC.ID, }) @@ -2438,7 +2576,7 @@ func TestPreSwap(t *testing.T) { return } if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatalf("%q: %v", test.name, err) } est := preSwap.Estimate @@ -2476,6 +2614,13 @@ func testSwap(t *testing.T, assetID uint32) { w, eth, node, shutdown := tassetWallet(assetID) defer shutdown() + assetCfg := tETHV0 + gases := ethGasesV0 + if assetID != BipID { + assetCfg = tTokenV0 + gases = &tokenGasesV0 + } + receivingAddress := "0x2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27" node.tContractor.initTx = types.NewTx(&types.DynamicFeeTx{}) @@ -2485,7 +2630,8 @@ func testSwap(t *testing.T, assetID uint32) { if assetID == BipID { coinIDs = append(coinIDs, createFundingCoin(eth.addr, amt).RecoveryID()) } else { - fees := n * tokenGases.Swap * tToken.MaxFeeRate + // Not gonna version the fees here unless it matters. + fees := n * gases.Swap * assetCfg.MaxFeeRate coinIDs = append(coinIDs, createTokenFundingCoin(eth.addr, amt, fees).RecoveryID()) } } @@ -2511,12 +2657,7 @@ func testSwap(t *testing.T, assetID uint32) { } gasNeededForSwaps := func(numSwaps int) uint64 { - if assetID == BipID { - return ethGases.Swap * uint64(numSwaps) - } else { - return tokenGases.Swap * uint64(numSwaps) - } - + return gases.Swap * uint64(numSwaps) } testSwap := func(testName string, swaps asset.Swaps, expectError bool) { @@ -2554,16 +2695,17 @@ func testSwap(t *testing.T, assetID uint32) { testName, receipt.Coin().Value(), contract.Value) } contractData := receipt.Contract() - ver, secretHash, err := dexeth.DecodeContractData(contractData) + contractVer, locator, err := dexeth.DecodeContractData(contractData) if err != nil { t.Fatalf("failed to decode contract data: %v", err) } - if swaps.Version != ver { + if contractVersion(swaps.AssetVersion) != contractVer { t.Fatal("wrong contract version") } - if !bytes.Equal(contract.SecretHash, secretHash[:]) { - t.Fatalf("%v, contract: %x != secret hash in input: %x", - testName, receipt.Contract(), secretHash) + chkLocator := acToLocator(contractVer, contract, dexeth.GweiToWei(contract.Value), node.addr) + if !bytes.Equal(locator, chkLocator) { + t.Fatalf("%v, contract: %x != locator in input: %x", + testName, receipt.Contract(), locator) } totalCoinValue += receipt.Coin().Value() @@ -2633,16 +2775,12 @@ func testSwap(t *testing.T, assetID uint32) { }, } inputs := refreshWalletAndFundCoins(5, []uint64{ethToGwei(2)}, 1) - assetCfg := tETH - if assetID != BipID { - assetCfg = tToken - } swaps := asset.Swaps{ - Version: assetCfg.Version, - Inputs: inputs, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + AssetVersion: assetCfg.Version, + Inputs: inputs, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("error initialize but no send", swaps, true) node.tContractor.initErr = nil @@ -2659,22 +2797,22 @@ func testSwap(t *testing.T, assetID uint32) { inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2)}, 1) swaps = asset.Swaps{ - Version: assetCfg.Version, - Inputs: inputs, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + AssetVersion: assetCfg.Version, + Inputs: inputs, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("one contract, don't lock change", swaps, false) // Test one contract with locking change inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2)}, 1) swaps = asset.Swaps{ - Version: assetCfg.Version, - Inputs: inputs, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: true, + AssetVersion: assetCfg.Version, + Inputs: inputs, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: true, } testSwap("one contract, lock change", swaps, false) @@ -2695,43 +2833,62 @@ func testSwap(t *testing.T, assetID uint32) { } inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(3)}, 2) swaps = asset.Swaps{ - Version: assetCfg.Version, - Inputs: inputs, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + AssetVersion: assetCfg.Version, + Inputs: inputs, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("two contracts", swaps, false) // Test error when funding coins are not enough to cover swaps inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(1)}, 2) swaps = asset.Swaps{ - Version: assetCfg.Version, - Inputs: inputs, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + AssetVersion: assetCfg.Version, + Inputs: inputs, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("funding coins not enough balance", swaps, true) // Ensure when funds are exactly the same as required works properly inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2) + (2 * 200 * dexeth.InitGas(1, 0))}, 2) swaps = asset.Swaps{ - Inputs: inputs, - Version: assetCfg.Version, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + Inputs: inputs, + AssetVersion: assetCfg.Version, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("exact change", swaps, false) + + // Version 1 + assetCfg = tETHV1 + gases = ethGasesV1 + if assetID != BipID { + assetCfg = tTokenV1 + gases = &tokenGasesV1 + } + node.tContractor.gasEstimates = gases + + inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2) + (2 * 200 * dexeth.InitGas(1, 1))}, 2) + swaps = asset.Swaps{ + Inputs: inputs, + AssetVersion: assetCfg.Version, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, + } + testSwap("v1", swaps, false) } func TestPreRedeem(t *testing.T) { - w, _, _, shutdown := tassetWallet(BipID) + w, _, node, shutdown := tassetWallet(BipID) defer shutdown() form := &asset.PreRedeemForm{ - Version: tETH.Version, + AssetVersion: tETHV0.Version, Lots: 5, FeeSuggestion: 100, } @@ -2749,7 +2906,8 @@ func TestPreRedeem(t *testing.T) { w, _, _, shutdown2 := tassetWallet(usdcTokenID) defer shutdown2() - form.Version = tToken.Version + form.AssetVersion = tTokenV0.Version + node.tokenContractor.allow = unlimitedAllowanceReplenishThreshold preRedeem, err = w.PreRedeem(form) if err != nil { @@ -2770,41 +2928,49 @@ func testRedeem(t *testing.T, assetID uint32) { w, eth, node, shutdown := tassetWallet(assetID) defer shutdown() - // Test with a non-zero contract version to ensure it makes it into the receipt - contractVer := uint32(1) - - eth.versionedGases[1] = ethGases - if assetID != BipID { - eth.versionedGases[1] = &tokenGases - } - - tokenContracts := eth.tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts - tokenContracts[1] = tokenContracts[0] - defer delete(tokenContracts, 1) - - contractorV1 := &tContractor{ - swapMap: make(map[[32]byte]*dexeth.SwapState, 1), - gasEstimates: ethGases, - redeemTx: types.NewTx(&types.DynamicFeeTx{Data: []byte{1, 2, 3}}), - } - var c contractor = contractorV1 - if assetID != BipID { - c = &tTokenContractor{ - tContractor: contractorV1, - } + // // Test with a non-zero contract version to ensure it makes it into the receipt + // contractVer := uint32(1) + + // eth.versionedGases[1] = ethGases + // if assetID != BipID { + // eth.versionedGases[1] = &tokenGases + // } + + // tokenContracts := eth.tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts + // tokenContracts[1] = tokenContracts[0] + // defer delete(tokenContracts, 1) + + // contractorV1 := &tContractor{ + // swapMap: make(map[[32]byte]*dexeth.SwapState, 1), + // gasEstimates: ethGases, + // redeemTx: types.NewTx(&types.DynamicFeeTx{Data: []byte{1, 2, 3}}), + // } + // var c contractor = contractorV1 + // if assetID != BipID { + // c = &tTokenContractor{ + // tContractor: contractorV1, + // } + // } + var contractor *tContractor + if assetID == BipID { + contractor = eth.contractorV0.(*tContractor) + } else { + contractor = eth.contractorV1.(*tTokenContractor).tContractor } - eth.contractors[1] = c + contractor.redeemTx = types.NewTx(&types.DynamicFeeTx{Data: []byte{1, 2, 3}}) + now := time.Now() + const value = 1e9 - addSwapToSwapMap := func(secretHash [32]byte, value uint64, step dexeth.SwapStep) { + addSwapToSwapMap := func(secretHash [32]byte, step dexeth.SwapStep) { swap := dexeth.SwapState{ BlockHeight: 1, - LockTime: time.Now(), + LockTime: now, Initiator: testAddressB, Participant: testAddressA, Value: dexeth.GweiToWei(value), State: step, } - contractorV1.swapMap[secretHash] = &swap + contractor.swapMap[secretHash] = &swap } numSecrets := 3 @@ -2818,20 +2984,22 @@ func testRedeem(t *testing.T, assetID uint32) { secretHashes = append(secretHashes, secretHash) } - addSwapToSwapMap(secretHashes[0], 1e9, dexeth.SSInitiated) // states will be reset by tests though - addSwapToSwapMap(secretHashes[1], 1e9, dexeth.SSInitiated) + addSwapToSwapMap(secretHashes[0], dexeth.SSInitiated) + addSwapToSwapMap(secretHashes[1], dexeth.SSInitiated) /* COMMENTED while estimateRedeemGas is on the $#!t list - var redeemGas uint64 + var redeemGasesV0, redeemGasesV1 *dexeth.Gases if assetID == BipID { - redeemGas = ethGases.Redeem + redeemGasesV0 = ethGasesV0 + redeemGasesV1 = ethGasesV1 } else { - redeemGas = tokenGases.Redeem + redeemGasesV0 = &tokenGasesV0 + redeemGasesV1 = &tokenGasesV1 } - - var higherGasEstimate uint64 = redeemGas * 2 * 12 / 10 // 120% of estimate - var doubleGasEstimate uint64 = (redeemGas * 2 * 2) * 10 / 11 // 200% of estimate after 10% increase - // var moreThanDoubleGasEstimate uint64 = (redeemGas * 2 * 21 / 10) * 10 / 11 // > 200% of estimate after 10% increase + redeemGas := redeemGasesV0.Redeem + var higherGasEstimate uint64 = redeemGas * 2 * 12 / 10 // 120% of estimate + var doubleGasEstimate uint64 = (redeemGas * 2 * 2) * 10 / 11 // 200% of estimate after 10% increase + var moreThanDoubleGasEstimate uint64 = (redeemGas * 2 * 21 / 10) * 10 / 11 // > 200% of estimate after 10% increase // additionalFundsNeeded calculates the amount of available funds that we be // needed to use a higher gas estimate than the original, and double the base // fee if it is higher than the server's max fee rate. @@ -2866,6 +3034,40 @@ func testRedeem(t *testing.T, assetID uint32) { secretHashes[0]: dexeth.SSInitiated, secretHashes[1]: dexeth.SSInitiated, } + newRedeem := func(idx int) *asset.Redemption { + return &asset.Redemption{ + Spends: &asset.AuditInfo{ + Contract: dexeth.EncodeContractData(0, secretHashes[idx][:]), + SecretHash: secretHashes[idx][:], // redundant for all current assets, unused with eth + Coin: &coin{ + id: randomHash(), + value: value, + }, + }, + Secret: secrets[idx][:], + } + } + + // newRedeemV1 := func(idx int) *asset.Redemption { + // locator := (&dexeth.SwapVector{ + // From: testAddressA, + // To: testAddressB, + // Value: value, + // SecretHash: secretHashes[idx], + // LockTime: uint64(now.Unix()), + // }).Locator() + // return &asset.Redemption{ + // Spends: &asset.AuditInfo{ + // Contract: dexeth.EncodeContractData(1, locator), + // SecretHash: secretHashes[idx][:], // redundant for all current assets, unused with eth + // Coin: &coin{ + // id: randomHash(), + // value: value, + // }, + // }, + // Secret: secrets[idx][:], + // } + // } tests := []struct { name string @@ -2878,6 +3080,7 @@ func testRedeem(t *testing.T, assetID uint32) { redeemGasOverride *uint64 expectedGasFeeCap *big.Int expectError bool + v1 bool }{ { name: "ok", @@ -2887,32 +3090,24 @@ func testRedeem(t *testing.T, assetID uint32) { baseFee: dexeth.GweiToWei(100), expectedGasFeeCap: dexeth.GweiToWei(100), form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, /* COMMENTED while estimateRedeemGas is on the $#!t list + { + name: "ok-v1", + expectError: false, + isRedeemable: true, + ethBal: dexeth.GweiToWei(10e9), + baseFee: dexeth.GweiToWei(100), + expectedGasFeeCap: dexeth.GweiToWei(100), + form: asset.RedeemForm{ + Redemptions: []*asset.Redemption{newRedeemV1(0), newRedeemV1(1)}, + FeeSuggestion: 100, + }, + v1: true, + }, { name: "higher gas estimate than reserved", expectError: false, @@ -2922,28 +3117,7 @@ func testRedeem(t *testing.T, assetID uint32) { expectedGasFeeCap: dexeth.GweiToWei(100), redeemGasOverride: &higherGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -2956,28 +3130,7 @@ func testRedeem(t *testing.T, assetID uint32) { expectedGasFeeCap: dexeth.GweiToWei(100), redeemGasOverride: &doubleGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -2989,28 +3142,7 @@ func testRedeem(t *testing.T, assetID uint32) { baseFee: dexeth.GweiToWei(100), redeemGasOverride: &moreThanDoubleGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -3022,28 +3154,7 @@ func testRedeem(t *testing.T, assetID uint32) { baseFee: dexeth.GweiToWei(100), redeemGasOverride: &higherGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -3056,28 +3167,7 @@ func testRedeem(t *testing.T, assetID uint32) { expectedGasFeeCap: dexeth.GweiToWei(300), redeemGasOverride: &higherGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -3090,28 +3180,7 @@ func testRedeem(t *testing.T, assetID uint32) { expectedGasFeeCap: dexeth.GweiToWei(298), redeemGasOverride: &higherGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -3127,28 +3196,7 @@ func testRedeem(t *testing.T, assetID uint32) { baseFee: dexeth.GweiToWei(100), form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -3159,28 +3207,7 @@ func testRedeem(t *testing.T, assetID uint32) { baseFee: dexeth.GweiToWei(100), swapErr: errors.New("swap() error"), form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -3192,18 +3219,7 @@ func testRedeem(t *testing.T, assetID uint32) { ethBal: dexeth.GweiToWei(10e9), baseFee: dexeth.GweiToWei(100), form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0)}, FeeSuggestion: 200, }, }, @@ -3214,18 +3230,7 @@ func testRedeem(t *testing.T, assetID uint32) { ethBal: dexeth.GweiToWei(10e9), baseFee: dexeth.GweiToWei(100), form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[2]), - SecretHash: secretHashes[2][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[2][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(2)}, FeeSuggestion: 100, }, }, @@ -3243,15 +3248,20 @@ func testRedeem(t *testing.T, assetID uint32) { } for _, test := range tests { - contractorV1.redeemErr = test.redeemErr - contractorV1.swapErr = test.swapErr - contractorV1.redeemGasOverride = test.redeemGasOverride + contractor.redeemErr = test.redeemErr + contractor.swapErr = test.swapErr + contractor.redeemGasOverride = test.redeemGasOverride for secretHash, step := range test.swapMap { - contractorV1.swapMap[secretHash].State = step + contractor.swapMap[secretHash].State = step } node.bal = test.ethBal node.baseFee = test.baseFee + var contractVer uint32 + if test.v1 { + contractVer = 1 + } + txs, out, fees, err := w.Redeem(&test.form) if test.expectError { if err == nil { @@ -3269,10 +3279,8 @@ func testRedeem(t *testing.T, assetID uint32) { } // Check fees returned from Redeem are as expected - expectedGas := dexeth.RedeemGas(len(test.form.Redemptions), 0) - if assetID != BipID { - expectedGas = tokenGases.Redeem + (uint64(len(test.form.Redemptions))-1)*tokenGases.RedeemAdd - } + rg := gases(contractVer, eth.versionedGases) + expectedGas := rg.Redeem + (uint64(len(test.form.Redemptions))-1)*rg.RedeemAdd expectedFees := expectedGas * test.form.FeeSuggestion if fees != expectedFees { t.Fatalf("%v: expected fees %d, but got %d", test.name, expectedFees, fees) @@ -3281,44 +3289,52 @@ func testRedeem(t *testing.T, assetID uint32) { // Check that value of output coin is as axpected var totalSwapValue uint64 for _, redemption := range test.form.Redemptions { - _, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract) + _, locator, err := dexeth.DecodeContractData(redemption.Spends.Contract) if err != nil { - t.Fatalf("DecodeContractData: %v", err) + t.Fatalf("DecodeLocator: %v", err) + } + var secretHash [32]byte + if test.v1 { + v, _ := dexeth.ParseV1Locator(locator) + secretHash = v.SecretHash + } else { + copy(secretHash[:], locator) } // secretHash should equal redemption.Spends.SecretHash, but it's // not part of the Redeem code, just the test input consistency. - swap := contractorV1.swapMap[secretHash] + swap := contractor.swapMap[secretHash] totalSwapValue += dexeth.WeiToGwei(swap.Value) } if out.Value() != totalSwapValue { - t.Fatalf("expected coin value to be %d but got %d", - totalSwapValue, out.Value()) + t.Fatalf("%s: expected coin value to be %d but got %d", + test.name, totalSwapValue, out.Value()) } // Check that gas limit in the transaction is as expected var expectedGasLimit uint64 - // if test.redeemGasOverride == nil { - if assetID == BipID { - expectedGasLimit = ethGases.Redeem * uint64(len(test.form.Redemptions)) + if test.redeemGasOverride == nil { + if assetID == BipID { + expectedGasLimit = rg.Redeem * uint64(len(test.form.Redemptions)) + } else { + expectedGasLimit = rg.Redeem * uint64(len(test.form.Redemptions)) + } } else { - expectedGasLimit = tokenGases.Redeem * uint64(len(test.form.Redemptions)) + expectedGasLimit = rg.Redeem * uint64(len(test.form.Redemptions)) } - // } else { - // expectedGasLimit = *test.redeemGasOverride * 11 / 10 - // } - if contractorV1.lastRedeemOpts.GasLimit != expectedGasLimit { - t.Fatalf("%s: expected gas limit %d, but got %d", test.name, expectedGasLimit, contractorV1.lastRedeemOpts.GasLimit) + if contractor.lastRedeemOpts.GasLimit != expectedGasLimit { + t.Fatalf("%s: expected gas limit %d, but got %d", test.name, expectedGasLimit, contractor.lastRedeemOpts.GasLimit) } // Check that the gas fee cap in the transaction is as expected - if contractorV1.lastRedeemOpts.GasFeeCap.Cmp(test.expectedGasFeeCap) != 0 { - t.Fatalf("%s: expected gas fee cap %v, but got %v", test.name, test.expectedGasFeeCap, contractorV1.lastRedeemOpts.GasFeeCap) + if contractor.lastRedeemOpts.GasFeeCap.Cmp(test.expectedGasFeeCap) != 0 { + t.Fatalf("%s: expected gas fee cap %v, but got %v", test.name, test.expectedGasFeeCap, contractor.lastRedeemOpts.GasFeeCap) } } } func TestMaxOrder(t *testing.T) { const baseFee, tip = 42, 2 + const currentFee = baseFee + tip type testData struct { name string @@ -3326,7 +3342,6 @@ func TestMaxOrder(t *testing.T) { balErr error lotSize uint64 maxFeeRate uint64 - feeSuggestion uint64 token bool parentBal uint64 wantErr bool @@ -3336,113 +3351,124 @@ func TestMaxOrder(t *testing.T) { wantWorstCase uint64 wantBestCase uint64 wantLocked uint64 + v1 bool } tests := []testData{ { - name: "no balance", - bal: 0, - lotSize: 10, - feeSuggestion: 90, - maxFeeRate: 100, + name: "no balance", + bal: 0, + lotSize: 10, + maxFeeRate: 100, }, { - name: "no balance - token", - bal: 0, - lotSize: 10, - feeSuggestion: 90, - maxFeeRate: 100, - token: true, - parentBal: 100, + name: "no balance - token", + bal: 0, + lotSize: 10, + maxFeeRate: 100, + token: true, + parentBal: 100, }, { - name: "not enough for fees", - bal: 10, - lotSize: 10, - feeSuggestion: 90, - maxFeeRate: 100, + name: "not enough for fees", + bal: 10, + lotSize: 10, + maxFeeRate: 100, }, { - name: "not enough for fees - token", - bal: 10, - token: true, - parentBal: 0, + name: "not enough for fees - token", + bal: 10, + token: true, + parentBal: 0, + lotSize: 10, + maxFeeRate: 100, + }, + { + name: "one lot enough for fees", + bal: 11, lotSize: 10, - feeSuggestion: 90, maxFeeRate: 100, + wantLots: 1, + wantValue: ethToGwei(10), + wantMaxFees: 100 * ethGasesV0.Swap, + wantBestCase: currentFee * ethGasesV0.Swap, + wantWorstCase: currentFee * ethGasesV0.Swap, + wantLocked: ethToGwei(10) + (100 * ethGasesV0.Swap), }, { - name: "one lot enough for fees", + name: "one lot enough for fees - v1", bal: 11, lotSize: 10, - feeSuggestion: 90, maxFeeRate: 100, wantLots: 1, wantValue: ethToGwei(10), - wantMaxFees: 100 * ethGases.Swap, - wantBestCase: (baseFee + tip) * ethGases.Swap, - wantWorstCase: (baseFee + tip) * ethGases.Swap, - wantLocked: ethToGwei(10) + (100 * ethGases.Swap), + wantMaxFees: 100 * ethGasesV1.Swap, + wantBestCase: currentFee * ethGasesV1.Swap, + wantWorstCase: currentFee * ethGasesV1.Swap, + wantLocked: ethToGwei(10) + (100 * ethGasesV0.Swap), + v1: true, }, { name: "one lot enough for fees - token", bal: 11, lotSize: 10, - feeSuggestion: 90, maxFeeRate: 100, token: true, parentBal: 1, wantLots: 1, wantValue: ethToGwei(10), - wantMaxFees: 100 * tokenGases.Swap, - wantBestCase: (baseFee + tip) * tokenGases.Swap, - wantWorstCase: (baseFee + tip) * tokenGases.Swap, - wantLocked: ethToGwei(10) + (100 * tokenGases.Swap), + wantMaxFees: 100 * tokenGasesV0.Swap, + wantBestCase: currentFee * tokenGasesV0.Swap, + wantWorstCase: currentFee * tokenGasesV0.Swap, + wantLocked: ethToGwei(10) + (100 * tokenGasesV0.Swap), }, { name: "multiple lots", bal: 51, lotSize: 10, - feeSuggestion: 90, maxFeeRate: 100, wantLots: 5, wantValue: ethToGwei(50), - wantMaxFees: 5 * 100 * ethGases.Swap, - wantBestCase: (baseFee + tip) * ethGases.Swap, - wantWorstCase: 5 * (baseFee + tip) * ethGases.Swap, - wantLocked: ethToGwei(50) + (5 * 100 * ethGases.Swap), + wantMaxFees: 5 * 100 * ethGasesV0.Swap, + wantBestCase: currentFee * ethGasesV0.Swap, + wantWorstCase: 5 * currentFee * ethGasesV0.Swap, + wantLocked: ethToGwei(50) + (5 * 100 * ethGasesV0.Swap), }, { name: "multiple lots - token", bal: 51, lotSize: 10, - feeSuggestion: 90, maxFeeRate: 100, token: true, parentBal: 1, wantLots: 5, wantValue: ethToGwei(50), - wantMaxFees: 5 * 100 * tokenGases.Swap, - wantBestCase: (baseFee + tip) * tokenGases.Swap, - wantWorstCase: 5 * (baseFee + tip) * tokenGases.Swap, - wantLocked: ethToGwei(50) + (5 * 100 * tokenGases.Swap), + wantMaxFees: 5 * 100 * tokenGasesV0.Swap, + wantBestCase: currentFee * tokenGasesV0.Swap, + wantWorstCase: 5 * currentFee * tokenGasesV0.Swap, + wantLocked: ethToGwei(50) + (5 * 100 * tokenGasesV0.Swap), }, { - name: "balanceError", - bal: 51, - lotSize: 10, - feeSuggestion: 90, - maxFeeRate: 100, - balErr: errors.New("test error"), - wantErr: true, + name: "balanceError", + bal: 51, + lotSize: 10, + // feeSuggestion: 90, + maxFeeRate: 100, + balErr: errors.New("test error"), + wantErr: true, }, } runTest := func(t *testing.T, test testData) { var assetID uint32 = BipID - assetCfg := tETH + gases := ethGasesV0 if test.token { assetID = usdcTokenID - assetCfg = tToken + gases = &tokenGasesV0 + if test.v1 { + gases = &tokenGasesV1 + } + } else if test.v1 { + gases = ethGasesV1 } w, _, node, shutdown := tassetWallet(assetID) @@ -3450,7 +3476,8 @@ func TestMaxOrder(t *testing.T) { node.baseFee, node.tip = dexeth.GweiToWei(baseFee), dexeth.GweiToWei(tip) if test.token { - node.tContractor.gasEstimates = &tokenGases + node.tContractor.gasEstimates = &tokenGasesV0 + // dexAsset = tTokenV0 node.tokenContractor.bal = dexeth.GweiToWei(ethToGwei(test.bal)) node.bal = dexeth.GweiToWei(ethToGwei(test.parentBal)) } else { @@ -3458,11 +3485,16 @@ func TestMaxOrder(t *testing.T) { } node.balErr = test.balErr + node.tContractor.gasEstimates = gases + + var serverVer uint32 + if test.v1 { + serverVer = 1 + } maxOrder, err := w.MaxOrder(&asset.MaxOrderForm{ LotSize: ethToGwei(test.lotSize), - FeeSuggestion: test.feeSuggestion, // ignored - AssetVersion: assetCfg.Version, + AssetVersion: serverVer, MaxFeeRate: test.maxFeeRate, RedeemVersion: tBTC.Version, RedeemAssetID: tBTC.ID, @@ -3501,15 +3533,6 @@ func TestMaxOrder(t *testing.T) { } } -func overMaxWei() *big.Int { - maxInt := ^uint64(0) - maxWei := new(big.Int).SetUint64(maxInt) - gweiFactorBig := big.NewInt(dexeth.GweiFactor) - maxWei.Mul(maxWei, gweiFactorBig) - overMaxWei := new(big.Int).Set(maxWei) - return overMaxWei.Add(overMaxWei, gweiFactorBig) -} - func packInitiateDataV0(initiations []*dexeth.Initiation) ([]byte, error) { abiInitiations := make([]swapv0.ETHSwapInitiation, 0, len(initiations)) for _, init := range initiations { @@ -3568,7 +3591,7 @@ func testAuditContract(t *testing.T, assetID uint32) { }{ { name: "ok", - contract: dexeth.EncodeContractData(0, secretHashes[1]), + contract: dexeth.EncodeContractData(0, secretHashes[1][:]), initiations: []*dexeth.Initiation{ { LockTime: now, @@ -3588,7 +3611,7 @@ func testAuditContract(t *testing.T, assetID uint32) { }, { name: "coin id different than tx hash", - contract: dexeth.EncodeContractData(0, secretHashes[0]), + contract: dexeth.EncodeContractData(0, secretHashes[0][:]), initiations: []*dexeth.Initiation{ { LockTime: now, @@ -3607,7 +3630,7 @@ func testAuditContract(t *testing.T, assetID uint32) { }, { name: "contract not part of transaction", - contract: dexeth.EncodeContractData(0, secretHashes[2]), + contract: dexeth.EncodeContractData(0, secretHashes[2][:]), initiations: []*dexeth.Initiation{ { LockTime: now, @@ -3626,13 +3649,13 @@ func testAuditContract(t *testing.T, assetID uint32) { }, { name: "cannot parse tx data", - contract: dexeth.EncodeContractData(0, secretHashes[2]), + contract: dexeth.EncodeContractData(0, secretHashes[2][:]), badTxData: true, wantErr: true, }, { name: "cannot unmarshal tx binary", - contract: dexeth.EncodeContractData(0, secretHashes[1]), + contract: dexeth.EncodeContractData(0, secretHashes[1][:]), initiations: []*dexeth.Initiation{ { LockTime: now, @@ -3843,8 +3866,12 @@ func TestSwapConfirmation(t *testing.T) { var secretHash [32]byte copy(secretHash[:], encode.RandomBytes(32)) - state := &dexeth.SwapState{} - hdr := &types.Header{} + state := &dexeth.SwapState{ + Value: dexeth.GweiToWei(1), + } + hdr := &types.Header{ + GasLimit: 30_000_000, + } node.tContractor.swapMap[secretHash] = state @@ -3860,7 +3887,7 @@ func TestSwapConfirmation(t *testing.T) { checkResult := func(expErr bool, expConfs uint32, expSpent bool) { t.Helper() - confs, spent, err := eth.SwapConfirmations(ctx, nil, dexeth.EncodeContractData(ver, secretHash), time.Time{}) + confs, spent, err := eth.SwapConfirmations(ctx, nil, dexeth.EncodeContractData(ver, secretHash[:]), time.Time{}) if err != nil { if expErr { return @@ -3889,7 +3916,7 @@ func TestSwapConfirmation(t *testing.T) { // ErrSwapNotInitiated state.State = dexeth.SSNone - _, _, err := eth.SwapConfirmations(ctx, nil, dexeth.EncodeContractData(0, secretHash), time.Time{}) + _, _, err := eth.SwapConfirmations(ctx, nil, dexeth.EncodeContractData(0, secretHash[:]), time.Time{}) if !errors.Is(err, asset.ErrSwapNotInitiated) { t.Fatalf("expected ErrSwapNotInitiated, got %v", err) } @@ -4059,6 +4086,7 @@ func TestLocktimeExpired(t *testing.T) { state := &dexeth.SwapState{ LockTime: time.Now(), State: dexeth.SSInitiated, + Value: dexeth.GweiToWei(1), } header := &types.Header{ @@ -4132,10 +4160,11 @@ func testFindRedemption(t *testing.T, assetID uint32) { copy(secret[:], encode.RandomBytes(32)) secretHash := sha256.Sum256(secret[:]) - contract := dexeth.EncodeContractData(0, secretHash) + contract := dexeth.EncodeContractData(0, secretHash[:]) state := &dexeth.SwapState{ Secret: secret, State: dexeth.SSInitiated, + Value: dexeth.GweiToWei(1), } node.tContractor.swapMap[secretHash] = state @@ -4179,7 +4208,7 @@ func testFindRedemption(t *testing.T, assetID uint32) { select { case <-time.After(time.Millisecond): eth.findRedemptionMtx.RLock() - pending := eth.findRedemptionReqs[secretHash] != nil + pending := eth.findRedemptionReqs[string(secretHash[:])] != nil eth.findRedemptionMtx.RUnlock() if !pending { continue @@ -4243,7 +4272,7 @@ func testFindRedemption(t *testing.T, assetID uint32) { // dupe eth.findRedemptionMtx.Lock() - eth.findRedemptionReqs[secretHash] = &findRedemptionRequest{} + eth.findRedemptionReqs[string(secretHash[:])] = &findRedemptionRequest{} eth.findRedemptionMtx.Unlock() res := make(chan error, 1) go func() { @@ -4280,25 +4309,14 @@ func testRefundReserves(t *testing.T, assetID uint32) { node.swapMap = map[[32]byte]*dexeth.SwapState{secretHash: {}} feeWallet := eth - gasesV0 := dexeth.VersionedGases[0] - gasesV1 := &dexeth.Gases{Refund: 1e6} - assetV0 := *tETH - - assetV1 := *tETH - if assetID == BipID { - eth.versionedGases[1] = gasesV1 - } else { + gasesV0 := eth.versionedGases[0] + gasesV1 := eth.versionedGases[1] + assetV0 := *tETHV0 + assetV1 := *tETHV0 + if assetID != BipID { feeWallet = node.tokenParent - assetV0 = *tToken - assetV1 = *tToken - tokenContracts := eth.tokens[usdcTokenID].NetTokens[dex.Simnet].SwapContracts - tc := *tokenContracts[0] - tc.Gas = *gasesV1 - tokenContracts[1] = &tc - defer delete(tokenContracts, 1) - gasesV0 = &tokenGases - eth.versionedGases[0] = gasesV0 - eth.versionedGases[1] = gasesV1 + assetV0 = *tTokenV0 + assetV1 = *tTokenV0 node.tokenContractor.bal = dexeth.GweiToWei(1e9) } @@ -4379,21 +4397,17 @@ func testRedemptionReserves(t *testing.T, assetID uint32) { var secretHash [32]byte node.tContractor.swapMap[secretHash] = &dexeth.SwapState{} - gasesV1 := &dexeth.Gases{Redeem: 1e6, RedeemAdd: 85e5} - gasesV0 := dexeth.VersionedGases[0] - assetV0 := *tETH - assetV1 := *tETH + gasesV0 := eth.versionedGases[0] + gasesV1 := eth.versionedGases[1] + eth.versionedGases[1] = gasesV1 + assetV0 := *tETHV0 + assetV1 := *tETHV0 feeWallet := eth - if assetID == BipID { - eth.versionedGases[1] = gasesV1 - } else { + if assetID != BipID { node.tokenContractor.allow = unlimitedAllowanceReplenishThreshold feeWallet = node.tokenParent - assetV0 = *tToken - assetV1 = *tToken - gasesV0 = &tokenGases - eth.versionedGases[0] = gasesV0 - eth.versionedGases[1] = gasesV1 + assetV0 = *tTokenV0 + assetV1 = *tTokenV0 } assetV0.MaxFeeRate = 45 @@ -4500,7 +4514,7 @@ func testSend(t *testing.T, assetID uint32) { maxFeeRate, _, _ := eth.recommendedMaxFeeRate(eth.ctx) ethFees := dexeth.WeiToGwei(maxFeeRate) * defaultSendGasLimit - tokenFees := dexeth.WeiToGwei(maxFeeRate) * tokenGases.Transfer + tokenFees := dexeth.WeiToGwei(maxFeeRate) * tokenGasesV1.Transfer const val = 10e9 const testAddr = "dd93b447f7eBCA361805eBe056259853F3912E04" @@ -4553,12 +4567,12 @@ func testSend(t *testing.T, assetID uint32) { coin, err := w.Send(test.addr, val, 0) if test.wantErr { if err == nil { - t.Fatalf("expected error for test %v", test.name) + t.Fatalf("expected error for test %q", test.name) } continue } if err != nil { - t.Fatalf("unexpected error for test %v: %v", test.name, err) + t.Fatalf("unexpected error for test %q: %v", test.name, err) } if !bytes.Equal(txHash[:], coin.ID()) { t.Fatal("coin is not the tx hash") @@ -4588,7 +4602,7 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { redemption := &asset.Redemption{ Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(0, secretHash), + Contract: dexeth.EncodeContractData(0, secretHash[:]), }, Secret: secret[:], } @@ -4690,7 +4704,7 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { fmt.Printf("###### %s ###### \n", test.name) node.tContractor.swapMap = map[[32]byte]*dexeth.SwapState{ - secretHash: {State: test.step}, + secretHash: {State: test.step, Value: big.NewInt(1)}, } node.tContractor.lastRedeems = nil node.tokenContractor.bal = big.NewInt(1e9) @@ -4798,7 +4812,7 @@ func testEstimateSendTxFee(t *testing.T, assetID uint32) { maxFeeRate, _, _ := eth.recommendedMaxFeeRate(eth.ctx) ethFees := dexeth.WeiToGwei(maxFeeRate) * defaultSendGasLimit - tokenFees := dexeth.WeiToGwei(maxFeeRate) * tokenGases.Transfer + tokenFees := dexeth.WeiToGwei(maxFeeRate) * tokenGasesV1.Transfer ethFees = ethFees * 12 / 10 tokenFees = tokenFees * 12 / 10 @@ -4872,70 +4886,6 @@ func testEstimateSendTxFee(t *testing.T, assetID uint32) { } } -// This test will fail if new versions of the eth or the test token -// 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, usdcTokenID) }) -} - -func testMaxSwapRedeemLots(t *testing.T, assetID uint32) { - drv := &Driver{} - logger := dex.StdOutLogger("ETHTEST", dex.LevelOff) - tmpDir := t.TempDir() - - settings := map[string]string{providersKey: "a.ipc"} - err := CreateEVMWallet(dexeth.ChainIDs[dex.Testnet], &asset.CreateWalletParams{ - Type: walletTypeRPC, - Seed: encode.RandomBytes(32), - Pass: encode.RandomBytes(32), - Settings: settings, - DataDir: tmpDir, - Net: dex.Testnet, - Logger: logger, - }, &testnetCompatibilityData, true) - if err != nil { - t.Fatalf("CreateEVMWallet error: %v", err) - } - - wallet, err := drv.Open(&asset.WalletConfig{ - Type: walletTypeRPC, - Settings: settings, - DataDir: tmpDir, - }, logger, dex.Testnet) - if err != nil { - t.Fatalf("driver open error: %v", err) - } - - if assetID != BipID { - eth, _ := wallet.(*ETHWallet) - eth.net = dex.Simnet - wallet, err = eth.OpenTokenWallet(&asset.TokenConfig{ - AssetID: assetID, - }) - if err != nil { - t.Fatal(err) - } - } - - info := wallet.Info() - if assetID == BipID { - if info.MaxSwapsInTx != 28 { - t.Fatalf("expected 28 for max swaps but got %d", info.MaxSwapsInTx) - } - if info.MaxRedeemsInTx != 63 { - t.Fatalf("expected 63 for max redemptions but got %d", info.MaxRedeemsInTx) - } - } else { - if info.MaxSwapsInTx != 20 { - t.Fatalf("expected 20 for max swaps but got %d", info.MaxSwapsInTx) - } - if info.MaxRedeemsInTx != 45 { - t.Fatalf("expected 45 for max redemptions but got %d", info.MaxRedeemsInTx) - } - } -} - func TestSwapOrRedemptionFeesPaid(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -4946,7 +4896,7 @@ func TestSwapOrRedemptionFeesPaid(t *testing.T) { contractDataFn := func(ver uint32, secretH []byte) []byte { s := [32]byte{} copy(s[:], secretH) - return dexeth.EncodeContractData(ver, s) + return dexeth.EncodeContractData(ver, s[:]) } const tip = 100 const feeRate = 2 // gwei / gas diff --git a/client/asset/eth/multirpc_live_test.go b/client/asset/eth/multirpc_live_test.go index 28cff731e9..99d6b4e6ce 100644 --- a/client/asset/eth/multirpc_live_test.go +++ b/client/asset/eth/multirpc_live_test.go @@ -119,3 +119,11 @@ func TestMainnetCompliance(t *testing.T) { func TestReceiptsHaveEffectiveGasPrice(t *testing.T) { mt.TestReceiptsHaveEffectiveGasPrice(t) } + +func TestBlockStats(t *testing.T) { + mt.BlockStats(t, 5, 1024, dex.Mainnet) +} + +func TestTestnetBlockStats(t *testing.T) { + mt.BlockStats(t, 5, 1024, dex.Testnet) +} diff --git a/client/asset/eth/multirpc_test_util.go b/client/asset/eth/multirpc_test_util.go index 6bae75c605..0a51c1163f 100644 --- a/client/asset/eth/multirpc_test_util.go +++ b/client/asset/eth/multirpc_test_util.go @@ -389,30 +389,32 @@ func (m *MRPCTest) FeeHistory(t *testing.T, net dex.Network, blockTimeSecs, days }) } -func (m *MRPCTest) TipCaps(t *testing.T, net dex.Network) { +func (m *MRPCTest) BlockStats(t *testing.T, steps, skipN int, net dex.Network) { m.withClient(t, net, func(ctx context.Context, cl *multiRPCClient) { if err := cl.withAny(ctx, func(ctx context.Context, p *provider) error { blk, err := p.ec.BlockByNumber(ctx, nil) if err != nil { return err } - h := blk.Number() - const m = 20 // how many txs - var n int - for { + tip := blk.Number() + for step := 0; step < steps; step++ { + if step != 0 { + blk, err = p.ec.BlockByNumber(ctx, tip.Add(tip, big.NewInt(int64(-skipN*step)))) + if err != nil { + return err + } + } txs := blk.Transactions() - fmt.Printf("##### Block %d has %d transactions \n", h, len(txs)) + fmt.Printf("##### Block %d, %d transactions, gas limit = %d \n", blk.Number(), len(txs), blk.GasLimit()) + const maxTxs = 10 + var n int for _, tx := range txs { - n++ + fmt.Println("##### Tx tip cap =", fmtFee(tx.GasTipCap())) - } - if n >= m { - break - } - h.Add(h, big.NewInt(-1)) - blk, err = p.ec.BlockByNumber(ctx, h) - if err != nil { - return fmt.Errorf("error getting block %d: %w", h, err) + n++ + if n >= maxTxs { + break + } } } diff --git a/client/asset/eth/nodeclient_harness_test.go b/client/asset/eth/nodeclient_harness_test.go index de0a19721f..e80399570e 100644 --- a/client/asset/eth/nodeclient_harness_test.go +++ b/client/asset/eth/nodeclient_harness_test.go @@ -27,6 +27,7 @@ import ( "fmt" "math" "math/big" + "math/rand" "os" "os/exec" "os/signal" @@ -66,6 +67,7 @@ const ( var ( homeDir = os.Getenv("HOME") + harnessCtlDir = filepath.Join(homeDir, "dextest", "eth", "harness-ctl") simnetWalletDir = filepath.Join(homeDir, "dextest", "eth", "client_rpc_tests", "simnet") participantWalletDir = filepath.Join(homeDir, "dextest", "eth", "client_rpc_tests", "participant") testnetWalletDir string @@ -85,14 +87,17 @@ var ( participantAddr common.Address participantAcct *accounts.Account participantEthClient ethFetcher - ethSwapContractAddr common.Address simnetContractor contractor participantContractor contractor simnetTokenContractor tokenContractor participantTokenContractor tokenContractor - ethGases = dexeth.VersionedGases[0] + ethGases *dexeth.Gases tokenGases *dexeth.Gases - secPerBlock = 15 * time.Second + 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 running on + // testnet. + secPerBlock = time.Second // If you are testing on testnet, you must specify the rpcNode. You can also // specify it in the testnet-credentials.json file. rpcProviders []string @@ -115,6 +120,9 @@ var ( testnetParticipantWalletSeed string usdcID, _ = dex.BipSymbolID("usdc.eth") masterToken *dexeth.Token + + v1 bool + contractVer uint32 ) func newContract(stamp uint64, secretHash [32]byte, val uint64) *asset.Contract { @@ -126,10 +134,34 @@ func newContract(stamp uint64, secretHash [32]byte, val uint64) *asset.Contract } } -func newRedeem(secret, secretHash [32]byte) *asset.Redemption { +func acLocator(c *asset.Contract) []byte { + return makeLocator(bytesToArray(c.SecretHash), c.Value, c.LockTime) +} + +func makeLocator(secretHash [32]byte, valg, lockTime uint64) []byte { + if contractVer == 1 { + return (&dexeth.SwapVector{ + From: ethClient.address(), + To: participantEthClient.address(), + Value: dexeth.GweiToWei(valg), + SecretHash: secretHash, + LockTime: lockTime, + }).Locator() + } + return secretHash[:] +} + +func newRedeem(secret, secretHash [32]byte, valg, lockTime uint64) *asset.Redemption { return &asset.Redemption{ Spends: &asset.AuditInfo{ SecretHash: secretHash[:], + Recipient: participantEthClient.address().String(), + Expiration: time.Unix(int64(lockTime), 0), + Coin: &coin{ + // id: txHash, + value: valg, + }, + Contract: dexeth.EncodeContractData(contractVer, makeLocator(secretHash, valg, lockTime)), }, Secret: secret[:], } @@ -168,7 +200,7 @@ func waitForMined() error { if err != nil { return err } - const targetConfs = 1 + const targetConfs = 3 currentHeight := hdr.Number barrierHeight := new(big.Int).Add(currentHeight, big.NewInt(targetConfs)) fmt.Println("Waiting for RPC blocks") @@ -245,18 +277,16 @@ func runSimnet(m *testing.M) (int, error) { return 1, fmt.Errorf("error creating participant wallet dir: %v", err) } - const contractVer = 0 - tokenGases = &dexeth.Tokens[usdcID].NetTokens[dex.Simnet].SwapContracts[contractVer].Gas // ETH swap contract. masterToken = dexeth.Tokens[usdcID] token := masterToken.NetTokens[dex.Simnet] fmt.Printf("ETH swap contract address is %v\n", dexeth.ContractAddresses[contractVer][dex.Simnet]) - fmt.Printf("Token swap contract addr is %v\n", token.SwapContracts[0].Address) + fmt.Printf("Token swap contract addr is %v\n", token.SwapContracts[contractVer].Address) fmt.Printf("Test token contract addr is %v\n", token.Address) - ethSwapContractAddr = dexeth.ContractAddresses[contractVer][dex.Simnet] + contractAddr := dexeth.ContractAddresses[contractVer][dex.Simnet] initiatorProviders, participantProviders := rpcEndpoints(dex.Simnet) @@ -288,28 +318,10 @@ func runSimnet(m *testing.M) (int, error) { contractAddr, exists := dexeth.ContractAddresses[contractVer][dex.Simnet] if !exists || contractAddr == (common.Address{}) { - return 1, fmt.Errorf("no contract address for version %d", contractVer) + return 1, fmt.Errorf("no contract address for contract version %d", contractVer) } - if simnetContractor, err = newV0Contractor(contractAddr, simnetAddr, ethClient.contractBackend()); err != nil { - return 1, fmt.Errorf("newV0Contractor error: %w", err) - } - if participantContractor, err = newV0Contractor(contractAddr, participantAddr, participantEthClient.contractBackend()); err != nil { - return 1, fmt.Errorf("participant newV0Contractor error: %w", err) - } - - if simnetTokenContractor, err = newV0TokenContractor(dex.Simnet, dexeth.Tokens[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.Simnet, dexeth.Tokens[usdcID], participantAddr, participantEthClient.contractBackend()); err != nil { - return 1, fmt.Errorf("participant newV0TokenContractor error: %w", err) - } + prepareSimnetContractors() if err := ethClient.unlock(pw); err != nil { return 1, fmt.Errorf("error unlocking initiator client: %w", err) @@ -319,11 +331,6 @@ func runSimnet(m *testing.M) (int, error) { } // Fund the wallets. - homeDir, err := os.UserHomeDir() - if err != nil { - return 1, err - } - harnessCtlDir := filepath.Join(homeDir, "dextest", "eth", "harness-ctl") send := func(exe, addr, amt string) error { cmd := exec.CommandContext(ctx, exe, addr, amt) cmd.Dir = harnessCtlDir @@ -370,9 +377,8 @@ func runSimnet(m *testing.M) (int, error) { } func runTestnet(m *testing.M) (int, error) { - usdcID = usdcID masterToken = dexeth.Tokens[usdcID] - tokenGases = &masterToken.NetTokens[dex.Testnet].SwapContracts[0].Gas + tokenGases = &masterToken.NetTokens[dex.Testnet].SwapContracts[contractVer].Gas if testnetWalletSeed == "" || testnetParticipantWalletSeed == "" { return 1, errors.New("testnet seeds not set") } @@ -386,9 +392,9 @@ func runTestnet(m *testing.M) (int, error) { if err != nil { return 1, fmt.Errorf("error creating testnet participant wallet dir: %v", err) } - const contractVer = 0 - ethSwapContractAddr = dexeth.ContractAddresses[contractVer][dex.Testnet] - fmt.Printf("ETH swap contract address is %v\n", ethSwapContractAddr) + secPerBlock = testnetSecPerBlock + contractAddr := dexeth.ContractAddresses[contractVer][dex.Testnet] + fmt.Printf("Swap contract address is %v\n", contractAddr) initiatorRPC, participantRPC := rpcEndpoints(dex.Testnet) @@ -437,10 +443,15 @@ func runTestnet(m *testing.M) (int, error) { return 1, fmt.Errorf("no contract address for version %d", contractVer) } - if simnetContractor, err = newV0Contractor(contractAddr, simnetAddr, ethClient.contractBackend()); err != nil { + ctor := newV0Contractor + if contractVer == 1 { + ctor = newV1Contractor + } + + if simnetContractor, err = ctor(dex.Testnet, contractAddr, simnetAddr, ethClient.contractBackend()); err != nil { return 1, fmt.Errorf("newV0Contractor error: %w", err) } - if participantContractor, err = newV0Contractor(contractAddr, participantAddr, participantEthClient.contractBackend()); err != nil { + if participantContractor, err = ctor(dex.Testnet, contractAddr, participantAddr, participantEthClient.contractBackend()); err != nil { return 1, fmt.Errorf("participant newV0Contractor error: %w", err) } @@ -451,17 +462,28 @@ func runTestnet(m *testing.M) (int, error) { return 1, fmt.Errorf("error unlocking initiator client: %w", err) } - if simnetTokenContractor, err = newV0TokenContractor(dex.Testnet, dexeth.Tokens[usdcID], simnetAddr, ethClient.contractBackend()); err != nil { - return 1, fmt.Errorf("newV0TokenContractor error: %w", err) - } + if v1 { + scv1 := simnetContractor.(*contractorV1) + if simnetTokenContractor, err = scv1.tokenContractor(dexeth.Tokens[usdcID]); err != nil { + return 1, fmt.Errorf("v1 tokenContractor error: %w", err) + } + pcv1 := participantContractor.(*contractorV1) + if participantTokenContractor, err = pcv1.tokenContractor(dexeth.Tokens[usdcID]); err != nil { + return 1, fmt.Errorf("participant v1 tokenContractor error: %w", err) + } + } else { + if simnetTokenContractor, err = newV0TokenContractor(dex.Testnet, dexeth.Tokens[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) + // 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, dexeth.Tokens[usdcID], participantAddr, participantEthClient.contractBackend()); err != nil { - return 1, fmt.Errorf("participant newV0TokenContractor error: %w", err) + if participantTokenContractor, err = newV0TokenContractor(dex.Testnet, dexeth.Tokens[usdcID], participantAddr, participantEthClient.contractBackend()); err != nil { + return 1, fmt.Errorf("participant newV0TokenContractor error: %w", err) + } } code := m.Run() @@ -480,6 +502,50 @@ func runTestnet(m *testing.M) (int, error) { return code, nil } +func prepareSimnetContractors() (err error) { + contractAddr := dexeth.ContractAddresses[contractVer][dex.Simnet] + + ctor := newV0Contractor + if contractVer == 1 { + ctor = newV1Contractor + } + + if simnetContractor, err = ctor(dex.Simnet, contractAddr, simnetAddr, ethClient.contractBackend()); err != nil { + return fmt.Errorf("new contractor error: %w", err) + } + if participantContractor, err = ctor(dex.Simnet, contractAddr, participantAddr, participantEthClient.contractBackend()); err != nil { + return fmt.Errorf("participant new contractor error: %w", err) + } + + token := dexeth.Tokens[usdcID] + + if v1 { + scv1 := simnetContractor.(*contractorV1) + if simnetTokenContractor, err = scv1.tokenContractor(token); err != nil { + return fmt.Errorf("v1 tokenContractor error: %w", err) + } + pcv1 := participantContractor.(*contractorV1) + if participantTokenContractor, err = pcv1.tokenContractor(token); err != nil { + return fmt.Errorf("participant v1 tokenContractor error: %w", err) + } + } else { + if simnetTokenContractor, err = newV0TokenContractor(dex.Simnet, token, simnetAddr, ethClient.contractBackend()); err != nil { + return 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.Simnet, token, participantAddr, participantEthClient.contractBackend()); err != nil { + return fmt.Errorf("participant newV0TokenContractor error: %w", err) + } + } + + return +} + func useTestnet() error { isTestnet = true b, err := os.ReadFile(filepath.Join(homeDir, "dextest", "credentials.json")) @@ -501,11 +567,19 @@ func useTestnet() error { } func TestMain(m *testing.M) { + rand.Seed(time.Now().UnixNano()) dexeth.MaybeReadSimnetAddrs() flag.BoolVar(&isTestnet, "testnet", false, "use testnet") + flag.BoolVar(&v1, "v1", true, "Use Version 1 contract") flag.Parse() + if v1 { + contractVer = 1 + } + + ethGases = dexeth.VersionedGases[contractVer] + if isTestnet { tmpDir, err := os.MkdirTemp("", "") if err != nil { @@ -674,20 +748,20 @@ func TestContract(t *testing.T) { if !t.Run("testAddressesHaveFunds", testAddressesHaveFundsFn(100_000_000 /* gwei */)) { t.Fatal("not enough funds") } - t.Run("testSwap", func(t *testing.T) { testSwap(t, BipID) }) + // t.Run("testSwap", func(t *testing.T) { testSwap(t, BipID) }) // TODO: Replace with testStatusAndVector? t.Run("testInitiate", func(t *testing.T) { testInitiate(t, BipID) }) t.Run("testRedeem", func(t *testing.T) { testRedeem(t, BipID) }) t.Run("testRefund", func(t *testing.T) { testRefund(t, BipID) }) } func TestGas(t *testing.T) { - t.Run("testInitiateGas", func(t *testing.T) { testInitiateGas(t, BipID) }) + // t.Run("testInitiateGas", func(t *testing.T) { testInitiateGas(t, BipID) }) t.Run("testRedeemGas", func(t *testing.T) { testRedeemGas(t, BipID) }) t.Run("testRefundGas", func(t *testing.T) { testRefundGas(t, BipID) }) } func TestTokenContract(t *testing.T) { - t.Run("testTokenSwap", func(t *testing.T) { testSwap(t, usdcID) }) + // t.Run("testTokenSwap", func(t *testing.T) { testSwap(t, usdcID) }) // TODO: Replace with testTokenStatusAndVector? t.Run("testInitiateToken", func(t *testing.T) { testInitiate(t, usdcID) }) t.Run("testRedeemToken", func(t *testing.T) { testRedeem(t, usdcID) }) t.Run("testRefundToken", func(t *testing.T) { testRefund(t, usdcID) }) @@ -696,7 +770,7 @@ func TestTokenContract(t *testing.T) { func TestTokenGas(t *testing.T) { t.Run("testTransferGas", testTransferGas) t.Run("testApproveGas", testApproveGas) - t.Run("testInitiateTokenGas", func(t *testing.T) { testInitiateGas(t, usdcID) }) + // t.Run("testInitiateTokenGas", func(t *testing.T) { testInitiateGas(t, usdcID) }) t.Run("testRedeemTokenGas", func(t *testing.T) { testRedeemGas(t, usdcID) }) t.Run("testRefundTokenGas", func(t *testing.T) { testRefundGas(t, usdcID) }) } @@ -957,17 +1031,6 @@ func testPendingTransactions(t *testing.T) { spew.Dump(txs) } -func testSwap(t *testing.T, assetID uint32) { - var secretHash [32]byte - copy(secretHash[:], encode.RandomBytes(32)) - swap, err := simnetContractor.swap(ctx, secretHash) - if err != nil { - t.Fatal(err) - } - // Should be empty. - spew.Dump(swap) -} - func testSyncProgress(t *testing.T) { p, _, err := ethClient.syncProgress(ctx) if err != nil { @@ -977,25 +1040,13 @@ func testSyncProgress(t *testing.T) { } func testInitiateGas(t *testing.T, assetID uint32) { - if assetID != BipID { - prepareTokenClients(t) - } - - net := dex.Simnet - if isTestnet { - net = dex.Testnet - } - + gases := ethGases c := simnetContractor - versionedGases := dexeth.VersionedGases if assetID != BipID { c = simnetTokenContractor - versionedGases = make(map[uint32]*dexeth.Gases) - for ver, c := range dexeth.Tokens[assetID].NetTokens[net].SwapContracts { - versionedGases[ver] = &c.Gas - } + prepareTokenClients(t) + gases = tokenGases } - gases := gases(0, versionedGases) var previousGas uint64 maxSwaps := 50 @@ -1014,7 +1065,7 @@ func testInitiateGas(t *testing.T, assetID uint32) { expectedGas = gases.SwapAdd actualGas = gas - previousGas } - if actualGas > expectedGas || actualGas < expectedGas*70/100 { + if actualGas > expectedGas || actualGas < expectedGas/2 { t.Fatalf("Expected incremental gas for %d initiations to be close to %d but got %d", i, expectedGas, actualGas) } @@ -1097,8 +1148,12 @@ func testInitiate(t *testing.T, assetID uint32) { return simnetTokenContractor.balance(ctx) } gases = tokenGases - tc := sc.(*tokenContractorV0) - evmify = tc.evmify + switch contractVer { + case 0: + evmify = sc.(*tokenContractorV0).evmify + case 1: + evmify = sc.(*tokenContractorV1).evmify + } } // Create a slice of random secret hashes that can be used in the tests and @@ -1106,15 +1161,7 @@ func testInitiate(t *testing.T, assetID uint32) { numSecretHashes := 10 secretHashes := make([][32]byte, numSecretHashes) for i := 0; i < numSecretHashes; i++ { - copy(secretHashes[i][:], encode.RandomBytes(32)) - swap, err := sc.swap(ctx, secretHashes[i]) - if err != nil { - t.Fatal("unable to get swap state") - } - state := dexeth.SwapStep(swap.State) - if state != dexeth.SSNone { - t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSNone, state) - } + secretHashes[i] = bytesToArray(encode.RandomBytes(32)) } now := uint64(time.Now().Unix()) @@ -1133,10 +1180,10 @@ func testInitiate(t *testing.T, assetID uint32) { }, }, { - name: "1 swap with existing hash", + name: "1 duplicate swap", success: false, swaps: []*asset.Contract{ - newContract(now, secretHashes[0], 1), + newContract(now, secretHashes[0], 2), }, }, { @@ -1192,28 +1239,29 @@ func testInitiate(t *testing.T, assetID uint32) { t.Fatalf("balance error for asset %d, test %s: %v", assetID, test.name, err) } + if !isETH { + originalParentBal, err = ethClient.addressBalance(ctx, ethClient.address()) + if err != nil { + t.Fatalf("balance error for eth, test %s: %v", test.name, err) + } + } + var totalVal uint64 originalStates := make(map[string]dexeth.SwapStep) for _, tSwap := range test.swaps { - swap, err := sc.swap(ctx, bytesToArray(tSwap.SecretHash)) + status, _, err := sc.statusAndVector(ctx, acLocator(tSwap)) if err != nil { t.Fatalf("%s: swap error: %v", test.name, err) } - originalStates[tSwap.SecretHash.String()] = dexeth.SwapStep(swap.State) + originalStates[tSwap.SecretHash.String()] = status.Step totalVal += tSwap.Value } optsVal := totalVal - if !isETH { - optsVal = 0 - originalParentBal, err = ethClient.addressBalance(ctx, ethClient.address()) - if err != nil { - t.Fatalf("balance error for eth, test %s: %v", test.name, err) - } - } - if test.overflow { optsVal = 2 + } else if !isETH { + optsVal = 0 } expGas := gases.SwapN(len(test.swaps)) @@ -1222,7 +1270,7 @@ func testInitiate(t *testing.T, assetID uint32) { t.Fatalf("%s: txOpts error: %v", test.name, err) } var tx *types.Transaction - if test.overflow { + if test.overflow && !v1 { // We're limited by uint64 in v1 switch c := sc.(type) { case *contractorV0: tx, err = initiateOverflow(c, txOpts, test.swaps) @@ -1295,19 +1343,18 @@ func testInitiate(t *testing.T, assetID uint32) { } for _, tSwap := range test.swaps { - swap, err := sc.swap(ctx, bytesToArray(tSwap.SecretHash)) + status, _, err := sc.statusAndVector(ctx, acLocator(tSwap)) if err != nil { t.Fatalf("%s: swap error post-init: %v", test.name, err) } - state := dexeth.SwapStep(swap.State) - if test.success && state != dexeth.SSInitiated { - t.Fatalf("%s: wrong success swap state: want %s got %s", test.name, dexeth.SSInitiated, state) + if test.success && status.Step != dexeth.SSInitiated { + t.Fatalf("%s: wrong success swap state: want %s got %s", test.name, dexeth.SSInitiated, status.Step) } originalState := originalStates[hex.EncodeToString(tSwap.SecretHash[:])] - if !test.success && state != originalState { - t.Fatalf("%s: wrong error swap state: want %s got %s", test.name, originalState, state) + if !test.success && status.Step != originalState { + t.Fatalf("%s: wrong error swap state: want %s got %s", test.name, originalState, status.Step) } } } @@ -1334,8 +1381,11 @@ func testRedeemGas(t *testing.T, assetID uint32) { now := uint64(time.Now().Unix()) swaps := make([]*asset.Contract, 0, numSwaps) + locators := make([][]byte, 0, numSwaps) for i := 0; i < numSwaps; i++ { - swaps = append(swaps, newContract(now, secretHashes[i], 1)) + c := newContract(now, secretHashes[i], 1) + swaps = append(swaps, c) + locators = append(locators, acLocator(c)) } gases := ethGases @@ -1373,19 +1423,19 @@ func testRedeemGas(t *testing.T, assetID uint32) { // Make sure swaps were properly initiated for i := range swaps { - swap, err := c.swap(ctx, bytesToArray(swaps[i].SecretHash)) + status, _, err := c.statusAndVector(ctx, locators[i]) if err != nil { t.Fatal("unable to get swap state") } - if swap.State != dexeth.SSInitiated { - t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSInitiated, swap.State) + if status.Step != dexeth.SSInitiated { + t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSInitiated, status.Step) } } // Test gas usage of redeem function var previous uint64 for i := 0; i < numSwaps; i++ { - gas, err := pc.estimateRedeemGas(ctx, secrets[:i+1]) + gas, err := pc.estimateRedeemGas(ctx, secrets[:i+1], locators[:i+1]) if err != nil { t.Fatalf("Error estimating gas for redeem function: %v", err) } @@ -1399,9 +1449,9 @@ func testRedeemGas(t *testing.T, assetID uint32) { expectedGas = gases.RedeemAdd actualGas = gas - previous } - if actualGas > expectedGas || actualGas < (expectedGas*70/100) { + if actualGas > expectedGas || actualGas < (expectedGas/2) { // Use GetGasEstimates to better precision estimates. t.Fatalf("Expected incremental gas for %d redemptions to be close to %d but got %d", - i, expectedGas, actualGas) + i+1, expectedGas, actualGas) } fmt.Printf("\n\nGas used to redeem %d swaps: %d -- %d more than previous \n\n", i+1, gas, gas-previous) @@ -1436,6 +1486,8 @@ func testRedeem(t *testing.T, assetID uint32) { evmify = tc.evmify } + const val = 1 + tests := []struct { name string sleepNBlocks int @@ -1454,8 +1506,8 @@ func testRedeem(t *testing.T, assetID uint32) { redeemerClient: participantEthClient, redeemer: participantAcct, redeemerContractor: pc, - swaps: []*asset.Contract{newContract(lockTime, secretHashes[0], 1)}, - redemptions: []*asset.Redemption{newRedeem(secrets[0], secretHashes[0])}, + swaps: []*asset.Contract{newContract(lockTime, secretHashes[0], val)}, + redemptions: []*asset.Redemption{newRedeem(secrets[0], secretHashes[0], val, lockTime)}, finalStates: []dexeth.SwapStep{dexeth.SSRedeemed}, addAmt: true, }, @@ -1466,12 +1518,12 @@ func testRedeem(t *testing.T, assetID uint32) { redeemer: participantAcct, redeemerContractor: pc, swaps: []*asset.Contract{ - newContract(lockTime, secretHashes[1], 1), - newContract(lockTime, secretHashes[2], 1), + newContract(lockTime, secretHashes[1], val), + newContract(lockTime, secretHashes[2], val), }, redemptions: []*asset.Redemption{ - newRedeem(secrets[1], secretHashes[1]), - newRedeem(secrets[2], secretHashes[2]), + newRedeem(secrets[1], secretHashes[1], val, lockTime), + newRedeem(secrets[2], secretHashes[2], val, lockTime), }, finalStates: []dexeth.SwapStep{ dexeth.SSRedeemed, dexeth.SSRedeemed, @@ -1484,8 +1536,8 @@ func testRedeem(t *testing.T, assetID uint32) { redeemerClient: participantEthClient, redeemer: participantAcct, redeemerContractor: pc, - swaps: []*asset.Contract{newContract(lockTime, secretHashes[3], 1)}, - redemptions: []*asset.Redemption{newRedeem(secrets[3], secretHashes[3])}, + swaps: []*asset.Contract{newContract(lockTime, secretHashes[3], val)}, + redemptions: []*asset.Redemption{newRedeem(secrets[3], secretHashes[3], val, lockTime)}, finalStates: []dexeth.SwapStep{dexeth.SSRedeemed}, addAmt: true, }, @@ -1495,19 +1547,20 @@ func testRedeem(t *testing.T, assetID uint32) { redeemerClient: ethClient, redeemer: simnetAcct, redeemerContractor: c, - swaps: []*asset.Contract{newContract(lockTime, secretHashes[4], 1)}, - redemptions: []*asset.Redemption{newRedeem(secrets[4], secretHashes[4])}, + swaps: []*asset.Contract{newContract(lockTime, secretHashes[4], val)}, + redemptions: []*asset.Redemption{newRedeem(secrets[4], secretHashes[4], val, lockTime)}, finalStates: []dexeth.SwapStep{dexeth.SSInitiated}, addAmt: false, }, { name: "bad secret", + expectRedeemErr: true, sleepNBlocks: 8, redeemerClient: participantEthClient, redeemer: participantAcct, redeemerContractor: pc, - swaps: []*asset.Contract{newContract(lockTime, secretHashes[5], 1)}, - redemptions: []*asset.Redemption{newRedeem(secrets[6], secretHashes[5])}, + swaps: []*asset.Contract{newContract(lockTime, secretHashes[5], val)}, + redemptions: []*asset.Redemption{newRedeem(secrets[6], secretHashes[5], val, lockTime)}, finalStates: []dexeth.SwapStep{dexeth.SSInitiated}, addAmt: false, }, @@ -1519,12 +1572,12 @@ func testRedeem(t *testing.T, assetID uint32) { redeemer: participantAcct, redeemerContractor: pc, swaps: []*asset.Contract{ - newContract(lockTime, secretHashes[7], 1), - newContract(lockTime, secretHashes[8], 1), + newContract(lockTime, secretHashes[7], val), + newContract(lockTime, secretHashes[8], val), }, redemptions: []*asset.Redemption{ - newRedeem(secrets[7], secretHashes[7]), - newRedeem(secrets[7], secretHashes[7]), + newRedeem(secrets[7], secretHashes[7], val, lockTime), + newRedeem(secrets[7], secretHashes[7], val, lockTime), }, finalStates: []dexeth.SwapStep{ dexeth.SSInitiated, @@ -1536,14 +1589,16 @@ func testRedeem(t *testing.T, assetID uint32) { for _, test := range tests { var optsVal uint64 - for i, contract := range test.swaps { - swap, err := c.swap(ctx, bytesToArray(test.swaps[i].SecretHash)) + locators := make([][]byte, 0, len(test.swaps)) + for _, contract := range test.swaps { + locator := acLocator(contract) + locators = append(locators, locator) + status, _, err := c.statusAndVector(ctx, locator) if err != nil { t.Fatal("unable to get swap state") } - state := dexeth.SwapStep(swap.State) - if state != dexeth.SSNone { - t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSNone, state) + if status.Step != dexeth.SSNone { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSNone, status.Step) } if isETH { optsVal += contract.Value @@ -1563,7 +1618,7 @@ func testRedeem(t *testing.T, assetID uint32) { if err != nil { t.Fatalf("%s: txOpts error: %v", test.name, err) } - tx, err := test.redeemerContractor.initiate(txOpts, test.swaps) + tx, err := c.initiate(txOpts, test.swaps) if err != nil { t.Fatalf("%s: initiate error: %v ", test.name, err) } @@ -1586,12 +1641,12 @@ func testRedeem(t *testing.T, assetID uint32) { fmt.Printf("Gas used for %d inits: %d \n", len(test.swaps), receipt.GasUsed) for i := range test.swaps { - swap, err := test.redeemerContractor.swap(ctx, bytesToArray(test.swaps[i].SecretHash)) + status, _, err := test.redeemerContractor.statusAndVector(ctx, locators[i]) if err != nil { t.Fatal("unable to get swap state") } - if swap.State != dexeth.SSInitiated { - t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSInitiated, swap.State) + if status.Step != dexeth.SSInitiated { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSInitiated, status.Step) } } @@ -1691,15 +1746,14 @@ func testRedeem(t *testing.T, assetID uint32) { test.name, wantBal, bal, diff) } - for i, redemption := range test.redemptions { - swap, err := c.swap(ctx, bytesToArray(redemption.Spends.SecretHash)) + for i := range test.redemptions { + status, _, err := c.statusAndVector(ctx, locators[i]) if err != nil { t.Fatalf("unexpected error for test %v: %v", test.name, err) } - state := dexeth.SwapStep(swap.State) - if state != test.finalStates[i] { + if status.Step != test.finalStates[i] { t.Fatalf("unexpected swap state for test %v [%d]: want %s got %s", - test.name, i, test.finalStates[i], state) + test.name, i, test.finalStates[i], status.Step) } } } @@ -1731,7 +1785,9 @@ func testRefundGas(t *testing.T, assetID uint32) { if err != nil { t.Fatalf("txOpts error: %v", err) } - _, err = c.initiate(txOpts, []*asset.Contract{newContract(lockTime, secretHash, 1)}) + ac := newContract(lockTime, secretHash, 1) + locator := acLocator(ac) + _, err = c.initiate(txOpts, []*asset.Contract{ac}) if err != nil { t.Fatalf("Unable to initiate swap: %v ", err) } @@ -1739,22 +1795,21 @@ func testRefundGas(t *testing.T, assetID uint32) { t.Fatalf("unexpected error while waiting to mine: %v", err) } - swap, err := c.swap(ctx, secretHash) + status, _, err := c.statusAndVector(ctx, locator) if err != nil { t.Fatal("unable to get swap state") } - state := dexeth.SwapStep(swap.State) - if state != dexeth.SSInitiated { - t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSInitiated, state) + if status.Step != dexeth.SSInitiated { + t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSInitiated, status.Step) } - gas, err := c.estimateRefundGas(ctx, secretHash) + gas, err := c.estimateRefundGas(ctx, locator) if err != nil { t.Fatalf("Error estimating gas for refund function: %v", err) } if isETH { expGas := gases.Refund - if gas > expGas || gas < expGas*70/100 { + if gas > expGas || gas < expGas/2 { t.Fatalf("expected refund gas to be near %d, but got %d", expGas, gas) } @@ -1845,21 +1900,23 @@ func testRefund(t *testing.T, assetID uint32) { copy(secret[:], encode.RandomBytes(32)) secretHash := sha256.Sum256(secret[:]) - swap, err := test.refunderContractor.swap(ctx, secretHash) + inLocktime := uint64(time.Now().Add(test.addTime).Unix()) + ac := newContract(inLocktime, secretHash, amt) + locator := acLocator(ac) + + status, _, err := test.refunderContractor.statusAndVector(ctx, locator) if err != nil { t.Fatalf("%s: unable to get swap state pre-init", test.name) } - if swap.State != dexeth.SSNone { - t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSNone, swap.State) + if status.Step != dexeth.SSNone { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSNone, status.Step) } - inLocktime := uint64(time.Now().Add(test.addTime).Unix()) - txOpts, err := ethClient.txOpts(ctx, optsVal, gases.SwapN(1), nil, nil, nil) if err != nil { t.Fatalf("%s: txOpts error: %v", test.name, err) } - _, err = c.initiate(txOpts, []*asset.Contract{newContract(inLocktime, secretHash, amt)}) + _, err = c.initiate(txOpts, []*asset.Contract{ac}) if err != nil { t.Fatalf("%s: initiate error: %v ", test.name, err) } @@ -1873,7 +1930,7 @@ func testRefund(t *testing.T, assetID uint32) { if err != nil { t.Fatalf("%s: txOpts error: %v", test.name, err) } - _, err := pc.redeem(txOpts, []*asset.Redemption{newRedeem(secret, secretHash)}) + _, err := pc.redeem(txOpts, []*asset.Redemption{newRedeem(secret, secretHash, amt, inLocktime)}) if err != nil { t.Fatalf("%s: redeem error: %v", test.name, err) } @@ -1897,7 +1954,7 @@ func testRefund(t *testing.T, assetID uint32) { t.Fatalf("%s: balance error: %v", test.name, err) } - isRefundable, err := test.refunderContractor.isRefundable(secretHash) + isRefundable, err := test.refunderContractor.isRefundable(locator) if err != nil { t.Fatalf("%s: isRefundable error %v", test.name, err) } @@ -1910,7 +1967,7 @@ func testRefund(t *testing.T, assetID uint32) { if err != nil { t.Fatalf("%s: txOpts error: %v", test.name, err) } - tx, err := test.refunderContractor.refund(txOpts, secretHash) + tx, err := test.refunderContractor.refund(txOpts, locator) if err != nil { t.Fatalf("%s: refund error: %v", test.name, err) } @@ -1985,12 +2042,12 @@ func testRefund(t *testing.T, assetID uint32) { test.name, wantBal, bal, diff) } - swap, err = test.refunderContractor.swap(ctx, secretHash) + status, _, err = test.refunderContractor.statusAndVector(ctx, locator) if err != nil { t.Fatalf("%s: post-refund swap error: %v", test.name, err) } - if swap.State != test.finalState { - t.Fatalf("%s: wrong swap state: want %s got %s", test.name, test.finalState, swap.State) + if status.Step != test.finalState { + t.Fatalf("%s: wrong swap state: want %s got %s", test.name, test.finalState, status.Step) } } } @@ -2141,7 +2198,7 @@ func TestTokenGasEstimates(t *testing.T) { runSimnetMiner(ctx, "eth", tLogger) prepareTokenClients(t) tLogger.SetLevel(dex.LevelInfo) - if err := getGasEstimates(ctx, ethClient, participantEthClient, simnetTokenContractor, participantTokenContractor, 5, tokenGases, tLogger); err != nil { + if err := getGasEstimates(ctx, ethClient, participantEthClient, simnetTokenContractor, participantTokenContractor, 5, contractVer, tokenGases, dexeth.GweiToWei, tLogger); err != nil { t.Fatalf("getGasEstimates error: %v", err) } } diff --git a/client/asset/interface.go b/client/asset/interface.go index 68af08d7f4..bd4df0d192 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -300,8 +300,9 @@ type WalletDefinition struct { // Token combines the generic dex.Token with a WalletDefinition. type Token struct { *dex.Token - Definition *WalletDefinition `json:"definition"` - ContractAddress string `json:"contractAddress"` // Set in SetNetwork + Definition *WalletDefinition `json:"definition"` + ContractAddress string `json:"contractAddress"` // Set in SetNetwork + SupportedAssetVersions []uint32 `json:"supportedAssetVersions"` } // WalletInfo is auxiliary information about an ExchangeWallet. @@ -325,12 +326,6 @@ type WalletInfo struct { // UnitInfo is the information about unit names and conversion factors for // the asset. UnitInfo dex.UnitInfo `json:"unitinfo"` - // MaxSwapsInTx is the max amount of swaps that this wallet can do in a - // single transaction. - MaxSwapsInTx uint64 - // MaxRedeemsInTx is the max amount of redemptions that this wallet can do - // in a single transaction. - MaxRedeemsInTx uint64 // IsAccountBased should be set to true for account-based (EVM) assets, so // that a common seed will be generated and wallets will generate the // same address. @@ -1340,8 +1335,8 @@ type AuditInfo struct { // Swaps is the details needed to broadcast a swap contract(s). type Swaps struct { - // Version is the asset version. - Version uint32 + // AssetVersion is the server's asset version.`` + AssetVersion uint32 // Inputs are the Coins being spent. Inputs Coins // Contract is the contract data. @@ -1392,9 +1387,9 @@ type RedeemForm struct { // Order is order details needed for FundOrder. type Order struct { - // Version is the asset version of the "from" asset with the init + // AssetVersion is the asset version of the "from" asset with the init // transaction. - Version uint32 + AssetVersion uint32 // Value is the amount required to satisfy the order. The Value does not // include fees. Fees will be calculated internally based on the number of // possible swaps (MaxSwapCount) and the exchange's configuration @@ -1446,10 +1441,10 @@ type MultiOrderValue struct { // MultiOrder is order details needed for FundMultiOrder. type MultiOrder struct { - // Version is the asset version of the "from" asset with the init + // AssetVersion is the asset version of the "from" asset with the init // transaction. - Version uint32 - Values []*MultiOrderValue + AssetVersion uint32 + Values []*MultiOrderValue // MaxFeeRate is the largest possible fee rate for the init transaction (of // this "from" asset) specific to and provided by a particular server, and // is used to calculate the funding required to cover fees. @@ -1478,6 +1473,12 @@ type GeocodeRedeemer interface { RedeemGeocode(code []byte, msg string) (dex.Bytes, uint64, error) } +// MaxMatchesCounter counts the maximum number of matches that can go in a tx. +type MaxMatchesCounter interface { + MaxSwaps(serverVer uint32, feeRate uint64) (int, error) + MaxRedeems(serverVer uint32) (int, error) +} + // WalletNotification can be any asynchronous information the wallet needs // to convey. type WalletNotification any diff --git a/client/asset/polygon/multirpc_live_test.go b/client/asset/polygon/multirpc_live_test.go index 551be982b0..fa9d62bd92 100644 --- a/client/asset/polygon/multirpc_live_test.go +++ b/client/asset/polygon/multirpc_live_test.go @@ -110,16 +110,16 @@ func TestTestnetFees(t *testing.T) { mt.FeeHistory(t, dex.Testnet, 3, 90) } -func TestTestnetTipCaps(t *testing.T) { - mt.TipCaps(t, dex.Testnet) +func TestBlockStats(t *testing.T) { + mt.BlockStats(t, 5, 1024, dex.Mainnet) } -func TestFees(t *testing.T) { - mt.FeeHistory(t, dex.Mainnet, 3, 365) +func TestTestnetBlockStats(t *testing.T) { + mt.BlockStats(t, 5, 1024, dex.Testnet) } -func TestTipCaps(t *testing.T) { - mt.TipCaps(t, dex.Mainnet) +func TestFees(t *testing.T) { + mt.FeeHistory(t, dex.Mainnet, 3, 365) } func TestReceiptsHaveEffectiveGasPrice(t *testing.T) { diff --git a/client/asset/polygon/polygon.go b/client/asset/polygon/polygon.go index 6e65cf8dff..50ace5d1df 100644 --- a/client/asset/polygon/polygon.go +++ b/client/asset/polygon/polygon.go @@ -12,6 +12,7 @@ import ( "decred.org/dcrdex/client/asset" "decred.org/dcrdex/client/asset/eth" "decred.org/dcrdex/dex" + dexeth "decred.org/dcrdex/dex/networks/eth" dexpolygon "decred.org/dcrdex/dex/networks/polygon" "github.com/ethereum/go-ethereum/common" ) @@ -26,14 +27,19 @@ func registerToken(tokenID uint32, desc string, nets ...dex.Network) { panic("token " + strconv.Itoa(int(tokenID)) + " not known") } netAddrs := make(map[dex.Network]string) + netVersions := make(map[dex.Network][]uint32, 3) for net, netToken := range token.NetTokens { netAddrs[net] = netToken.Address.String() + netVersions[net] = make([]uint32, 0, 1) + for ver := range netToken.SwapContracts { + netVersions[net] = append(netVersions[net], ver) + } } asset.RegisterToken(tokenID, token.Token, &asset.WalletDefinition{ Type: walletTypeToken, Tab: "Polygon token", Description: desc, - }, netAddrs) + }, netAddrs, netVersions) } func init() { @@ -73,7 +79,7 @@ var ( } WalletInfo = asset.WalletInfo{ Name: "Polygon", - SupportedVersions: []uint32{0}, + SupportedVersions: []uint32{0, 1}, UnitInfo: dexpolygon.UnitInfo, AvailableWallets: []*asset.WalletDefinition{ { @@ -84,8 +90,6 @@ var ( Seeded: true, NoAuth: true, }, - // MaxSwapsInTx and MaxRedeemsInTx are set in (Wallet).Info, since - // the value cannot be known until we connect and get network info. }, IsAccountBased: true, } @@ -152,6 +156,7 @@ func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, net dex.Networ WalletInfo: WalletInfo, Net: net, DefaultProviders: defaultProviders, + MaxTxFeeGwei: 1000 * dexeth.GweiFactor, // 1000 POL }) } diff --git a/client/asset/zec/regnet_test.go b/client/asset/zec/regnet_test.go index 419949426e..6088018d1d 100644 --- a/client/asset/zec/regnet_test.go +++ b/client/asset/zec/regnet_test.go @@ -266,7 +266,7 @@ func TestMultiSplit(t *testing.T) { // All funds should be transparent now. multiFund := &asset.MultiOrder{ - Version: version, + AssetVersion: version, Values: []*asset.MultiOrderValue{ {Value: v0, MaxSwapCount: 1}, {Value: v1, MaxSwapCount: 2}, diff --git a/client/asset/zec/zec_test.go b/client/asset/zec/zec_test.go index 7795d32073..a3fef741be 100644 --- a/client/asset/zec/zec_test.go +++ b/client/asset/zec/zec_test.go @@ -538,7 +538,7 @@ func TestFundOrder(t *testing.T) { } ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: 0, MaxSwapCount: 1, Options: make(map[string]string), @@ -1335,10 +1335,10 @@ func TestPreSwap(t *testing.T) { } form := &asset.PreSwapForm{ - Version: version, - LotSize: tLotSize, - Lots: lots, - Immediate: false, + AssetVersion: version, + LotSize: tLotSize, + Lots: lots, + Immediate: false, } setFunds(minReq) diff --git a/client/cmd/bwctl/simnet-setup.sh b/client/cmd/bwctl/simnet-setup.sh index aaf6a888d8..05a6f99dae 100755 --- a/client/cmd/bwctl/simnet-setup.sh +++ b/client/cmd/bwctl/simnet-setup.sh @@ -3,7 +3,7 @@ # dcrdex, bisonw, and the wallet simnet harnesses should all be running before # calling this script. # -# bisonw can be built with -ldflags "-X 'decred.org/dcrdex/dex.testLockTimeTaker=30s' -X 'decred.org/dcrdex/dex.testLockTimeMaker=1m'" +# bisonw can be built with -ldflags "-X 'decred.org/dcrdex/dex.testLockTimeTaker=3m' -X 'decred.org/dcrdex/dex.testLockTimeMaker=6m'" # in order to set simnet locktimes. set +e diff --git a/client/cmd/simnet-trade-tests/run b/client/cmd/simnet-trade-tests/run index 4303d20354..f2bf586ea9 100755 --- a/client/cmd/simnet-trade-tests/run +++ b/client/cmd/simnet-trade-tests/run @@ -2,8 +2,8 @@ set -e go build -tags harness -ldflags \ - "-X 'decred.org/dcrdex/dex.testLockTimeTaker=1m' \ - -X 'decred.org/dcrdex/dex.testLockTimeMaker=2m'" + "-X 'decred.org/dcrdex/dex.testLockTimeTaker=3m' \ + -X 'decred.org/dcrdex/dex.testLockTimeMaker=6m'" case $1 in diff --git a/client/core/core.go b/client/core/core.go index f3572b99d2..d44666082b 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -5077,7 +5077,7 @@ func (c *Core) MaxBuy(host string, baseID, quoteID uint32, rate uint64) (*MaxOrd } preRedeem, err := baseWallet.PreRedeem(&asset.PreRedeemForm{ - Version: baseAsset.Version, + AssetVersion: baseAsset.Version, Lots: maxBuy.Lots, FeeSuggestion: redeemFeeSuggestion, }) @@ -5136,7 +5136,7 @@ func (c *Core) MaxSell(host string, base, quote uint32) (*MaxOrderEstimate, erro } preRedeem, err := quoteWallet.PreRedeem(&asset.PreRedeemForm{ - Version: quoteAsset.Version, + AssetVersion: quoteAsset.Version, Lots: maxSell.Lots, FeeSuggestion: redeemFeeSuggestion, }) @@ -5722,7 +5722,7 @@ func (c *Core) PreOrder(form *TradeForm) (*OrderEstimate, error) { } swapEstimate, err := wallets.fromWallet.PreSwap(&asset.PreSwapForm{ - Version: assetConfigs.fromAsset.Version, + AssetVersion: assetConfigs.fromAsset.Version, LotSize: swapLotSize, Lots: lots, MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, @@ -5737,7 +5737,7 @@ func (c *Core) PreOrder(form *TradeForm) (*OrderEstimate, error) { } redeemEstimate, err := wallets.toWallet.PreRedeem(&asset.PreRedeemForm{ - Version: assetConfigs.toAsset.Version, + AssetVersion: assetConfigs.toAsset.Version, Lots: lots, FeeSuggestion: redeemFeeSuggestion, SelectedOptions: form.Options, @@ -6235,7 +6235,7 @@ func (c *Core) prepareTradeRequest(pw []byte, form *TradeForm) (*tradeRequest, e } coins, redeemScripts, fundingFees, err := fromWallet.FundOrder(&asset.Order{ - Version: assetConfigs.fromAsset.Version, + AssetVersion: assetConfigs.fromAsset.Version, Value: fundQty, MaxSwapCount: lots, MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, @@ -6325,7 +6325,7 @@ func (c *Core) prepareMultiTradeRequests(pw []byte, form *MultiTradeForm) ([]*tr } allCoins, allRedeemScripts, fundingFees, err := fromWallet.FundMultiOrder(&asset.MultiOrder{ - Version: assetConfigs.fromAsset.Version, + AssetVersion: assetConfigs.fromAsset.Version, Values: orderValues, MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, FeeSuggestion: c.feeSuggestion(dc, assetConfigs.fromAsset.ID), diff --git a/client/core/core_test.go b/client/core/core_test.go index 5bdacecb94..a32593ece5 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -715,6 +715,7 @@ type TXCWallet struct { reserves atomic.Uint64 findBond *asset.BondDetails findBondErr error + maxSwaps, maxRedeems int confirmRedemptionResult *asset.ConfirmRedemptionStatus confirmRedemptionErr error @@ -1122,6 +1123,15 @@ func (w *TXCWallet) WalletTransaction(context.Context, dex.Bytes) (*asset.Wallet return nil, nil } +var _ asset.MaxMatchesCounter = (*TXCWallet)(nil) + +func (w *TXCWallet) MaxSwaps(serverVer uint32, feeRate uint64) (int, error) { + return w.maxSwaps, nil +} +func (w *TXCWallet) MaxRedeems(serverVer uint32) (int, error) { + return w.maxRedeems, nil +} + type TAccountLocker struct { *TXCWallet reserveNRedemptions uint64 @@ -9150,8 +9160,8 @@ func TestMaxSwapsRedeemsInTx(t *testing.T) { tCore.wallets[tUTXOAssetB.ID] = btcWallet walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) - tDcrWallet.info.MaxSwapsInTx = 4 - tBtcWallet.info.MaxRedeemsInTx = 4 + tDcrWallet.maxSwaps = 4 + tDcrWallet.maxRedeems = 4 lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, 0, 0) oid := lo.ID() @@ -9215,7 +9225,7 @@ func TestMaxSwapsRedeemsInTx(t *testing.T) { t.Helper() for i := range expected { if expected[i] != len(wallet.lastRedeems[i].Redemptions) { - t.Fatalf("expected %d swaps but got %d", expected[i], len(wallet.lastRedeems[i].Redemptions)) + t.Fatalf("expected %d redeems but got %d", expected[i], len(wallet.lastRedeems[i].Redemptions)) } } } diff --git a/client/core/simnet_trade.go b/client/core/simnet_trade.go index 2e94f27341..a4015973b0 100644 --- a/client/core/simnet_trade.go +++ b/client/core/simnet_trade.go @@ -2130,7 +2130,7 @@ func (s *simulationTest) assertBalanceChanges(client *simulationClient, isRefund if isRefund { // NOTE: Gas price may be higher if the eth harness has // had a lot of use. The minimum is the gas tip cap. - ethRefundFees := int64(dexeth.RefundGas(0 /*version*/)) * dexeth.MinGasTipCap + ethRefundFees := int64(dexeth.RefundGas(1 /*version*/)) * dexeth.MinGasTipCap msgTx := wire.NewMsgTx(0) prevOut := wire.NewOutPoint(&chainhash.Hash{}, 0) diff --git a/client/core/trade.go b/client/core/trade.go index e69489b8d0..15bd2a09af 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -2284,7 +2284,7 @@ func (t *trackedTrade) revokeMatch(matchID order.MatchID, fromServer bool) error // // This method modifies match fields and MUST be called with the trackedTrade // mutex lock held for writes. -func (c *Core) swapMatches(t *trackedTrade, matches []*matchTracker) error { +func (c *Core) swapMatches(t *trackedTrade, matches []*matchTracker) (err error) { errs := newErrorSet("swapMatches order %s - ", t.ID()) groupables := make([]*matchTracker, 0, len(matches)) // Over-allocating if there are suspect matches var suspects []*matchTracker @@ -2295,36 +2295,75 @@ func (c *Core) swapMatches(t *trackedTrade, matches []*matchTracker) error { groupables = append(groupables, m) } } + feeRate := t.bestSwapGroupFeeRate(matches) if len(groupables) > 0 { - maxSwapsInTx := int(t.wallets.fromWallet.Info().MaxSwapsInTx) + var maxSwapsInTx int + if counter, is := t.wallets.fromWallet.Wallet.(asset.MaxMatchesCounter); is { + if maxSwapsInTx, err = counter.MaxSwaps(t.metaData.FromVersion, feeRate); err != nil { + t.dc.log.Meter("failed_swap_count", time.Minute*10).Warn("Failed to count swap txs: %v", err) + } + } if maxSwapsInTx <= 0 || len(groupables) < maxSwapsInTx { - c.swapMatchGroup(t, groupables, errs) + c.swapMatchGroup(t, groupables, feeRate, errs) } else { for i := 0; i < len(groupables); i += maxSwapsInTx { if i+maxSwapsInTx < len(groupables) { - c.swapMatchGroup(t, groupables[i:i+maxSwapsInTx], errs) + c.swapMatchGroup(t, groupables[i:i+maxSwapsInTx], feeRate, errs) } else { - c.swapMatchGroup(t, groupables[i:], errs) + c.swapMatchGroup(t, groupables[i:], feeRate, errs) } } } } for _, m := range suspects { - c.swapMatchGroup(t, []*matchTracker{m}, errs) + c.swapMatchGroup(t, []*matchTracker{m}, feeRate, errs) } return errs.ifAny() } +// bestSwapGroupRate gets the most appropriate fee rate for a group of swaps. +func (t *trackedTrade) bestSwapGroupFeeRate(matches []*matchTracker) uint64 { + var highestFeeRate uint64 + for _, match := range matches { + if match.FeeRateSwap > highestFeeRate { + highestFeeRate = match.FeeRateSwap + } + } + // Use a higher swap fee rate if a local estimate is higher than the + // prescribed rate, but not higher than the funded (max) rate. + if highestFeeRate < t.metaData.MaxFeeRate { + freshRate := t.wallets.fromWallet.feeRate() + if freshRate == 0 { // either not a FeeRater, or FeeRate failed + freshRate = t.dc.bestBookFeeSuggestion(t.wallets.fromWallet.AssetID) + } + if freshRate > t.metaData.MaxFeeRate { + freshRate = t.metaData.MaxFeeRate + } + if highestFeeRate < freshRate { + t.dc.log.Infof("Prescribed %v fee rate %v looks low, using %v", + t.wallets.fromWallet.Symbol, highestFeeRate, freshRate) + highestFeeRate = freshRate + } + } + return highestFeeRate +} + // swapMatchGroup will send a transaction with swap outputs for the specified // matches. // // This method modifies match fields and MUST be called with the trackedTrade // mutex lock held for writes. -func (c *Core) swapMatchGroup(t *trackedTrade, matches []*matchTracker, errs *errorSet) { +func (c *Core) swapMatchGroup(t *trackedTrade, matches []*matchTracker, highestFeeRate uint64, errs *errorSet) { + // Ensure swap is not sent with a zero fee rate. + if highestFeeRate == 0 { + errs.add("swap cannot proceed with a zero fee rate") + return + } + // Prepare the asset.Contracts. contracts := make([]*asset.Contract, len(matches)) // These matches may have different fee rates, matched in different epochs. - var highestFeeRate uint64 + for i, match := range matches { value := match.Quantity if !match.trade.Sell { @@ -2345,10 +2384,6 @@ func (c *Core) swapMatchGroup(t *trackedTrade, matches []*matchTracker, errs *er SecretHash: match.MetaData.Proof.SecretHash, LockTime: uint64(lockTime), } - - if match.FeeRateSwap > highestFeeRate { - highestFeeRate = match.FeeRateSwap - } } lockChange := true @@ -2396,41 +2431,18 @@ func (c *Core) swapMatchGroup(t *trackedTrade, matches []*matchTracker, errs *er return } - // Use a higher swap fee rate if a local estimate is higher than the - // prescribed rate, but not higher than the funded (max) rate. - if highestFeeRate < t.metaData.MaxFeeRate { - freshRate := fromWallet.feeRate() - if freshRate == 0 { // either not a FeeRater, or FeeRate failed - freshRate = t.dc.bestBookFeeSuggestion(fromWallet.AssetID) - } - if freshRate > t.metaData.MaxFeeRate { - freshRate = t.metaData.MaxFeeRate - } - if highestFeeRate < freshRate { - c.log.Infof("Prescribed %v fee rate %v looks low, using %v", - fromWallet.Symbol, highestFeeRate, freshRate) - highestFeeRate = freshRate - } - } - - // Ensure swap is not sent with a zero fee rate. - if highestFeeRate == 0 { - errs.add("swap cannot proceed with a zero fee rate") - return - } - // swapMatches is no longer idempotent after this point. // Send the swap. If the swap fails, set the swapErr flag for all matches. // A more sophisticated solution might involve tracking the error time too // and trying again in certain circumstances. swaps := &asset.Swaps{ - Version: t.metaData.FromVersion, - Inputs: inputs, - Contracts: contracts, - FeeRate: highestFeeRate, - LockChange: lockChange, - Options: t.options, + AssetVersion: t.metaData.FromVersion, + Inputs: inputs, + Contracts: contracts, + FeeRate: highestFeeRate, + LockChange: lockChange, + Options: t.options, } receipts, change, fees, err := fromWallet.Swap(swaps) if err != nil { @@ -2659,7 +2671,7 @@ func (c *Core) sendInitAsync(t *trackedTrade, match *matchTracker, coinID, contr // // This method modifies match fields and MUST be called with the trackedTrade // mutex lock held for writes. -func (c *Core) redeemMatches(t *trackedTrade, matches []*matchTracker) error { +func (c *Core) redeemMatches(t *trackedTrade, matches []*matchTracker) (err error) { errs := newErrorSet("redeemMatches order %s - ", t.ID()) groupables := make([]*matchTracker, 0, len(matches)) // Over-allocating if there are suspect matches var suspects []*matchTracker @@ -2674,7 +2686,12 @@ func (c *Core) redeemMatches(t *trackedTrade, matches []*matchTracker) error { if !t.wallets.toWallet.connected() { return errWalletNotConnected // don't ungroup, just return } - maxRedeemsInTx := int(t.wallets.toWallet.Info().MaxRedeemsInTx) + var maxRedeemsInTx int + if counter, is := t.wallets.fromWallet.Wallet.(asset.MaxMatchesCounter); is { + if maxRedeemsInTx, err = counter.MaxRedeems(t.metaData.FromVersion); err != nil { + t.dc.log.Meter("failed_redeem_count", time.Minute*10).Warn("Failed to count redeem txs: %v", err) + } + } if maxRedeemsInTx <= 0 || len(groupables) < maxRedeemsInTx { c.redeemMatchGroup(t, groupables, errs) } else { diff --git a/client/webserver/site/src/css/main.scss b/client/webserver/site/src/css/main.scss index 60db707897..224193a193 100644 --- a/client/webserver/site/src/css/main.scss +++ b/client/webserver/site/src/css/main.scss @@ -12,14 +12,11 @@ body { bottom: 0; left: 0; right: 0; + display: flex; flex-direction: column; justify-content: flex-start; background-color: var(--body-bg); color: var(--text-color); - - &.loaded { - display: flex; - } } header#header { diff --git a/client/webserver/site/src/html/bodybuilder.tmpl b/client/webserver/site/src/html/bodybuilder.tmpl index 4c65135dc6..690e3168f7 100644 --- a/client/webserver/site/src/html/bodybuilder.tmpl +++ b/client/webserver/site/src/html/bodybuilder.tmpl @@ -9,14 +9,9 @@