From 3fe9316c8a56cba409b2cce81270096bf433248b Mon Sep 17 00:00:00 2001 From: buck54321 Date: Wed, 6 Sep 2023 14:10:21 -0700 Subject: [PATCH] finish polygon (#2431) * Finish Polygon Generalize the ethereum wallet. Add tokens. Implement rpc tests and getgas. Server backend. Deploy usdc, Some frontend. --------- Co-authored-by: Jonathan Chappelow --- .gitignore | 1 + client/app/importlgpl.go | 3 + client/asset/eth/chaincfg.go | 142 ++++ client/asset/eth/cmd/deploy/main.go | 194 ++++++ client/asset/eth/cmd/getgas/README.md | 17 +- client/asset/eth/cmd/getgas/main.go | 108 ++- client/asset/eth/contractor.go | 24 +- client/asset/eth/deploy.go | 319 +++++++++ client/asset/eth/eth.go | 626 +++++++++++------- client/asset/eth/eth_test.go | 71 +- client/asset/eth/genesis_config.go | 65 -- client/asset/eth/multirpc.go | 218 +++--- client/asset/eth/multirpc_live_test.go | 345 +--------- client/asset/eth/multirpc_test_util.go | 363 ++++++++++ client/asset/eth/node.go | 2 +- client/asset/eth/nodeclient.go | 38 +- client/asset/eth/nodeclient_harness_test.go | 215 +++--- client/asset/interface.go | 4 + client/asset/polygon/chaincfg.go | 120 ++++ client/asset/polygon/multirpc_live_test.go | 126 ++++ client/asset/polygon/polygon.go | 88 ++- client/cmd/assetseed/main.go | 6 + client/cmd/simnet-trade-tests/main.go | 2 + client/cmd/simnet-trade-tests/run | 12 +- client/core/core.go | 23 +- client/core/helpers.go | 18 +- client/core/simnet_trade.go | 111 +++- client/webserver/live_test.go | 2 +- client/webserver/locales/en-us.go | 1 + client/webserver/site/src/css/main.scss | 9 +- client/webserver/site/src/css/market.scss | 4 +- client/webserver/site/src/css/wallets.scss | 30 +- .../webserver/site/src/css/wallets_dark.scss | 9 + .../webserver/site/src/html/bodybuilder.tmpl | 4 +- client/webserver/site/src/html/forms.tmpl | 14 +- client/webserver/site/src/html/markets.tmpl | 26 +- client/webserver/site/src/html/order.tmpl | 4 +- client/webserver/site/src/html/wallets.tmpl | 27 +- .../img/coins/{dextt.eth.png => dextt.png} | Bin .../src/img/coins/{matic.png => polygon.png} | Bin .../src/img/coins/{usdc.eth.png => usdc.png} | Bin client/webserver/site/src/js/doc.ts | 38 +- client/webserver/site/src/js/forms.ts | 56 +- client/webserver/site/src/js/markets.ts | 77 +-- client/webserver/site/src/js/mm.ts | 55 +- client/webserver/site/src/js/order.ts | 35 +- client/webserver/site/src/js/orders.ts | 39 +- client/webserver/site/src/js/wallets.ts | 61 +- client/webserver/template.go | 7 + dex/bip-id.go | 26 +- dex/networks/erc20/contracts/TestToken.sol | 8 +- .../erc20/contracts/updatecontract.sh | 16 +- dex/networks/eth/contracts/updatecontract.sh | 9 +- dex/networks/eth/tokens.go | 42 +- dex/networks/polygon/params.go | 176 ++++- dex/testing/dcrdex/harness.sh | 55 +- dex/testing/eth/harness.sh | 16 +- dex/testing/loadbot/loadbot.go | 2 + dex/testing/polygon/harness.sh | 206 ++++-- server/asset/eth/coiner.go | 2 +- server/asset/eth/eth.go | 200 +++--- server/asset/eth/eth_test.go | 12 +- server/asset/eth/rpcclient.go | 34 +- server/asset/eth/rpcclient_harness_test.go | 4 +- server/asset/eth/tokener.go | 24 +- server/asset/polygon/polygon.go | 79 +++ server/cmd/dcrdex/importlgpl.go | 5 +- server/market/routers_test.go | 10 +- 68 files changed, 3275 insertions(+), 1410 deletions(-) create mode 100644 client/asset/eth/chaincfg.go create mode 100644 client/asset/eth/cmd/deploy/main.go create mode 100644 client/asset/eth/deploy.go delete mode 100644 client/asset/eth/genesis_config.go create mode 100644 client/asset/eth/multirpc_test_util.go create mode 100644 client/asset/polygon/chaincfg.go create mode 100644 client/asset/polygon/multirpc_live_test.go rename client/webserver/site/src/img/coins/{dextt.eth.png => dextt.png} (100%) rename client/webserver/site/src/img/coins/{matic.png => polygon.png} (100%) rename client/webserver/site/src/img/coins/{usdc.eth.png => usdc.png} (100%) create mode 100644 server/asset/polygon/polygon.go diff --git a/.gitignore b/.gitignore index 48ddbe1eb6..47b35066cf 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ client/asset/btc/electrum/example/server/server client/asset/btc/electrum/example/wallet/wallet **/testdata/fuzz client/asset/eth/cmd/getgas/getgas +client/asset/eth/cmd/deploy/deploy client/cmd/dexc-desktop/pkg/installers diff --git a/client/app/importlgpl.go b/client/app/importlgpl.go index d38377e6ee..dec6986899 100644 --- a/client/app/importlgpl.go +++ b/client/app/importlgpl.go @@ -13,8 +13,11 @@ import ( _ "decred.org/dcrdex/client/asset/eth" // register eth asset _ "decred.org/dcrdex/client/asset/polygon" // register polygon network dexeth "decred.org/dcrdex/dex/networks/eth" + dexpolygon "decred.org/dcrdex/dex/networks/polygon" ) func init() { dexeth.MaybeReadSimnetAddrs() + dexpolygon.MaybeReadSimnetAddrs() + } diff --git a/client/asset/eth/chaincfg.go b/client/asset/eth/chaincfg.go new file mode 100644 index 0000000000..4e9d9ab29d --- /dev/null +++ b/client/asset/eth/chaincfg.go @@ -0,0 +1,142 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package eth + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "decred.org/dcrdex/dex" + dexeth "decred.org/dcrdex/dex/networks/eth" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + ethcore "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/params" +) + +// CompatibilityData is some addresses and hashes used for validating an RPC +// APIs compatibility with trading. +type CompatibilityData struct { + Addr common.Address + TokenAddr common.Address + TxHash common.Hash + BlockHash common.Hash +} + +var ( + mainnetCompatibilityData = CompatibilityData{ + // Vitalik's address from https://twitter.com/VitalikButerin/status/1050126908589887488 + Addr: common.HexToAddress("0xab5801a7d398351b8be11c439e05c5b3259aec9b"), + TokenAddr: common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), + TxHash: common.HexToHash("0xea1a717af9fad5702f189d6f760bb9a5d6861b4ee915976fe7732c0c95cd8a0e"), + BlockHash: common.HexToHash("0x44ebd6f66b4fd546bccdd700869f6a433ef9a47e296a594fa474228f86eeb353"), + } + + testnetCompatibilityData = CompatibilityData{ + Addr: common.HexToAddress("0x8879F72728C5eaf5fB3C55e6C3245e97601FBa32"), + TokenAddr: common.HexToAddress("0x07865c6E87B9F70255377e024ace6630C1Eaa37F"), + TxHash: common.HexToHash("0x4e1d455f7eac7e3a5f7c1e0989b637002755eaee3a262f90b0f3aef1f1c4dcf0"), + BlockHash: common.HexToHash("0x8896021c2666303a85b7e4a6a6f2b075bc705d4e793bf374cd44b83bca23ef9a"), + } +) + +// NetworkCompatibilityData returns the CompatibilityData for the specified +// network. If using simnet, make sure the simnet harness is running. +func NetworkCompatibilityData(net dex.Network) (c CompatibilityData, err error) { + switch net { + case dex.Mainnet: + return mainnetCompatibilityData, nil + case dex.Testnet: + return testnetCompatibilityData, nil + case dex.Simnet: + default: + return c, fmt.Errorf("No compatibility data for network # %d", net) + } + // simnet + tDir, err := simnetDataDir() + if err != nil { + return + } + + addr := common.HexToAddress("18d65fb8d60c1199bb1ad381be47aa692b482605") + var ( + tTxHashFile = filepath.Join(tDir, "test_tx_hash.txt") + tBlockHashFile = filepath.Join(tDir, "test_block10_hash.txt") + tContractFile = filepath.Join(tDir, "test_token_contract_address.txt") + ) + readIt := func(path string) string { + b, err := os.ReadFile(path) + if err != nil { + panic(fmt.Sprintf("Problem reading simnet testing file %q: %v", path, err)) + } + return strings.TrimSpace(string(b)) // mainly the trailing "\r\n" + } + return CompatibilityData{ + Addr: addr, + TokenAddr: common.HexToAddress(readIt(tContractFile)), + TxHash: common.HexToHash(readIt(tTxHashFile)), + BlockHash: common.HexToHash(readIt(tBlockHashFile)), + }, nil +} + +// simnetDataDir returns the data directory for Ethereum simnet. +func simnetDataDir() (string, error) { + u, err := user.Current() + if err != nil { + return "", fmt.Errorf("error getting current user: %w", err) + } + + return filepath.Join(u.HomeDir, "dextest", "eth"), nil +} + +// ETHConfig returns the ETH protocol configuration for the specified network. +func ETHConfig(net dex.Network) (c ethconfig.Config, err error) { + c = ethconfig.Defaults + switch net { + // Ethereum + case dex.Testnet: + c.Genesis = core.DefaultGoerliGenesisBlock() + case dex.Mainnet: + c.Genesis = core.DefaultGenesisBlock() + case dex.Simnet: + c.Genesis, err = readSimnetGenesisFile() + if err != nil { + return c, fmt.Errorf("readSimnetGenesisFile error: %w", err) + } + default: + return c, fmt.Errorf("unknown network %d", net) + + } + c.NetworkId = c.Genesis.Config.ChainID.Uint64() + return +} + +// ChainConfig returns the core configuration for the blockchain. +func ChainConfig(net dex.Network) (c *params.ChainConfig, err error) { + cfg, err := ETHConfig(net) + if err != nil { + return nil, err + } + return cfg.Genesis.Config, nil +} + +// readSimnetGenesisFile reads the simnet genesis file. +func readSimnetGenesisFile() (*ethcore.Genesis, error) { + dataDir, err := simnetDataDir() + if err != nil { + return nil, err + } + + genesisFile := filepath.Join(dataDir, "genesis.json") + genesisCfg, err := dexeth.LoadGenesisFile(genesisFile) + if err != nil { + return nil, fmt.Errorf("error reading genesis file: %v", err) + } + + return genesisCfg, nil +} diff --git a/client/asset/eth/cmd/deploy/main.go b/client/asset/eth/cmd/deploy/main.go new file mode 100644 index 0000000000..84cf1270e9 --- /dev/null +++ b/client/asset/eth/cmd/deploy/main.go @@ -0,0 +1,194 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package main + +/* + deploy is a utility for deploying swap contracts. Examples of use: + + 1) Estimate the funding required to deploy a base asset swap contract to + Ethereum + ./deploy --mainnet --chain eth --fundingrequired + This will use the current prevailing fee rate to estimate the fee + requirements for the deployment transaction. The estimate is only + accurate if there are already enough funds in the wallet (so estimateGas + can be used), otherwise, a generously-padded constant is used to estimate + the gas requirements. + + 2) Deploy a base asset swap contract to Ethereum. + ./deploy --mainnet --chain eth + + 3) Deploy a token swap contract to Polygon. + ./deploy --mainnet --chain polygon --tokenaddr 0x2791bca1f2de4661ed88a30c99a7a9449aa84174 + + 4) Return remaining Goerli testnet ETH balance to specified address. + ./deploy --testnet --chain eth --returnaddr 0x18d65fb8d60c1199bb1ad381be47aa692b482605 + + IMPORTANT: deploy uses the same wallet configuration as getgas. See getgas + README for instructions. + + 5) Test reading of the Polygon credentials file. + ./deploy --chain polygon --mainnet --readcreds +*/ + +import ( + "context" + "flag" + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "decred.org/dcrdex/client/asset" + "decred.org/dcrdex/client/asset/eth" + "decred.org/dcrdex/client/asset/polygon" + "decred.org/dcrdex/dex" + dexeth "decred.org/dcrdex/dex/networks/eth" + dexpolygon "decred.org/dcrdex/dex/networks/polygon" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +func main() { + if err := mainErr(); err != nil { + fmt.Fprint(os.Stderr, err, "\n") + os.Exit(1) + } + os.Exit(0) +} + +func mainErr() error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + u, err := user.Current() + if err != nil { + return fmt.Errorf("could not get the current user: %w", err) + } + defaultCredentialsPath := filepath.Join(u.HomeDir, "dextest", "credentials.json") + + var contractVerI int + var chain, credentialsPath, tokenAddress, returnAddr string + var useTestnet, useMainnet, useSimnet, trace, debug, readCreds, fundingReq bool + flag.BoolVar(&readCreds, "readcreds", false, "does not run gas estimates. read the credentials file and print the address") + flag.BoolVar(&fundingReq, "fundingrequired", false, "does not run gas estimates. calculate the funding required by the wallet to get estimates") + flag.StringVar(&returnAddr, "return", "", "does not run gas estimates. return ethereum funds to supplied address") + 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.StringVar(&chain, "chain", "eth", "symbol of the base chain") + flag.StringVar(&tokenAddress, "tokenaddr", "", "launches an erc20-linked contract with this token. default launches a base chain contract") + flag.IntVar(&contractVerI, "ver", 0, "contract version") + flag.StringVar(&credentialsPath, "creds", defaultCredentialsPath, "path for JSON credentials file.") + flag.Parse() + + if !useMainnet && !useTestnet && !useSimnet { + return fmt.Errorf("no network specified. add flag --mainnet, --testnet, or --simnet") + } + if (useMainnet && useTestnet) || (useMainnet && useSimnet) || (useTestnet && useSimnet) { + return fmt.Errorf("more than one network specified") + } + + net := dex.Mainnet + if useSimnet { + net = dex.Simnet + dexeth.MaybeReadSimnetAddrs() + dexpolygon.MaybeReadSimnetAddrs() + } + if useTestnet { + net = dex.Testnet + } + + if readCreds { + addr, providers, err := eth.GetGas.ReadCredentials(chain, credentialsPath, net) + if err != nil { + return err + } + fmt.Println("Credentials successfully parsed") + fmt.Println("Address:", addr) + fmt.Println("Providers:", strings.Join(providers, ", ")) + return nil + } + + assetID, found := dex.BipSymbolID(chain) + if !found { + return fmt.Errorf("asset %s not known", chain) + } + + if tkn := asset.TokenInfo(assetID); tkn != nil { + return fmt.Errorf("specified chain is not a base chain. appears to be token %s", tkn.Name) + } + + contractVer := uint32(contractVerI) + + logLvl := dex.LevelInfo + if debug { + logLvl = dex.LevelDebug + } + if trace { + logLvl = dex.LevelTrace + } + + var tokenAddr common.Address + if tokenAddress != "" { + if !common.IsHexAddress(tokenAddress) { + return fmt.Errorf("token address %q does not appear to be valid", tokenAddress) + } + tokenAddr = common.HexToAddress(tokenAddress) + } + + log := dex.StdOutLogger("DEPLOY", logLvl) + + var bui *dex.UnitInfo + var chainCfg *params.ChainConfig + switch chain { + case "eth": + bui = &dexeth.UnitInfo + chainCfg, err = eth.ChainConfig(net) + if err != nil { + return fmt.Errorf("error finding chain config: %v", err) + } + case "polygon": + bui = &dexpolygon.UnitInfo + chainCfg, err = polygon.ChainConfig(net) + if err != nil { + return fmt.Errorf("error finding chain config: %v", err) + } + } + + switch { + case fundingReq: + return eth.ContractDeployer.EstimateDeployFunding( + ctx, + chain, + contractVer, + tokenAddr, + credentialsPath, + chainCfg, + bui, + log, + net, + ) + case returnAddr != "": + if !common.IsHexAddress(returnAddr) { + return fmt.Errorf("return address %q is not valid", returnAddr) + } + addr := common.HexToAddress(returnAddr) + return eth.ContractDeployer.ReturnETH(ctx, chain, addr, credentialsPath, chainCfg, bui, log, net) + default: + return eth.ContractDeployer.DeployContract( + ctx, + chain, + contractVer, + tokenAddr, + credentialsPath, + chainCfg, + bui, + log, + net, + ) + } +} diff --git a/client/asset/eth/cmd/getgas/README.md b/client/asset/eth/cmd/getgas/README.md index 0f979a90c0..3be8c1c7b6 100644 --- a/client/asset/eth/cmd/getgas/README.md +++ b/client/asset/eth/cmd/getgas/README.md @@ -8,19 +8,28 @@ If this is a new asset, you must populate either `dexeth.VersionedGases` or `dexeth.Tokens` with generous estimates before running `getgas`. ### Use -- Create a credentials file. `getgas` will look in `~/ethtest/getgas-credentials.json`. You can override that location with the `--creds` CLI argument. The credentials file should have the JSON format in the example below. The seed can be anything. +- Create a credentials file. `getgas` will look for `~/dextest/credentials.json`. +You can override that location with the `--creds` CLI argument. +The credentials file should have the JSON format in the example below. The seed can be anything. **example credentials file** ``` { "seed": "<32-bytes hex>", "providers": { - "simnet": "http://localhost:38556", - "testnet": "https://goerli.infura.io/v3/", - "mainnet": "https://mainnet.infura.io/v3/" + "eth": { + "testnet: [ + "https://goerli.infura.io/v3/" + ], + "mainnet": [ + "https://mainnet.infura.io/v3/" + ] + }, + "polygon": { ... } } } ``` +- Select the blockchain with `--chain`. The default is `--chain eth`, but `--chain polygon` can be selected as well. - Use the `--readcreds` utility to check the validity of the credentials file and to print the address. e.g. `./getgas --readcreds --mainnet`. diff --git a/client/asset/eth/cmd/getgas/main.go b/client/asset/eth/cmd/getgas/main.go index ffa910a171..d02f3bb64e 100644 --- a/client/asset/eth/cmd/getgas/main.go +++ b/client/asset/eth/cmd/getgas/main.go @@ -10,9 +10,15 @@ import ( "os" "os/user" "path/filepath" + "strings" "decred.org/dcrdex/client/asset/eth" + "decred.org/dcrdex/client/asset/polygon" "decred.org/dcrdex/dex" + dexeth "decred.org/dcrdex/dex/networks/eth" + dexpolygon "decred.org/dcrdex/dex/networks/polygon" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" ) func main() { @@ -31,10 +37,10 @@ func mainErr() error { if err != nil { return fmt.Errorf("could not get the current user: %w", err) } - defaultCredsPath := filepath.Join(u.HomeDir, "ethtest", "getgas-credentials.json") + defaultCredentialsPath := filepath.Join(u.HomeDir, "dextest", "credentials.json") var maxSwaps, contractVerI int - var token, credentialsPath, returnAddr string + var chain, token, credentialsPath, returnAddr string var useTestnet, useMainnet, useSimnet, trace, debug, readCreds, fundingReq bool flag.BoolVar(&readCreds, "readcreds", false, "does not run gas estimates. read the credentials file and print the address") flag.BoolVar(&fundingReq, "fundingrequired", false, "does not run gas estimates. calculate the funding required by the wallet to get estimates") @@ -45,9 +51,10 @@ func mainErr() error { flag.BoolVar(&trace, "trace", false, "use simnet") flag.BoolVar(&debug, "debug", false, "use simnet") 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(&token, "token", "eth", "symbol of the token. if token is not specified, will check gas for Ethereum") + 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") flag.IntVar(&contractVerI, "ver", 0, "contract version") - flag.StringVar(&credentialsPath, "creds", defaultCredsPath, "path for JSON credentials file") + flag.StringVar(&credentialsPath, "creds", defaultCredentialsPath, "path for JSON credentials file.") flag.Parse() if !useMainnet && !useTestnet && !useSimnet { @@ -60,25 +67,37 @@ func mainErr() error { net := dex.Mainnet if useSimnet { net = dex.Simnet + dexeth.MaybeReadSimnetAddrs() + dexpolygon.MaybeReadSimnetAddrs() } if useTestnet { net = dex.Testnet } if readCreds { - addr, provider, err := eth.GetGas.ReadCredentials(credentialsPath, net) + addr, providers, err := eth.GetGas.ReadCredentials(chain, credentialsPath, net) if err != nil { return err } fmt.Println("Credentials successfully parsed") fmt.Println("Address:", addr) - fmt.Println("Provider:", provider) + fmt.Println("Providers:", strings.Join(providers, ", ")) return nil } if maxSwaps < 2 { return fmt.Errorf("n cannot be < 2") } + + if token == "" { + token = chain + } + + // Allow specification of tokens without .[chain] suffix. + if chain != token && !strings.Contains(token, ".") { + token = token + "." + chain + } + assetID, found := dex.BipSymbolID(token) if !found { return fmt.Errorf("asset %s not known", token) @@ -95,12 +114,83 @@ func mainErr() error { log := dex.StdOutLogger("GG", logLvl) + walletParams := func( + gases map[uint32]*dexeth.Gases, + contracts map[uint32]map[dex.Network]common.Address, + tokens map[uint32]*dexeth.Token, + compatLookup func(net dex.Network) (c eth.CompatibilityData, err error), + chainCfg func(net dex.Network) (c *params.ChainConfig, err error), + bui *dex.UnitInfo, + ) (*eth.GetGasWalletParams, error) { + + wParams := new(eth.GetGasWalletParams) + wParams.BaseUnitInfo = bui + if token != chain { + var exists bool + tkn, exists := tokens[assetID] + if !exists { + return nil, fmt.Errorf("specified token %s does not exist on base chain %s", token, chain) + } + wParams.Token = tkn + wParams.UnitInfo = &tkn.UnitInfo + netToken, exists := tkn.NetTokens[net] + if !exists { + return nil, fmt.Errorf("no %s token on %s network %s", tkn.Name, chain, net) + } + 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) + } + 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) + } + wParams.Gas = g + cs, exists := contracts[contractVer] + if !exists { + return nil, fmt.Errorf("no version %d base chain swap contract on %s", contractVer, chain) + } + wParams.ContractAddr, exists = cs[net] + if !exists { + return nil, fmt.Errorf("no version %d base chain swap contract on %s network %s", contractVer, chain, net) + } + } + wParams.ChainCfg, err = chainCfg(net) + if err != nil { + return nil, fmt.Errorf("error finding chain config: %v", err) + } + compat, err := compatLookup(net) + if err != nil { + return nil, fmt.Errorf("error finding api compatibility data: %v", err) + } + wParams.Compat = &compat + return wParams, nil + } + + var wParams *eth.GetGasWalletParams + switch chain { + case "eth": + wParams, err = walletParams(dexeth.VersionedGases, dexeth.ContractAddresses, dexeth.Tokens, + eth.NetworkCompatibilityData, eth.ChainConfig, &dexeth.UnitInfo) + case "polygon": + wParams, err = walletParams(dexpolygon.VersionedGases, dexpolygon.ContractAddresses, dexpolygon.Tokens, + polygon.NetworkCompatibilityData, polygon.ChainConfig, &dexpolygon.UnitInfo) + default: + return fmt.Errorf("chain %s not known", chain) + } + if err != nil { + return fmt.Errorf("error generating wallet params: %w", err) + } + switch { case fundingReq: - return eth.GetGas.EstimateFunding(ctx, net, assetID, contractVer, maxSwaps, credentialsPath, log) + return eth.GetGas.EstimateFunding(ctx, net, assetID, contractVer, maxSwaps, credentialsPath, wParams, log) case returnAddr != "": - return eth.GetGas.ReturnETH(ctx, credentialsPath, returnAddr, net, log) + return eth.GetGas.Return(ctx, assetID, credentialsPath, returnAddr, wParams, net, log) default: - return eth.GetGas.Estimate(ctx, net, assetID, contractVer, maxSwaps, credentialsPath, log) + return eth.GetGas.Estimate(ctx, net, assetID, contractVer, maxSwaps, credentialsPath, wParams, log) } } diff --git a/client/asset/eth/contractor.go b/client/asset/eth/contractor.go index fcd87540c3..f00a0ea31f 100644 --- a/client/asset/eth/contractor.go +++ b/client/asset/eth/contractor.go @@ -57,8 +57,8 @@ type tokenContractor interface { estimateTransferGas(context.Context, *big.Int) (uint64, error) } -type contractorConstructor func(net dex.Network, addr common.Address, ec bind.ContractBackend) (contractor, error) -type tokenContractorConstructor func(net dex.Network, assetID uint32, acctAddr common.Address, ec bind.ContractBackend) (tokenContractor, 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) // contractV0 is the interface common to a version 0 swap contract or version 0 // token swap contract. @@ -88,11 +88,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(net dex.Network, acctAddr common.Address, cb bind.ContractBackend) (contractor, error) { - contractAddr, exists := dexeth.ContractAddresses[0][net] - if !exists || contractAddr == (common.Address{}) { - return nil, fmt.Errorf("no contract address for version 0, net %s", net) - } +func newV0Contractor(contractAddr, acctAddr common.Address, cb bind.ContractBackend) (contractor, error) { c, err := swapv0.NewETHSwap(contractAddr, cb) if err != nil { return nil, err @@ -332,11 +328,17 @@ var _ contractor = (*tokenContractorV0)(nil) var _ tokenContractor = (*tokenContractorV0)(nil) // newV0TokenContractor is a contractor for version 0 erc20 token swap contract. -func newV0TokenContractor(net dex.Network, assetID uint32, acctAddr common.Address, cb bind.ContractBackend) (tokenContractor, error) { - token, tokenAddr, swapContractAddr, err := dexeth.VersionedNetworkToken(assetID, 0, net) - if err != nil { - return nil, err +func newV0TokenContractor(net dex.Network, token *dexeth.Token, acctAddr common.Address, cb bind.ContractBackend) (tokenContractor, error) { + netToken, found := token.NetTokens[net] + if !found { + return nil, fmt.Errorf("token %s has no network %s", token.Name, net) + } + tokenAddr := netToken.Address + contract, found := netToken.SwapContracts[0] // contract version 0 + if !found { + return nil, fmt.Errorf("token %s version 0 has no network %s token info", token.Name, net) } + swapContractAddr := contract.Address c, err := erc20v0.NewERC20Swap(swapContractAddr, cb) if err != nil { diff --git a/client/asset/eth/deploy.go b/client/asset/eth/deploy.go new file mode 100644 index 0000000000..ef4b6564ac --- /dev/null +++ b/client/asset/eth/deploy.go @@ -0,0 +1,319 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package eth + +import ( + "context" + "fmt" + "math/big" + "os" + "strings" + + "decred.org/dcrdex/client/asset" + "decred.org/dcrdex/dex" + erc20v0 "decred.org/dcrdex/dex/networks/erc20/contracts/v0" + dexeth "decred.org/dcrdex/dex/networks/eth" + ethv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +// contractDeployer deploys a dcrdex swap contract for an evm-compatible +// blockchain. contractDeployer can deploy both base asset contracts or ERC20 +// contracts. contractDeployer is used by the cmd/deploy/deploy utility. +type contractDeployer byte + +var ContractDeployer contractDeployer + +// EstimateDeployFunding estimates the fees required to deploy a contract. +// The gas estimate is only accurate if sufficient funds are in the wallet (so +// that estimateGas succeeds), otherwise a generously-padded estimate is +// generated. +func (contractDeployer) EstimateDeployFunding( + ctx context.Context, + chain string, + contractVer uint32, + tokenAddress common.Address, + credentialsPath string, + chainCfg *params.ChainConfig, + ui *dex.UnitInfo, + log dex.Logger, + net dex.Network, +) error { + + walletDir, err := os.MkdirTemp("", "") + if err != nil { + return err + } + defer os.RemoveAll(walletDir) + + cl, feeRate, err := ContractDeployer.nodeAndRate(ctx, chain, walletDir, credentialsPath, chainCfg, log, net) + if err != nil { + return err + } + defer cl.shutdown() + + log.Infof("Address: %s", cl.address()) + + baseChainBal, err := cl.addressBalance(ctx, cl.address()) + if err != nil { + return fmt.Errorf("error getting eth balance: %v", err) + } + + log.Infof("Balance: %s %s", ui.ConventionalString(dexeth.WeiToGwei(baseChainBal)), ui.Conventional.Unit) + + txData, err := ContractDeployer.txData(contractVer, tokenAddress) + if err != nil { + return err + } + + var gas uint64 + if baseChainBal.Cmp(new(big.Int)) > 0 { + // We may be able to get a proper estimate. + gas, err = cl.EstimateGas(ctx, ethereum.CallMsg{ + From: cl.creds.addr, + To: nil, // special value means deploy contract + Data: txData, + }) + gas = gas * 5 / 4 // 20% buffer on gas + if err != nil { + log.Debugf("EstimateGas error: %v", err) + log.Info("Unable to get on-chain estimate. balance probably too low. Falling back to rough estimate") + } + } + + const deploymentGas = 1_500_000 // eth v0: 687_671, token v0 825_478 + if gas == 0 { + gas = deploymentGas + } + fees := feeRate * gas + + log.Infof("Estimated fees: %s", ui.ConventionalString(fees)) + + gweiBal := dexeth.WeiToGwei(baseChainBal) + if fees < gweiBal { + log.Infof("👍 Current balance (%s %s) sufficient for fees (%s)", + ui.ConventionalString(gweiBal), ui.Conventional.Unit, ui.ConventionalString(fees)) + return nil + } + + shortage := fees - gweiBal + log.Infof("❌ Current balance (%[1]s %[2]s) insufficient for fees (%[3]s). Send %[4]s %[2]s to %[5]s", + ui.ConventionalString(gweiBal), ui.Conventional.Unit, ui.ConventionalString(fees), + ui.ConventionalString(shortage), cl.address()) + + return nil +} + +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 { + 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() + } + } + 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...) + } + return +} + +// DeployContract deployes a dcrdex swap contract. +func (contractDeployer) DeployContract( + ctx context.Context, + chain string, + contractVer uint32, + tokenAddress common.Address, + credentialsPath string, + chainCfg *params.ChainConfig, + ui *dex.UnitInfo, + log dex.Logger, + net dex.Network, +) error { + + walletDir, err := os.MkdirTemp("", "") + if err != nil { + return err + } + defer os.RemoveAll(walletDir) + + cl, feeRate, err := ContractDeployer.nodeAndRate(ctx, chain, walletDir, credentialsPath, chainCfg, log, net) + if err != nil { + return err + } + defer cl.shutdown() + + log.Infof("Address: %s", cl.address()) + + baseChainBal, err := cl.addressBalance(ctx, cl.address()) + if err != nil { + return fmt.Errorf("error getting eth balance: %v", err) + } + + log.Infof("Balance: %s %s", ui.ConventionalString(dexeth.WeiToGwei(baseChainBal)), ui.Conventional.Unit) + + txData, err := ContractDeployer.txData(contractVer, tokenAddress) + if err != nil { + return err + } + + // We may be able to get a proper estimate. + gas, err := cl.EstimateGas(ctx, ethereum.CallMsg{ + From: cl.address(), + To: nil, // special value means deploy contract + Data: txData, + }) + if err != nil { + return fmt.Errorf("EstimateGas error: %v", err) + } + + log.Infof("Estimated fees: %s", ui.ConventionalString(feeRate*gas)) + + gas *= 5 / 4 // Add 20% buffer + feesWithBuffer := feeRate * gas + + gweiBal := dexeth.WeiToGwei(baseChainBal) + if feesWithBuffer >= gweiBal { + shortage := feesWithBuffer - gweiBal + return fmt.Errorf("❌ Current balance (%[1]s %[2]s) insufficient for fees (%[3]s). Send %[4]s %[2]s to %[5]s", + ui.ConventionalString(gweiBal), ui.Conventional.Unit, ui.ConventionalString(feesWithBuffer), + ui.ConventionalString(shortage), cl.address()) + } + + txOpts, err := cl.txOpts(ctx, 0, gas, dexeth.GweiToWei(feeRate), nil) + if err != nil { + return err + } + + isToken := tokenAddress != (common.Address{}) + var contractAddr common.Address + var tx *types.Transaction + if isToken { + switch contractVer { + case 0: + contractAddr, tx, _, err = erc20v0.DeployERC20Swap(txOpts, cl.contractBackend(), tokenAddress) + } + } else { + switch contractVer { + case 0: + contractAddr, tx, _, err = ethv0.DeployETHSwap(txOpts, cl.contractBackend()) + } + } + if err != nil { + return fmt.Errorf("error deploying contract: %w", err) + } + if tx == nil { + return fmt.Errorf("no deployment function for version %d", contractVer) + } + + log.Infof("👍 Contract %s launched with tx %s", contractAddr, tx.Hash()) + + return nil +} + +// ReturnETH returns the remaining base asset balance from the deployment/getgas +// wallet to the specified return address. +func (contractDeployer) ReturnETH( + ctx context.Context, + chain string, + returnAddr common.Address, + credentialsPath string, + chainCfg *params.ChainConfig, + ui *dex.UnitInfo, + log dex.Logger, + net dex.Network, +) error { + + walletDir, err := os.MkdirTemp("", "") + if err != nil { + return err + } + defer os.RemoveAll(walletDir) + + cl, feeRate, err := ContractDeployer.nodeAndRate(ctx, chain, walletDir, credentialsPath, chainCfg, log, net) + if err != nil { + return err + } + defer cl.shutdown() + + return GetGas.returnFunds(ctx, cl, dexeth.GweiToWei(feeRate), returnAddr, nil, ui, log, net) +} + +func (contractDeployer) nodeAndRate( + ctx context.Context, + chain string, + walletDir, + credentialsPath string, + + chainCfg *params.ChainConfig, + log dex.Logger, + net dex.Network, +) (*multiRPCClient, uint64, error) { + + seed, providers, err := getFileCredentials(chain, credentialsPath, net) + if err != nil { + return nil, 0, err + } + + pw := []byte("abc") + chainID := chainCfg.ChainID.Int64() + + if err := CreateEVMWallet(chainID, &asset.CreateWalletParams{ + Type: walletTypeRPC, + Seed: seed, + Pass: pw, + Settings: map[string]string{providersKey: strings.Join(providers, " ")}, + DataDir: walletDir, + Net: net, + Logger: log, + }, nil /* we don't need the full api, skipConnect = true allows for nil compat */, true); err != nil { + return nil, 0, fmt.Errorf("error creating wallet: %w", err) + } + + cl, err := newMultiRPCClient(walletDir, providers, log, chainCfg, net) + if err != nil { + return nil, 0, fmt.Errorf("error creating rpc client: %w", err) + } + + if err := cl.unlock(string(pw)); err != nil { + return nil, 0, fmt.Errorf("error unlocking rpc client: %w", err) + } + + if err = cl.connect(ctx); err != nil { + return nil, 0, fmt.Errorf("error connecting: %w", err) + } + + base, tip, err := cl.currentFees(ctx) + if err != nil { + cl.shutdown() + return nil, 0, fmt.Errorf("Error estimating fee rate: %v", err) + } + + feeRate := dexeth.WeiToGwei(new(big.Int).Add(tip, new(big.Int).Mul(base, big.NewInt(2)))) + return cl, feeRate, nil +} diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index 15a90e73fc..75495d998a 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -31,8 +31,8 @@ import ( "decred.org/dcrdex/dex/config" "decred.org/dcrdex/dex/encode" "decred.org/dcrdex/dex/keygen" + "decred.org/dcrdex/dex/networks/erc20" dexeth "decred.org/dcrdex/dex/networks/eth" - dexpolygon "decred.org/dcrdex/dex/networks/polygon" "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" "github.com/decred/dcrd/hdkeychain/v3" "github.com/ethereum/go-ethereum" @@ -105,7 +105,7 @@ var ( // blockTicker is the delay between calls to check for new blocks. blockTicker = time.Second peerCountTicker = 5 * time.Second - WalletOpts = []*asset.ConfigOption{ + walletOpts = []*asset.ConfigOption{ { Key: "gasfeelimit", DisplayName: "Gas Fee Limit", @@ -129,7 +129,7 @@ var ( }, } // WalletInfo defines some general information about a Ethereum wallet. - WalletInfo = &asset.WalletInfo{ + WalletInfo = asset.WalletInfo{ Name: "Ethereum", Version: 0, // SupportedVersions: For Ethereum, the server backend maintains a @@ -153,13 +153,14 @@ var ( Type: walletTypeRPC, Tab: "RPC", Description: "Infrastructure providers (e.g. Infura) or local nodes", - ConfigOpts: append(RPCOpts, WalletOpts...), + ConfigOpts: append(RPCOpts, walletOpts...), 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, } // unlimitedAllowance is the maximum supported allowance for an erc20 @@ -178,24 +179,32 @@ var ( 0, // branch 0 0, // index 0 } +) - // 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. - perTxGasLimit = func() uint64 { - // blockGasLimit is the amount of gas we can use in one transaction - // according to the block gas limit. - blockGasLimit := ethconfig.Defaults.Miner.GasCeil / maxProportionOfBlockGasLimitToUse +// 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 - // txGasLimit is the amount of gas we can use in one transaction - // according to the default transaction gas fee limit. - txGasLimit := uint64(maxTxFeeGwei / defaultGasFeeLimit) + // blockGasLimit is the amount of gas we can use in one transaction + // according to the block gas limit. - if blockGasLimit > txGasLimit { - return txGasLimit - } - return blockGasLimit - }() -) + // 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 +} // WalletConfig are wallet-level configuration settings. type WalletConfig struct { @@ -221,7 +230,7 @@ var _ asset.Creator = (*Driver)(nil) // Open opens the ETH exchange wallet. Start the wallet with its Run method. func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) { - return NewEVMWallet(BipID, dexeth.ChainIDs[network], cfg, logger, network) + return newWallet(cfg, logger, network) } // DecodeCoinID creates a human-readable representation of a coin ID for Ethereum. @@ -271,16 +280,10 @@ func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { return "", fmt.Errorf("unknown coin ID format: %x", coinID) } -func ethWalletInfo(maxSwaps, maxRedeems uint64) *asset.WalletInfo { - wi := *WalletInfo - wi.MaxSwapsInTx = maxSwaps - wi.MaxRedeemsInTx = maxRedeems - return &wi -} - // Info returns basic information about the wallet and asset. func (d *Driver) Info() *asset.WalletInfo { - return WalletInfo + wi := WalletInfo + return &wi } // Exists checks the existence of the wallet. @@ -299,7 +302,11 @@ func (d *Driver) Exists(walletType, dataDir string, settings map[string]string, } func (d *Driver) Create(cfg *asset.CreateWalletParams) error { - return CreateEVMWallet(dexeth.ChainIDs[cfg.Net], cfg, false) + comp, err := NetworkCompatibilityData(cfg.Net) + if err != nil { + return fmt.Errorf("error finding compatibility data: %v", err) + } + return CreateEVMWallet(dexeth.ChainIDs[cfg.Net], cfg, &comp, false) } // Balance is the current balance, including information about the pending @@ -438,7 +445,7 @@ var _ asset.Wallet = (*TokenWallet)(nil) var _ asset.AccountLocker = (*ETHWallet)(nil) var _ asset.AccountLocker = (*TokenWallet)(nil) var _ asset.TokenMaster = (*ETHWallet)(nil) -var _ asset.WalletRestorer = (*assetWallet)(nil) +var _ asset.WalletRestorer = (*ETHWallet)(nil) var _ asset.LiveReconfigurer = (*ETHWallet)(nil) var _ asset.LiveReconfigurer = (*TokenWallet)(nil) var _ asset.TxFeeEstimator = (*ETHWallet)(nil) @@ -459,10 +466,11 @@ type baseWallet struct { dir string walletType string - // bipID is the asset ID of the chain's native token (e.g. ETH, MATIC, etc). - // This becomes the parent asset ID for non-native tokens(e.g USDC etc). - bipID uint32 - chainID int64 + baseChainID uint32 + chainCfg *params.ChainConfig + chainID int64 + compat *CompatibilityData + tokens map[uint32]*dexeth.Token tipMtx sync.RWMutex currentTip *types.Header @@ -500,6 +508,13 @@ type assetWallet struct { log dex.Logger ui dex.UnitInfo connected atomic.Bool + wi asset.WalletInfo + + versionedContracts map[uint32]common.Address + versionedGases map[uint32]*dexeth.Gases + + maxSwapGas uint64 + maxRedeemGas uint64 lockedFunds struct { mtx sync.RWMutex @@ -523,9 +538,6 @@ type assetWallet struct { evmify func(uint64) *big.Int atomize func(*big.Int) uint64 - maxSwapsInTx uint64 - maxRedeemsInTx uint64 - // pendingTxCheckBal is protected by the pendingTxMtx. We use this field // as a secondary check to see if we need to request confirmations for // pending txs, since tips are cached for up to 10 seconds. We check the @@ -553,26 +565,18 @@ type TokenWallet struct { netToken *dexeth.NetToken } -// 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 - -// Info returns basic information about the wallet and asset. -func (w *ETHWallet) Info() *asset.WalletInfo { - return ethWalletInfo(w.maxSwapsInTx, w.maxRedeemsInTx) +func (w *assetWallet) maxSwapsAndRedeems() (maxSwaps, maxRedeems uint64) { + txGasLimit := perTxGasLimit(atomic.LoadUint64(&w.gasFeeLimitV)) + return txGasLimit / w.maxSwapGas, txGasLimit / w.maxRedeemGas } // Info returns basic information about the wallet and asset. -func (w *TokenWallet) Info() *asset.WalletInfo { - return &asset.WalletInfo{ - Name: w.token.Name, - Version: WalletInfo.Version, - SupportedVersions: WalletInfo.SupportedVersions, - UnitInfo: w.token.UnitInfo, - MaxSwapsInTx: w.maxSwapsInTx, - MaxRedeemsInTx: w.maxRedeemsInTx, - } +func (w *assetWallet) Info() *asset.WalletInfo { + wi := w.wi + maxSwaps, maxRedeems := w.maxSwapsAndRedeems() + wi.MaxSwapsInTx = maxSwaps + wi.MaxRedeemsInTx = maxRedeems + return &wi } // genWalletSeed uses the wallet seed passed from core as the entropy for @@ -609,7 +613,7 @@ func privKeyFromSeed(seed []byte) (pk []byte, zero func(), err error) { return pk, extKey.Zero, nil } -func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams, skipConnect bool) error { +func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams, compat *CompatibilityData, skipConnect bool) error { switch createWalletParams.Type { case walletTypeGeth: return asset.ErrWalletTypeDisabled @@ -639,7 +643,7 @@ func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams // 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 + // write the compliant-providers.json file. Create the keystore // subdirectory as well to avoid a "failed to watch keystore folder" // error from the keystore's internal account cache supervisor. keystoreDir := filepath.Join(walletDir, "keystore") @@ -664,7 +668,7 @@ func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams if !skipConnect { if err := createAndCheckProviders(context.Background(), walletDir, endpoints, - big.NewInt(chainID), createWalletParams.Net, createWalletParams.Logger); err != nil { + big.NewInt(chainID), compat, createWalletParams.Net, createWalletParams.Logger); err != nil { return fmt.Errorf("create and check providers: %v", err) } } @@ -674,38 +678,91 @@ func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams return fmt.Errorf("unknown wallet type %q", createWalletParams.Type) } -// NewEVMWallet is the exported constructor required for asset.Wallet. -func NewEVMWallet(assetID uint32, chainID int64, assetCFG *asset.WalletConfig, logger dex.Logger, net dex.Network) (w *ETHWallet, err error) { +// newWallet is the constructor for an Ethereum asset.Wallet. +func newWallet(assetCFG *asset.WalletConfig, logger dex.Logger, net dex.Network) (w *ETHWallet, err error) { + chainCfg, err := ChainConfig(net) + if err != nil { + return nil, fmt.Errorf("failed to locate Ethereum genesis configuration for network %s", net) + } + comp, err := NetworkCompatibilityData(net) + if err != nil { + return nil, fmt.Errorf("failed to locate Ethereum compatibility data: %s", net) + } + contracts := make(map[uint32]common.Address, 1) + for ver, netAddrs := range dexeth.ContractAddresses { + for netw, addr := range netAddrs { + if netw == net { + contracts[ver] = addr + break + } + } + } + + return NewEVMWallet(&EVMWalletConfig{ + BaseChainID: BipID, + ChainCfg: chainCfg, + AssetCfg: assetCFG, + CompatData: &comp, + VersionedGases: dexeth.VersionedGases, + Tokens: dexeth.Tokens, + Logger: logger, + BaseChainContracts: contracts, + WalletInfo: WalletInfo, + Net: net, + }) +} + +// EVMWalletConfig is the configuration for an evm-compatible wallet. +type EVMWalletConfig struct { + BaseChainID uint32 + ChainCfg *params.ChainConfig + AssetCfg *asset.WalletConfig + CompatData *CompatibilityData + VersionedGases map[uint32]*dexeth.Gases + Tokens map[uint32]*dexeth.Token + Logger dex.Logger + BaseChainContracts map[uint32]common.Address + WalletInfo asset.WalletInfo + Net dex.Network +} + +func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) { + assetID := cfg.BaseChainID + chainID := cfg.ChainCfg.ChainID.Int64() + // var cl ethFetcher - switch assetCFG.Type { + switch cfg.AssetCfg.Type { case walletTypeGeth: return nil, asset.ErrWalletTypeDisabled case walletTypeRPC: - if _, found := assetCFG.Settings[providersKey]; !found { + if _, found := cfg.AssetCfg.Settings[providersKey]; !found { return nil, errors.New("no providers specified") } default: - return nil, fmt.Errorf("unknown wallet type %q", assetCFG.Type) + return nil, fmt.Errorf("unknown wallet type %q", cfg.AssetCfg.Type) } - cfg, err := parseWalletConfig(assetCFG.Settings) + wCfg, err := parseWalletConfig(cfg.AssetCfg.Settings) if err != nil { return nil, err } - gasFeeLimit := cfg.GasFeeLimit + gasFeeLimit := wCfg.GasFeeLimit if gasFeeLimit == 0 { gasFeeLimit = defaultGasFeeLimit } eth := &baseWallet{ - bipID: assetID, + net: cfg.Net, + baseChainID: cfg.BaseChainID, + chainCfg: cfg.ChainCfg, chainID: chainID, - log: logger, - net: net, - dir: assetCFG.DataDir, - walletType: assetCFG.Type, - settings: assetCFG.Settings, + compat: cfg.CompatData, + tokens: cfg.Tokens, + log: cfg.Logger, + dir: cfg.AssetCfg.DataDir, + walletType: cfg.AssetCfg.Type, + settings: cfg.AssetCfg.Settings, gasFeeLimitV: gasFeeLimit, wallets: make(map[uint32]*assetWallet), monitoredTxs: make(map[common.Hash]*monitoredTx), @@ -713,7 +770,7 @@ func NewEVMWallet(assetID uint32, chainID int64, assetCFG *asset.WalletConfig, l } var maxSwapGas, maxRedeemGas uint64 - for _, gases := range dexeth.VersionedGases { + for _, gases := range cfg.VersionedGases { if gases.Swap > maxSwapGas { maxSwapGas = gases.Swap } @@ -722,33 +779,40 @@ func NewEVMWallet(assetID uint32, chainID int64, assetCFG *asset.WalletConfig, l } } - if maxSwapGas == 0 || perTxGasLimit < maxSwapGas { + txGasLimit := perTxGasLimit(gasFeeLimit) + + if maxSwapGas == 0 || txGasLimit < maxSwapGas { return nil, errors.New("max swaps cannot be zero or undefined") } - if maxRedeemGas == 0 || perTxGasLimit < maxRedeemGas { + if maxRedeemGas == 0 || txGasLimit < maxRedeemGas { return nil, errors.New("max redeems cannot be zero or undefined") } aw := &assetWallet{ baseWallet: eth, - log: logger, + log: cfg.Logger, assetID: assetID, - tipChange: assetCFG.TipChange, + versionedContracts: cfg.BaseChainContracts, + versionedGases: cfg.VersionedGases, + maxSwapGas: maxSwapGas, + maxRedeemGas: maxRedeemGas, + tipChange: cfg.AssetCfg.TipChange, findRedemptionReqs: make(map[[32]byte]*findRedemptionRequest), pendingApprovals: make(map[uint32]*pendingApproval), approvalCache: make(map[uint32]bool), - peersChange: assetCFG.PeersChange, + peersChange: cfg.AssetCfg.PeersChange, contractors: make(map[uint32]contractor), evmify: dexeth.GweiToWei, atomize: dexeth.WeiToGwei, ui: dexeth.UnitInfo, - maxSwapsInTx: perTxGasLimit / maxSwapGas, - maxRedeemsInTx: perTxGasLimit / maxRedeemGas, pendingTxCheckBal: new(big.Int), + wi: cfg.WalletInfo, } - logger.Infof("ETH wallet will support a maximum of %d swaps and %d redeems per transaction.", - aw.maxSwapsInTx, aw.maxRedeemsInTx) + maxSwaps, maxRedeems := aw.maxSwapsAndRedeems() + + cfg.Logger.Infof("ETH wallet will support a maximum of %d swaps and %d redeems per transaction.", + maxSwaps, maxRedeems) aw.wallets = map[uint32]*assetWallet{ assetID: aw, @@ -807,11 +871,6 @@ func (w *ETHWallet) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) w.settingsMtx.RLock() defer w.settingsMtx.RUnlock() endpoints := strings.Split(w.settings[providersKey], " ") - ethCfg, err := chainConfig(w.chainID, w.net) - if err != nil { - return nil, err - } - chainConfig := ethCfg.Genesis.Config // Point to a harness node on simnet, if not specified. if w.net == dex.Simnet && len(endpoints) == 0 { @@ -819,7 +878,7 @@ func (w *ETHWallet) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) endpoints = append(endpoints, filepath.Join(u.HomeDir, "dextest", "eth", "beta", "node", "geth.ipc")) } - cl, err = newMultiRPCClient(w.dir, endpoints, w.log.SubLogger("RPC"), chainConfig, big.NewInt(w.chainID), w.net) + cl, err = newMultiRPCClient(w.dir, endpoints, w.log.SubLogger("RPC"), w.chainCfg, w.net) if err != nil { return nil, err } @@ -836,7 +895,11 @@ func (w *ETHWallet) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) } for ver, constructor := range contractorConstructors { - c, err := constructor(w.net, w.addr, w.node.contractBackend()) + 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) + } + c, err := constructor(contractAddr, w.addr, w.node.contractBackend()) if err != nil { return nil, fmt.Errorf("error constructor version %d contractor: %v", ver, err) } @@ -937,7 +1000,7 @@ func (w *ETHWallet) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, cu if rpc, is := w.node.(*multiRPCClient); is { walletDir := getWalletDir(w.dir, w.net) - if err := rpc.reconfigure(ctx, cfg.Settings, walletDir); err != nil { + if err := rpc.reconfigure(ctx, cfg.Settings, w.compat, walletDir); err != nil { return false, err } } @@ -1000,9 +1063,9 @@ func parseTokenWalletConfig(settings map[string]string) (cfg *tokenWalletConfig, // CreateTokenWallet "creates" a wallet for a token. There is really nothing // to do, except check that the token exists. -func (*baseWallet) CreateTokenWallet(tokenID uint32, _ map[string]string) error { +func (w *baseWallet) CreateTokenWallet(tokenID uint32, _ map[string]string) error { // Just check that the token exists for now. - if dexeth.Tokens[tokenID] == nil { + if w.tokens[tokenID] == nil { return fmt.Errorf("token not found for asset ID %d", tokenID) } return nil @@ -1010,7 +1073,7 @@ func (*baseWallet) CreateTokenWallet(tokenID uint32, _ map[string]string) error // OpenTokenWallet creates a new TokenWallet. func (w *ETHWallet) OpenTokenWallet(tokenCfg *asset.TokenConfig) (asset.Wallet, error) { - token, found := dexeth.Tokens[tokenCfg.AssetID] + token, found := w.tokens[tokenCfg.AssetID] if !found { return nil, fmt.Errorf("token %d not found", tokenCfg.AssetID) } @@ -1035,17 +1098,30 @@ func (w *ETHWallet) OpenTokenWallet(tokenCfg *asset.TokenConfig) (asset.Wallet, } } - if maxSwapGas == 0 || perTxGasLimit < maxSwapGas { + 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 || perTxGasLimit < maxRedeemGas { + if maxRedeemGas == 0 || txGasLimit < maxRedeemGas { return nil, errors.New("max redeems cannot be zero or undefined") } + 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 + } + aw := &assetWallet{ baseWallet: w.baseWallet, log: w.baseWallet.log.SubLogger(strings.ToUpper(dex.BipIDSymbol(tokenCfg.AssetID))), assetID: tokenCfg.AssetID, + versionedContracts: contracts, + versionedGases: gases, + maxSwapGas: maxSwapGas, + maxRedeemGas: maxRedeemGas, tipChange: tokenCfg.TipChange, peersChange: tokenCfg.PeersChange, findRedemptionReqs: make(map[[32]byte]*findRedemptionRequest), @@ -1055,9 +1131,13 @@ func (w *ETHWallet) OpenTokenWallet(tokenCfg *asset.TokenConfig) (asset.Wallet, evmify: token.AtomicToEVM, atomize: token.EVMToAtomic, ui: token.UnitInfo, - maxSwapsInTx: perTxGasLimit / maxSwapGas, - maxRedeemsInTx: perTxGasLimit / maxRedeemGas, - pendingTxCheckBal: new(big.Int), + wi: asset.WalletInfo{ + Name: token.Name, + Version: w.wi.Version, + SupportedVersions: w.wi.SupportedVersions, + UnitInfo: token.UnitInfo, + }, + pendingTxCheckBal: new(big.Int), } w.baseWallet.walletsMtx.Lock() @@ -1341,7 +1421,7 @@ func (w *assetWallet) estimateSwap(lots, lotSize uint64, maxFeeRate uint64, ver // gases gets the gas table for the specified contract version. func (w *assetWallet) gases(contractVer uint32) *dexeth.Gases { - return gases(w.bipID, w.assetID, contractVer, w.net) + return gases(contractVer, w.versionedGases) } // PreRedeem generates an estimate of the range of redemption fees that could @@ -1647,9 +1727,10 @@ func (w *assetWallet) swapGas(n int, ver uint32) (oneSwap, nSwap uint64, err err // 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) > w.maxSwapsInTx { - nMax = int(w.maxSwapsInTx) + if uint64(n) > maxSwaps { + nMax = int(maxSwaps) nFull = n / nMax nSwap = (oneSwap + uint64(nMax-1)*g.SwapAdd) * uint64(nFull) nRemain = n % nMax @@ -1941,7 +2022,7 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 } txHash := tx.Hash() - w.addPendingTx(w.bipID, txHash, tx.Nonce(), swapVal, 0, fees) + w.addPendingTx(w.assetID, txHash, tx.Nonce(), swapVal, 0, fees) receipts := make([]asset.Receipt, 0, n) for _, swap := range swaps.Contracts { @@ -1953,7 +2034,7 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 txHash: txHash, secretHash: secretHash, ver: swaps.Version, - contractAddr: dexeth.ContractAddresses[swaps.Version][w.net].String(), + contractAddr: w.versionedContracts[swaps.Version].String(), }) } @@ -2288,7 +2369,7 @@ func (w *assetWallet) approveToken(amount *big.Int, maxFeeRate, gasLimit uint64, } func (w *assetWallet) approvalStatus(version uint32) (asset.ApprovalStatus, error) { - if w.assetID == BipID { + if w.assetID == w.baseChainID { return asset.Approved, nil } @@ -2356,7 +2437,7 @@ func (w *TokenWallet) ApproveToken(assetVer uint32, onConfirm func()) (string, e return "", fmt.Errorf("error getting eth balance: %w", err) } if ethBal.Available < approvalGas*feeRateGwei { - return "", fmt.Errorf("insufficient eth balance for approval. required: %d, available: %d", + return "", fmt.Errorf("insufficient fee balance for approval. required: %d, available: %d", approvalGas*feeRateGwei, ethBal.Available) } @@ -3080,7 +3161,7 @@ func (w *TokenWallet) EstimateSendTxFee(addr string, value, _ uint64, subtract b // RestorationInfo returns information about how to restore the wallet in // various external wallets. -func (w *assetWallet) RestorationInfo(seed []byte) ([]*asset.WalletRestoration, error) { +func (w *ETHWallet) RestorationInfo(seed []byte) ([]*asset.WalletRestoration, error) { privateKey, zero, err := privKeyFromSeed(seed) if err != nil { return nil, err @@ -3991,12 +4072,12 @@ func (w *assetWallet) sumPendingTxs(bal *big.Int) (out, in uint64) { tip := w.currentTip.Number.Uint64() w.tipMtx.RUnlock() - isToken := w.assetID != w.bipID + isToken := w.assetID != w.baseChainID addPendingTx := func(pt *pendingTx) { in += pt.in if !isToken { - if pt.assetID != w.bipID { + if pt.assetID != w.baseChainID { out += pt.maxFees } else { out += pt.out + pt.maxFees @@ -4051,7 +4132,7 @@ func (w *assetWallet) sumPendingTxs(bal *big.Int) (out, in uint64) { } func (w *assetWallet) balanceWithTxPool() (*Balance, error) { - isToken := w.assetID != w.bipID + isToken := w.assetID != w.baseChainID getBal := func() (*big.Int, error) { if !isToken { @@ -4210,7 +4291,7 @@ func (w *assetWallet) initiate(ctx context.Context, assetID uint32, contracts [] maxFeeRate, gasLimit uint64, contractVer uint32) (tx *types.Transaction, err error) { var val uint64 - if assetID == w.bipID { + if assetID == w.baseChainID { for _, c := range contracts { val += c.Value } @@ -4262,7 +4343,7 @@ func (w *assetWallet) estimateRefundGas(ctx context.Context, secretHash [32]byte // loadContractors prepares the token contractors and add them to the map. func (w *assetWallet) loadContractors() error { - token, found := dexeth.Tokens[w.assetID] + token, found := w.tokens[w.assetID] if !found { return fmt.Errorf("token %d not found", w.assetID) } @@ -4277,7 +4358,7 @@ func (w *assetWallet) loadContractors() error { w.log.Errorf("contractor constructor not found for token %s, version %d", token.Name, ver) continue } - c, err := constructor(w.net, w.assetID, w.addr, w.node.contractBackend()) + c, err := constructor(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) } @@ -4430,60 +4511,64 @@ func checkTxStatus(receipt *types.Receipt, gasLimit uint64) error { return nil } -// fileCredentials contain the seed and providers to use for GetGasEstimates. -type fileCredentials struct { - Seed dex.Bytes `json:"seed"` - Providers map[string]string `json:"providers"` +// providersFile reads a file located at ~/dextest/credentials.json. +// The file contains seed and provider information for wallets used for +// getgas, deploy, and nodeclient testing. If simnet providers are not +// specified, getFileCredentials will add the simnet alpha node. +type providersFile struct { + Seed dex.Bytes `json:"seed"` + Providers map[string] /* symbol */ map[string] /* network */ []string `json:"providers"` } // getFileCredentials reads the file at path and extracts the seed and the // provider for the network. -func getFileCredentials(path string, net dex.Network) (*fileCredentials, string, error) { +func getFileCredentials(chain, path string, net dex.Network) (seed []byte, providers []string, err error) { b, err := os.ReadFile(path) if err != nil { - return nil, "", fmt.Errorf("error reading credentials file: %v", err) + return nil, nil, fmt.Errorf("error reading credentials file: %v", err) } - creds := new(fileCredentials) - if err := json.Unmarshal(b, creds); err != nil { - return nil, "", fmt.Errorf("error parsing credentials file: %v", err) + var p providersFile + if err := json.Unmarshal(b, &p); err != nil { + return nil, nil, fmt.Errorf("error parsing credentials file: %v", err) } - if len(creds.Seed) == 0 { - return nil, "", fmt.Errorf("must provide both seeds in testnet credentials file") + if len(p.Seed) == 0 { + return nil, nil, fmt.Errorf("must provide both seeds in credentials file") } - provider := creds.Providers[net.String()] - if provider == "" { - return nil, "", fmt.Errorf("credentials file does not specify an RPC provider") + seed = p.Seed + providers = p.Providers[chain][net.String()] + if net == dex.Simnet && len(providers) == 0 { + u, _ := user.Current() + switch chain { + case "polygon": + providers = []string{filepath.Join(u.HomeDir, "dextest", chain, "alpha", "bor", "bor.ipc")} + default: + providers = []string{filepath.Join(u.HomeDir, "dextest", chain, "alpha", "node", "geth.ipc")} + } } - return creds, provider, nil + return } // quickNode constructs a multiRPCClient and a contractor for the specified // asset. The client is connected and unlocked. -func quickNode(ctx context.Context, walletDir string, assetID, contractVer uint32, - seed []byte, provider string, net dex.Network, log dex.Logger) (*multiRPCClient, contractor, error) { +func quickNode(ctx context.Context, walletDir string, contractVer uint32, + seed []byte, providers []string, wParams *GetGasWalletParams, net dex.Network, log dex.Logger) (*multiRPCClient, contractor, error) { pw := []byte("abc") + chainID := wParams.ChainCfg.ChainID.Int64() - chainID := dexeth.ChainIDs[net] if err := CreateEVMWallet(chainID, &asset.CreateWalletParams{ Type: walletTypeRPC, Seed: seed, Pass: pw, - Settings: map[string]string{providersKey: provider}, + Settings: map[string]string{providersKey: strings.Join(providers, " ")}, DataDir: walletDir, Net: net, Logger: log, - }, false); err != nil { + }, wParams.Compat, false); err != nil { return nil, nil, fmt.Errorf("error creating initiator wallet: %v", err) } - ethCfg, err := chainConfig(chainID, net) - if err != nil { - return nil, nil, err - } - chainConfig := ethCfg.Genesis.Config - - cl, err := newMultiRPCClient(walletDir, []string{provider}, log, chainConfig, big.NewInt(chainID), net) + cl, err := newMultiRPCClient(walletDir, providers, log, wParams.ChainCfg, net) if err != nil { return nil, nil, fmt.Errorf("error opening initiator rpc client: %v", err) } @@ -4504,12 +4589,12 @@ func quickNode(ctx context.Context, walletDir string, assetID, contractVer uint3 } var c contractor - if assetID == BipID { + if wParams.Token == nil { ctor := contractorConstructors[contractVer] if ctor == nil { return nil, nil, fmt.Errorf("no contractor constructor for eth contract version %d", contractVer) } - c, err = ctor(net, cl.address(), cl.contractBackend()) + c, err = ctor(wParams.ContractAddr, cl.address(), cl.contractBackend()) if err != nil { return nil, nil, fmt.Errorf("contractor constructor error: %v", err) } @@ -4518,7 +4603,7 @@ func quickNode(ctx context.Context, walletDir string, assetID, contractVer uint3 if ctor == nil { return nil, nil, fmt.Errorf("no token contractor constructor for eth contract version %d", contractVer) } - c, err = ctor(net, assetID, cl.address(), cl.contractBackend()) + c, err = ctor(net, wParams.Token, cl.address(), cl.contractBackend()) if err != nil { return nil, nil, fmt.Errorf("token contractor constructor error: %v", err) } @@ -4563,7 +4648,7 @@ func waitForConfirmation(ctx context.Context, cl ethFetcher, txHash common.Hash) // runSimnetMiner starts a gouroutine to generate a simnet block every 5 seconds // until the ctx is canceled. By default, the eth harness will mine a block // every 15s. We want to speed it up a bit for e.g. GetGas testing. -func runSimnetMiner(ctx context.Context, log dex.Logger) { +func runSimnetMiner(ctx context.Context, symbol string, log dex.Logger) { log.Infof("Starting the simnet miner") go func() { tick := time.NewTicker(time.Second * 5) @@ -4577,7 +4662,7 @@ func runSimnetMiner(ctx context.Context, log dex.Logger) { case <-tick.C: log.Debugf("Mining a simnet block") mine := exec.CommandContext(ctx, "./mine-alpha", "1") - mine.Dir = filepath.Join(u.HomeDir, "dextest", "eth", "harness-ctl") + mine.Dir = filepath.Join(u.HomeDir, "dextest", symbol, "harness-ctl") b, err := mine.CombinedOutput() if err != nil { log.Errorf("Mining error: %v", err) @@ -4595,22 +4680,33 @@ type getGas byte // GetGas provides access to the gas estimation utilities. var GetGas getGas +// GetGasWalletParams are the configuration parameters required to estimate +// swap contract gas usage. +type GetGasWalletParams struct { + ChainCfg *params.ChainConfig + Gas *dexeth.Gases + Token *dexeth.Token + UnitInfo *dex.UnitInfo + BaseUnitInfo *dex.UnitInfo + Compat *CompatibilityData + ContractAddr common.Address // Base chain contract addr. +} + // ReadCredentials reads the credentials for the network from the credentials // file. -func (getGas) ReadCredentials(credentialsPath string, net dex.Network) (addr, provider string, err error) { - var creds *fileCredentials - creds, provider, err = getFileCredentials(credentialsPath, net) +func (getGas) ReadCredentials(chain, credentialsPath string, net dex.Network) (addr string, providers []string, err error) { + seed, providers, err := getFileCredentials(chain, credentialsPath, net) if err != nil { - return "", "", err + return "", nil, err } - privB, zero, err := privKeyFromSeed(creds.Seed) + privB, zero, err := privKeyFromSeed(seed) if err != nil { - return "", "", err + return "", nil, err } defer zero() privateKey, err := crypto.ToECDSA(privB) if err != nil { - return "", "", err + return "", nil, err } addr = crypto.PubkeyToAddress(privateKey.PublicKey).String() @@ -4618,17 +4714,12 @@ func (getGas) ReadCredentials(credentialsPath string, net dex.Network) (addr, pr } func getGetGasClientWithEstimatesAndBalances(ctx context.Context, net dex.Network, assetID, contractVer uint32, maxSwaps int, - walletDir, provider string, seed []byte, log dex.Logger) (cl *multiRPCClient, c contractor, g *dexeth.Gases, + 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) { - g = gases(uint32(dexeth.ChainIDs[net]), assetID, contractVer, net) - if g == nil { - return nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("no gas table found for %s, contract version %d", dex.BipIDSymbol(assetID), contractVer) - } - - cl, c, err = quickNode(ctx, walletDir, assetID, contractVer, seed, provider, net, log) + cl, c, err = quickNode(ctx, walletDir, contractVer, seed, providers, wParams, net, log) if err != nil { - return nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error creating initiator wallet: %v", err) + return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error creating initiator wallet: %v", err) } var success bool @@ -4640,17 +4731,18 @@ func getGetGasClientWithEstimatesAndBalances(ctx context.Context, net dex.Networ base, tip, err := cl.currentFees(ctx) if err != nil { - return nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("Error estimating fee rate: %v", err) + return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("Error estimating fee rate: %v", err) } ethBal, err = cl.addressBalance(ctx, cl.address()) if err != nil { - return nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error getting eth balance: %v", err) + return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error getting eth balance: %v", err) } feeRate = dexeth.WeiToGwei(new(big.Int).Add(tip, new(big.Int).Mul(base, big.NewInt(2)))) // Check that we have a balance for swaps and fees. + g := wParams.Gas const swapVal = 1 n := uint64(maxSwaps) swapReq = n * (n + 1) / 2 * swapVal // Sum of positive integers up to n @@ -4663,13 +4755,13 @@ func getGetGasClientWithEstimatesAndBalances(ctx context.Context, net dex.Networ 6 / 5 * // base rate increase accommodation feeRate - isToken := assetID != BipID + isToken := wParams.Token != nil ethReq = fees + swapReq if isToken { tc := c.(tokenContractor) tokenBal, err = tc.balance(ctx) if err != nil { - return nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error fetching token balance: %v", err) + return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error fetching token balance: %v", err) } fees += (g.Transfer*2 + g.Approve*2*2 /* two approvals */ + defaultSendGasLimit /* approval client fee funding tx */) * @@ -4680,19 +4772,23 @@ func getGetGasClientWithEstimatesAndBalances(ctx context.Context, net dex.Networ return } +func (getGas) chainForAssetID(assetID uint32) string { + ti := asset.TokenInfo(assetID) + if ti == nil { + return dex.BipIDSymbol(assetID) + } + return dex.BipIDSymbol(ti.ParentID) +} + // EstimateFunding estimates how much funding is needed for estimating gas, and // prints helpful messages for the user. func (getGas) EstimateFunding(ctx context.Context, net dex.Network, assetID, contractVer uint32, - maxSwaps int, credentialsPath string, log dex.Logger) error { + maxSwaps int, credentialsPath string, wParams *GetGasWalletParams, log dex.Logger) error { symbol := dex.BipIDSymbol(assetID) - log.Infof("Estimating required funding for up to %d swaps of asset %s, contract version %d on %s", maxSwaps, symbol, contractVer, symbol) - - if net == dex.Simnet { - dexeth.MaybeReadSimnetAddrs() - } + log.Infof("Estimating required funding for up to %d swaps of asset %s, contract version %d on %s", maxSwaps, symbol, contractVer, net) - creds, provider, err := getFileCredentials(credentialsPath, net) + seed, providers, err := getFileCredentials(GetGas.chainForAssetID(assetID), credentialsPath, net) if err != nil { return err } @@ -4703,45 +4799,50 @@ func (getGas) EstimateFunding(ctx context.Context, net dex.Network, assetID, con } defer os.RemoveAll(walletDir) - cl, _, _, ethReq, swapReq, _, ethBalBig, tokenBalBig, err := getGetGasClientWithEstimatesAndBalances(ctx, net, assetID, contractVer, maxSwaps, walletDir, provider, creds.Seed, log) + cl, _, ethReq, swapReq, _, ethBalBig, tokenBalBig, err := getGetGasClientWithEstimatesAndBalances(ctx, net, assetID, contractVer, maxSwaps, walletDir, providers, seed, wParams, log) if err != nil { return err } defer cl.shutdown() ethBal := dexeth.WeiToGwei(ethBalBig) - ethFmt := dexeth.UnitInfo.ConventionalString + ui := wParams.UnitInfo + assetFmt := ui.ConventionalString + bui := wParams.BaseUnitInfo + ethFmt := bui.ConventionalString log.Info("Address:", cl.address()) - log.Info("Ethereum balance:", ethFmt(ethBal), "ETH") + log.Infof("%s balance: %s", bui.Conventional.Unit, ethFmt(ethBal)) - isToken := assetID != BipID + isToken := wParams.Token != nil tokenBalOK := true if isToken { - log.Infof("Ethereum required for fees: %s", ethFmt(ethReq)) + log.Infof("%s required for fees: %s", bui.Conventional.Unit, ethFmt(ethReq)) ui, err := asset.UnitInfo(assetID) if err != nil { return fmt.Errorf("error getting unit info for %d: %v", assetID, err) } - tokenBal := dexeth.Tokens[assetID].EVMToAtomic(tokenBalBig) - log.Infof("%s balance: %s", ui.Conventional.Unit, ui.ConventionalString(tokenBal)) - log.Infof("%s required for trading: %s", ui.Conventional.Unit, ui.ConventionalString(swapReq)) + tokenBal := wParams.Token.EVMToAtomic(tokenBalBig) + log.Infof("%s balance: %s", ui.Conventional.Unit, assetFmt(tokenBal)) + log.Infof("%s required for trading: %s", ui.Conventional.Unit, assetFmt(swapReq)) if tokenBal < swapReq { tokenBalOK = false - log.Infof("❌ Insufficient token balance. Deposit %s %s before getting a gas estimate", - ui.ConventionalString(swapReq-tokenBal), ui.Conventional.Unit) + log.Infof("❌ Insufficient %[2]s balance. Deposit %[1]s %[2]s before getting a gas estimate", + assetFmt(swapReq-tokenBal), ui.Conventional.Unit) } } else { - log.Infof("Ethereum required: %s (swaps) + %s (fees) = %s", ethFmt(swapReq), ethFmt(ethReq-swapReq), ethFmt(ethReq)) + log.Infof("%s required: %s (swaps) + %s (fees) = %s", + ui.Conventional.Unit, ethFmt(swapReq), ethFmt(ethReq-swapReq), ethFmt(ethReq)) } if ethBal < ethReq { // Add 10% for fee drift. ethRecommended := ethReq * 11 / 10 - log.Infof("❌ Insufficient Ethereum Balance. Deposit about %s ETH before getting a gas estimate", ethFmt(ethRecommended-ethBal)) + log.Infof("❌ Insufficient %s balance. Deposit about %s %s before getting a gas estimate", + bui.Conventional.Unit, ethFmt(ethRecommended-ethBal), bui.Conventional.Unit) } else if tokenBalOK { log.Infof("👍 You have sufficient funding to run a gas estimate") } @@ -4749,20 +4850,25 @@ func (getGas) EstimateFunding(ctx context.Context, net dex.Network, assetID, con return nil } -// ReturnETH returns the estimation wallet's Ethereum balance to a specified -// address, if it is more than fees required to send. Note: There is no way yet -// to get token balances returned, because the amount of token balance required -// is typically only a few atoms. The user should only fund a token's balance -// with the recommended amount from EstimateFunding. -func (getGas) ReturnETH(ctx context.Context, credentialsPath, returnAddr string, net dex.Network, log dex.Logger) error { - const assetID = BipID // Only return ethereum. For tokens, the wallets should only ever be loaded with a few atoms. +// Return returns the estimation wallet's base-chain or token balance to a +// specified address, if it is more than fees required to send. +func (getGas) Return( + ctx context.Context, + assetID uint32, + credentialsPath, + returnAddr string, + wParams *GetGasWalletParams, + net dex.Network, + log dex.Logger, +) error { + const contractVer = 0 // Doesn't matter if !common.IsHexAddress(returnAddr) { return fmt.Errorf("supplied return address %q is not an Ethereum address", returnAddr) } - creds, provider, err := getFileCredentials(credentialsPath, net) + seed, providers, err := getFileCredentials(GetGas.chainForAssetID(assetID), credentialsPath, net) if err != nil { return err } @@ -4773,7 +4879,7 @@ func (getGas) ReturnETH(ctx context.Context, credentialsPath, returnAddr string, } defer os.RemoveAll(walletDir) - cl, _, err := quickNode(ctx, walletDir, assetID, contractVer, creds.Seed, provider, net, log) + cl, _, err := quickNode(ctx, walletDir, contractVer, seed, providers, wParams, net, log) if err != nil { return fmt.Errorf("error creating initiator wallet: %v", err) } @@ -4786,29 +4892,89 @@ func (getGas) ReturnETH(ctx context.Context, credentialsPath, returnAddr string, recommendedFeeRate := new(big.Int).Add(tip, new(big.Int).Mul(base, big.NewInt(2))) + return GetGas.returnFunds(ctx, cl, recommendedFeeRate, common.HexToAddress(returnAddr), wParams.Token, wParams.UnitInfo, log, net) +} + +func (getGas) returnFunds( + ctx context.Context, + cl *multiRPCClient, + feeRate *big.Int, + returnAddr common.Address, + token *dexeth.Token, // nil for base chain + ui *dex.UnitInfo, + log dex.Logger, + net dex.Network, +) error { + bigEthBal, err := cl.addressBalance(ctx, cl.address()) if err != nil { return fmt.Errorf("error getting eth balance: %v", err) } ethBal := dexeth.WeiToGwei(bigEthBal) - bigFees := new(big.Int).Mul(new(big.Int).SetUint64(defaultSendGasLimit), recommendedFeeRate) + if token != nil { + nt, found := token.NetTokens[net] + if !found { + return fmt.Errorf("no %s token for %s", token.Name, net) + } + var g dexeth.Gases + for _, sc := range nt.SwapContracts { + g = sc.Gas + break + } + fees := g.Transfer * dexeth.WeiToGwei(feeRate) + if fees > ethBal { + return fmt.Errorf("not enough base chain balance (%s) to cover fees (%s)", + dexeth.UnitInfo.ConventionalString(ethBal), dexeth.UnitInfo.ConventionalString(fees)) + } + + tokenContract, err := erc20.NewIERC20(nt.Address, cl.contractBackend()) + if err != nil { + return fmt.Errorf("NewIERC20 error: %v", err) + } + + callOpts := &bind.CallOpts{ + From: cl.address(), + Context: ctx, + } + + bigTokenBal, err := tokenContract.BalanceOf(callOpts, cl.address()) + if err != nil { + return fmt.Errorf("error getting token balance: %w", err) + } + + txOpts, err := cl.txOpts(ctx, 0, g.Transfer, feeRate, nil) + if err != nil { + return fmt.Errorf("error generating tx opts: %w", err) + } + + tx, err := tokenContract.Transfer(txOpts, returnAddr, bigTokenBal) + if err != nil { + return fmt.Errorf("error transferring tokens : %w", err) + } + log.Infof("Sent %s in transaction %s", ui.ConventionalString(token.EVMToAtomic(bigTokenBal)), tx.Hash()) + return nil + } + + bigFees := new(big.Int).Mul(new(big.Int).SetUint64(defaultSendGasLimit), feeRate) + fees := dexeth.WeiToGwei(bigFees) - ethFmt := dexeth.UnitInfo.ConventionalString + + ethFmt := ui.ConventionalString if fees >= ethBal { return fmt.Errorf("balance is lower than projected fees: %s < %s", ethFmt(ethBal), ethFmt(fees)) } remainder := ethBal - fees - txOpts, err := cl.txOpts(ctx, remainder, defaultSendGasLimit, recommendedFeeRate, nil) + txOpts, err := cl.txOpts(ctx, remainder, defaultSendGasLimit, feeRate, nil) if err != nil { return fmt.Errorf("error generating tx opts: %w", err) } - tx, err := cl.sendTransaction(ctx, txOpts, common.HexToAddress(returnAddr), nil) + tx, err := cl.sendTransaction(ctx, txOpts, returnAddr, nil) if err != nil { return fmt.Errorf("error sending funds: %w", err) } - log.Info("!!! Success!!! txid =", tx.Hash()) + log.Infof("Sent %s in transaction %s", ui.ConventionalString(remainder), tx.Hash()) return nil } @@ -4821,17 +4987,15 @@ func (getGas) ReturnETH(ctx context.Context, credentialsPath, returnAddr string, // not recoverable. If you run this function with insufficient or zero ETH // and/or token balance on the seed, the function will error with a message // indicating the amount of funding needed to run. -func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVer uint32, maxSwaps int, credentialsPath string, log dex.Logger) error { +func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVer uint32, maxSwaps int, + credentialsPath string, wParams *GetGasWalletParams, log dex.Logger) error { + 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) - if net == dex.Simnet { - dexeth.MaybeReadSimnetAddrs() - } - - isToken := assetID != BipID + isToken := wParams.Token != nil - creds, provider, err := getFileCredentials(credentialsPath, net) + seed, providers, err := getFileCredentials(GetGas.chainForAssetID(assetID), credentialsPath, net) if err != nil { return err } @@ -4842,7 +5006,7 @@ func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVe } defer os.RemoveAll(walletDir) - cl, c, gases, ethReq, swapReq, feeRate, ethBal, tokenBal, err := getGetGasClientWithEstimatesAndBalances(ctx, net, assetID, contractVer, maxSwaps, walletDir, provider, creds.Seed, log) + cl, c, ethReq, swapReq, feeRate, ethBal, tokenBal, err := getGetGasClientWithEstimatesAndBalances(ctx, net, assetID, contractVer, maxSwaps, walletDir, providers, seed, wParams, log) if err != nil { return err } @@ -4850,39 +5014,42 @@ func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVe log.Infof("Initiator address: %s", cl.address()) - log.Infof("ETH balance: %s", dexeth.UnitInfo.ConventionalString(dexeth.WeiToGwei(ethBal))) + ui := wParams.UnitInfo + assetFmt := ui.ConventionalString + bui := wParams.BaseUnitInfo + bUnit := bui.Conventional.Unit + ethFmt := bui.ConventionalString + + log.Infof("%s balance: %s", bUnit, ethFmt(dexeth.WeiToGwei(ethBal))) atomicBal := dexeth.WeiToGwei(ethBal) if atomicBal < ethReq { - return fmt.Errorf("eth balance insufficient to get gas estimates. current: %s, required %s ETH. send eth to %s", - dexeth.UnitInfo.ConventionalString(atomicBal), dexeth.UnitInfo.ConventionalString(ethReq), cl.address()) + return fmt.Errorf("%s balance insufficient to get gas estimates. current: %[2]s, required ~ %[3]s %[1]s. send %[1]s to %[4]s", + bUnit, ethFmt(atomicBal), ethFmt(ethReq*5/4), cl.address()) } // Run the miner now, in case we need it for the approval client preload. if net == dex.Simnet { - runSimnetMiner(ctx, log) + symbolParts := strings.Split(symbol, ".") // e.g. dextt.polygon, dextt.eth + runSimnetMiner(ctx, symbolParts[len(symbolParts)-1], log) } var approvalClient *multiRPCClient var approvalContractor tokenContractor if isToken { - ui, err := asset.UnitInfo(assetID) - if err != nil { - return fmt.Errorf("error getting unit info for %d: %v", assetID, err) - } - atomicBal := dexeth.Tokens[assetID].EVMToAtomic(tokenBal) + atomicBal := wParams.Token.EVMToAtomic(tokenBal) convUnit := ui.Conventional.Unit - log.Infof("%s balance: %s %s", strings.ToUpper(symbol), ui.ConventionalString(atomicBal), convUnit) + log.Infof("%s balance: %s %s", strings.ToUpper(symbol), assetFmt(atomicBal), convUnit) log.Infof("%d %s required for swaps", swapReq, ui.AtomicUnit) - log.Infof("%d gwei eth required for fees", ethReq) + log.Infof("%d gwei %s required for fees", ethReq, bui.Conventional.Unit) if atomicBal < swapReq { - return fmt.Errorf("token balance insufficient to get gas estimates. current: %s, required %s %s. send %s to %s", - ui.ConventionalString(atomicBal), ui.ConventionalString(swapReq), convUnit, symbol, cl.address()) + return fmt.Errorf("%[3]s balance insufficient to get gas estimates. current: %[1]s, required ~ %[2]s %[3]s. send %[3]s to %[4]s", + assetFmt(atomicBal), assetFmt(swapReq), convUnit, cl.address()) } var mrc contractor - approvalClient, mrc, err = quickNode(ctx, filepath.Join(walletDir, "ac_dir"), assetID, contractVer, encode.RandomBytes(32), provider, net, log) + approvalClient, mrc, err = quickNode(ctx, filepath.Join(walletDir, "ac_dir"), contractVer, encode.RandomBytes(32), providers, wParams, net, log) if err != nil { return fmt.Errorf("error creating approval contract node: %v", err) } @@ -4892,7 +5059,7 @@ func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVe // TODO: We're overloading by probably 140% here, but this is what // we've reserved in our fee checks. Is it worth recovering unused // balance? - feePreload := gases.Approve * 2 * 6 / 5 * feeRate + feePreload := wParams.Gas.Approve * 2 * 6 / 5 * feeRate txOpts, err := cl.txOpts(ctx, feePreload, defaultSendGasLimit, nil, nil) if err != nil { return fmt.Errorf("error creating tx opts for sending fees for approval client: %v", err) @@ -4907,11 +5074,11 @@ func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVe } } else { - log.Infof("%d gwei eth required for fees and swaps", ethReq) + log.Infof("%d gwei %s required for fees and swaps", ethReq, bui.Conventional.Unit) } log.Debugf("Getting gas estimates") - return getGasEstimates(ctx, cl, approvalClient, c, approvalContractor, maxSwaps, gases, log) + return getGasEstimates(ctx, cl, approvalClient, c, approvalContractor, maxSwaps, wParams.Gas, log) } // getGasEstimate is used to get a gas table for an asset's contract(s). The @@ -5152,24 +5319,3 @@ func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac t return nil } - -// simnetDataDir returns the data directory for the given simnet chainID. See: -// dex/testing/{dir} -func simnetDataDir(chainID int64) (string, error) { - var dir string - switch chainID { - case dexpolygon.SimnetChainID: - dir = "polygon" - case dexeth.SimnetChainID: - dir = "eth" - default: - return "", fmt.Errorf("unknown simnet chainID %d", chainID) - } - - u, err := user.Current() - if err != nil { - return "", fmt.Errorf("error getting current user: %w", err) - } - - return filepath.Join(u.HomeDir, "dextest", dir), nil -} diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index 88580daf98..cc2efe6c70 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -593,10 +593,23 @@ func tassetWallet(assetID uint32) (asset.Wallet, *assetWallet, *tMempoolNode, co c = node.tokenContractor } + versionedGases := make(map[uint32]*dexeth.Gases) + if assetID == BipID { // just make a copy + for ver, g := range dexeth.VersionedGases { + versionedGases[ver] = g + } + } else { + netToken := dexeth.Tokens[assetID].NetTokens[dex.Simnet] + for ver, c := range netToken.SwapContracts { + versionedGases[ver] = &c.Gas + } + } + aw := &assetWallet{ baseWallet: &baseWallet{ - bipID: BipID, + baseChainID: BipID, chainID: dexeth.ChainIDs[dex.Simnet], + tokens: dexeth.Tokens, addr: node.addr, net: dex.Simnet, node: node, @@ -607,17 +620,20 @@ func tassetWallet(assetID uint32) (asset.Wallet, *assetWallet, *tMempoolNode, co monitoredTxDB: kvdb.NewMemoryDB(), pendingTxs: make(map[common.Hash]*pendingTx), }, + 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), evmify: dexeth.GweiToWei, atomize: dexeth.WeiToGwei, - maxSwapsInTx: 40, - maxRedeemsInTx: 60, pendingTxCheckBal: new(big.Int), pendingApprovals: make(map[uint32]*pendingApproval), approvalCache: make(map[uint32]bool), + // move up after review + wi: WalletInfo, } aw.wallets = map[uint32]*assetWallet{ BipID: aw, @@ -1010,14 +1026,9 @@ func testRefund(t *testing.T, assetID uint32) { gasesV1 := &dexeth.Gases{Refund: 1e5} if assetID == BipID { - dexeth.VersionedGases[1] = gasesV1 - defer delete(dexeth.VersionedGases, 1) + eth.versionedGases[1] = gasesV1 } else { - tokenContracts := dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts - tc := *tokenContracts[0] - tc.Gas = *gasesV1 - tokenContracts[1] = &tc - defer delete(tokenContracts, 1) + eth.versionedGases[1] = &dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas v1c = &tTokenContractor{tContractor: v1Contractor} } @@ -2226,10 +2237,14 @@ func testRedeem(t *testing.T, assetID uint32) { // Test with a non-zero contract version to ensure it makes it into the receipt contractVer := uint32(1) - dexeth.VersionedGases[1] = ethGases // for dexeth.RedeemGas(..., 1) - tokenContracts := dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts + + eth.versionedGases[1] = ethGases + if assetID != BipID { + eth.versionedGases[1] = &tokenGases + } + + tokenContracts := eth.tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts tokenContracts[1] = tokenContracts[0] - defer delete(dexeth.VersionedGases, 1) defer delete(tokenContracts, 1) contractorV1 := &tContractor{ @@ -3383,7 +3398,7 @@ func TestDriverOpen(t *testing.T) { DataDir: tmpDir, Net: dex.Testnet, Logger: logger, - }, true) + }, &testnetCompatibilityData, true) if err != nil { t.Fatalf("CreateWallet error: %v", err) } @@ -3445,7 +3460,7 @@ func TestDriverExists(t *testing.T) { DataDir: tmpDir, Net: dex.Simnet, Logger: tLogger, - }, true) + }, &testnetCompatibilityData, true) if err != nil { t.Fatalf("CreateEVMWallet error: %v", err) } @@ -3752,21 +3767,23 @@ func testRefundReserves(t *testing.T, assetID uint32) { gasesV0 := dexeth.VersionedGases[0] gasesV1 := &dexeth.Gases{Refund: 1e6} assetV0 := *tETH + assetV1 := *tETH if assetID == BipID { - dexeth.VersionedGases[1] = gasesV1 - defer delete(dexeth.VersionedGases, 1) + eth.versionedGases[1] = gasesV1 } else { feeWallet = node.tokenParent assetV0 = *tToken assetV1 = *tToken - tokenContracts := dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts - gasesV0 = &tokenGases + tokenContracts := eth.tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts tc := *tokenContracts[0] tc.Gas = *gasesV1 tokenContracts[1] = &tc - node.tokenContractor.bal = dexeth.GweiToWei(1e9) defer delete(tokenContracts, 1) + gasesV0 = &tokenGases + eth.versionedGases[0] = gasesV0 + eth.versionedGases[1] = gasesV1 + node.tokenContractor.bal = dexeth.GweiToWei(1e9) } assetV0.MaxFeeRate = 45 @@ -3852,19 +3869,15 @@ func testRedemptionReserves(t *testing.T, assetID uint32) { assetV1 := *tETH feeWallet := eth if assetID == BipID { - dexeth.VersionedGases[1] = gasesV1 - defer delete(dexeth.VersionedGases, 1) + eth.versionedGases[1] = gasesV1 } else { node.tokenContractor.allow = unlimitedAllowanceReplenishThreshold feeWallet = node.tokenParent assetV0 = *tToken assetV1 = *tToken - tokenContracts := dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts gasesV0 = &tokenGases - tc := *tokenContracts[0] - tc.Gas = *gasesV1 - tokenContracts[1] = &tc - defer delete(tokenContracts, 1) + eth.versionedGases[0] = gasesV0 + eth.versionedGases[1] = gasesV1 } assetV0.MaxFeeRate = 45 @@ -4997,7 +5010,7 @@ func testEstimateSendTxFee(t *testing.T, assetID uint32) { } } else { if estimate != tokenFees { - t.Fatalf("%s: expected fees to be %v, got %v", test.name, ethFees, estimate) + t.Fatalf("%s: expected fees to be %v, got %v", test.name, tokenFees, estimate) } } if err != nil { @@ -5027,7 +5040,7 @@ func testMaxSwapRedeemLots(t *testing.T, assetID uint32) { DataDir: tmpDir, Net: dex.Testnet, Logger: logger, - }, true) + }, &testnetCompatibilityData, true) if err != nil { t.Fatalf("CreateEVMWallet error: %v", err) } diff --git a/client/asset/eth/genesis_config.go b/client/asset/eth/genesis_config.go deleted file mode 100644 index 2e810bd27e..0000000000 --- a/client/asset/eth/genesis_config.go +++ /dev/null @@ -1,65 +0,0 @@ -package eth - -import ( - "fmt" - "path/filepath" - - "decred.org/dcrdex/dex" - dexeth "decred.org/dcrdex/dex/networks/eth" - dexpolygon "decred.org/dcrdex/dex/networks/polygon" - ethCore "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/eth/ethconfig" -) - -// chainConfig returns chain config for Ethereum and Ethereum compatible chains -// (Polygon). -// NOTE: Other consensus engines (bor) are not considered in ethconfig.Config -// but this is not an issue because there have not been an obvious use for it. -func chainConfig(chainID int64, network dex.Network) (c ethconfig.Config, err error) { - if network == dex.Simnet { - c.Genesis, err = readSimnetGenesisFile(chainID) - if err != nil { - return c, fmt.Errorf("readSimnetGenesisFile error: %w", err) - } - c.NetworkId = c.Genesis.Config.ChainID.Uint64() - return c, nil - } - - cfg := ethconfig.Defaults - switch chainID { - // Ethereum - case dexeth.TestnetChainID: - cfg.Genesis = ethCore.DefaultGoerliGenesisBlock() - case dexeth.MainnetChainID: - cfg.Genesis = ethCore.DefaultGenesisBlock() - - // Polygon - case dexpolygon.MainnetChainID: - cfg.Genesis = dexpolygon.DefaultBorMainnetGenesisBlock() - case dexpolygon.TestnetChainID: - cfg.Genesis = dexpolygon.DefaultMumbaiGenesisBlock() - default: - return c, fmt.Errorf("unknown chain ID: %d", chainID) - } - - cfg.NetworkId = cfg.Genesis.Config.ChainID.Uint64() - - return cfg, nil -} - -// readSimnetGenesisFile reads the simnet genesis file for the wallet with the -// specified chainID. -func readSimnetGenesisFile(chainID int64) (*ethCore.Genesis, error) { - dataDir, err := simnetDataDir(chainID) - if err != nil { - return nil, err - } - - genesisFile := filepath.Join(dataDir, "genesis.json") - genesisCfg, err := dexeth.LoadGenesisFile(genesisFile) - if err != nil { - return nil, fmt.Errorf("error reading genesis file: %v", err) - } - - return genesisCfg, nil -} diff --git a/client/asset/eth/multirpc.go b/client/asset/eth/multirpc.go index d23a748bb1..25ca6be463 100644 --- a/client/asset/eth/multirpc.go +++ b/client/asset/eth/multirpc.go @@ -27,7 +27,6 @@ import ( "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/networks/erc20" dexeth "decred.org/dcrdex/dex/networks/eth" - dexpolygon "decred.org/dcrdex/dex/networks/polygon" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -55,14 +54,25 @@ const ( tipCapSuggestionExpiration = time.Hour brickedFailCount = 100 providerDelimiter = " " - defaultRequestTimeout = time.Second * 10 + // Infura and Rivet (basic plans) seem to have a 15 second delay for 1) + // initializing websocket connection, or 2) the first eth_chainId request on + // HTTPS, but not for other requests. + // TODO: Keep a file mapping provider URL to retrieved chain IDs, and skip + // the eth_chainId request after verified for the first time? + defaultRequestTimeout = time.Second * 20 ) -// nonceProviderStickiness is the minimum amount of time that must pass between -// requests to DIFFERENT nonce providers. If we use a provider for a -// nonce-sensitive (NS) operation, and later have another NS operation, we will -// use the same provider if < nonceProviderStickiness has passed. -var nonceProviderStickiness = time.Minute +var ( + // nonceProviderStickiness is the minimum amount of time that must pass + // between requests to DIFFERENT nonce providers. If we use a provider for a + // nonce-sensitive (NS) operation, and later have another NS operation, we + // will use the same provider if < nonceProviderStickiness has passed. + nonceProviderStickiness = time.Minute + // By default, connectProviders will attempt to get a WebSockets endpoint + // when given an HTTP(S) provider URL. Can be disabled for testing + // ((*MRPCTest).TestRPC). + forceTryWS = true +) // TODO: Handle rate limiting? From the docs: // When you are rate limited, your JSON-RPC responses have HTTP Status code 429. @@ -380,7 +390,7 @@ type multiRPCClient struct { var _ ethFetcher = (*multiRPCClient)(nil) -func newMultiRPCClient(dir string, endpoints []string, log dex.Logger, cfg *params.ChainConfig, chainID *big.Int, net dex.Network) (*multiRPCClient, error) { +func newMultiRPCClient(dir string, endpoints []string, log dex.Logger, cfg *params.ChainConfig, net dex.Network) (*multiRPCClient, error) { walletDir := getWalletDir(dir, net) creds, err := pathCredentials(filepath.Join(walletDir, "keystore")) if err != nil { @@ -392,7 +402,7 @@ func newMultiRPCClient(dir string, endpoints []string, log dex.Logger, cfg *para cfg: cfg, log: log, creds: creds, - chainID: chainID, + chainID: cfg.ChainID, endpoints: endpoints, } m.receipts.cache = make(map[common.Hash]*receiptRecord) @@ -417,8 +427,13 @@ func connectProviders(ctx context.Context, endpoints []string, log dex.Logger, c } }() + // We'll connect concurrently, and cancel the context on the first error + // received. + ctx, cancel := context.WithCancel(ctx) + defer cancel() + // addEndpoint only returns errors that should be propagated immediately. - addEndpoint := func(endpoint string) error { + addEndpoint := func(endpoint string) (*provider, error) { // Give ourselves a limited time to resolve a connection. timedCtx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) defer cancel() @@ -432,12 +447,18 @@ func connectProviders(ctx context.Context, endpoints []string, log dex.Logger, c var wsSubscribed bool var h chan *types.Header host := providerIPC - if !strings.HasSuffix(endpoint, ".ipc") { - wsURL, err := url.Parse(endpoint) + var scratchURL *url.URL + isIPC := strings.HasSuffix(endpoint, ".ipc") + if !isIPC { + var err error + scratchURL, err = url.Parse(endpoint) if err != nil { - return fmt.Errorf("failed to parse url %q: %w", endpoint, err) + return nil, fmt.Errorf("failed to parse url %q: %w", endpoint, err) } - host = wsURL.Host + host = scratchURL.Host + } + if forceTryWS && !isIPC { + wsURL := scratchURL ogScheme := wsURL.Scheme switch ogScheme { case "https": @@ -446,7 +467,7 @@ func connectProviders(ctx context.Context, endpoints []string, log dex.Logger, c wsURL.Scheme = "ws" case "ws", "wss": default: - return fmt.Errorf("unknown scheme for endpoint %q: %q, expected any of: ws(s)/http(s)", + return nil, fmt.Errorf("unknown scheme for endpoint %q: %q, expected any of: ws(s)/http(s)", endpoint, wsURL.Scheme) } replaced := ogScheme != wsURL.Scheme @@ -463,7 +484,11 @@ func connectProviders(ctx context.Context, endpoints []string, log dex.Logger, c host = providerRivetCloud } + // Some providers appear to meter websocket connections. + var err error + startTime := time.Now() rpcClient, err = rpc.DialWebsocket(timedCtx, wsURL.String(), "") + log.Tracef("%s to connect to %s", time.Since(startTime), wsURL) if err == nil { ec = ethclient.NewClient(rpcClient) h = make(chan *types.Header, 8) @@ -491,26 +516,30 @@ func connectProviders(ctx context.Context, endpoints []string, log dex.Logger, c // path discrimination, so I won't even try to validate the protocol. if ec == nil { var err error + startTime := time.Now() rpcClient, err = rpc.DialContext(timedCtx, endpoint) + log.Tracef("%s to connect to %s", time.Since(startTime), endpoint) if err != nil { log.Errorf("error creating http client for %q: %v", endpoint, err) - return nil + return nil, nil } ec = ethclient.NewClient(rpcClient) } - // Get chain ID. + // Chain ID seems to be metered for some providers. + startTime := time.Now() reportedChainID, err := ec.ChainID(timedCtx) + log.Tracef("%s to fetch chain ID for verification at %s", time.Since(startTime), host) if err != nil { // If we can't get a header, don't use this provider. ec.Close() log.Errorf("Failed to get chain ID from %q: %v", endpoint, err) - return nil + return nil, nil } if chainID.Cmp(reportedChainID) != 0 { ec.Close() log.Errorf("%q reported wrong chain ID. expected %d, got %d", endpoint, chainID, reportedChainID) - return nil + return nil, nil } hdr, err := ec.HeaderByNumber(timedCtx, nil /* latest */) @@ -518,7 +547,7 @@ func connectProviders(ctx context.Context, endpoints []string, log dex.Logger, c // If we can't get a header, don't use this provider. ec.Close() log.Errorf("Failed to get header from %q: %v", endpoint, err) - return nil + return nil, nil } p := &provider{ @@ -556,19 +585,48 @@ func connectProviders(ctx context.Context, endpoints []string, log dex.Logger, c wg.Wait() } - providers = append(providers, p) - - return nil + return p, nil } + type addResult struct { + provider *provider + err error + endpoint string + } + resultChan := make(chan *addResult) for _, endpoint := range endpoints { - if err := addEndpoint(endpoint); err != nil { - return nil, err + go func(endpoint string) { + p, err := addEndpoint(endpoint) + resultChan <- &addResult{p, err, endpoint} + }(endpoint) + } + var resCount int + var err error +out: + for { + select { + case res := <-resultChan: + resCount++ + if res.provider != nil { + providers = append(providers, res.provider) + } + if res.err != nil && err == nil { + err = res.err + cancel() + } + if resCount == len(endpoints) { + break out + } + case <-ctx.Done(): } } + if err != nil { + return nil, err + } + if len(providers) == 0 { - return nil, fmt.Errorf("failed to connect to even single provider among: %s", + return nil, fmt.Errorf("failed to connect to even a single provider among: %s", failedProviders(providers, endpoints)) } @@ -652,8 +710,9 @@ func (m *multiRPCClient) voidUnusedNonce() { // createAndCheckProviders creates and connects to providers. It checks that // unknown providers have a sufficient api to trade and saves good providers to // file. One bad provider or connect problem will cause this to error. -func createAndCheckProviders(ctx context.Context, walletDir string, endpoints []string, chainID *big.Int, net dex.Network, - log dex.Logger) error { +func createAndCheckProviders(ctx context.Context, walletDir string, endpoints []string, chainID *big.Int, + compat *CompatibilityData, net dex.Network, log dex.Logger) error { + var localCP map[string]bool path := filepath.Join(walletDir, "compliant-providers.json") b, err := os.ReadFile(path) @@ -703,7 +762,7 @@ func createAndCheckProviders(ctx context.Context, walletDir string, endpoints [] return fmt.Errorf("expected to successfully connect to all of these unfamiliar providers: %s", failedProviders(providers, unknownEndpoints)) } - if err := checkProvidersCompliance(ctx, providers, dex.Disabled /* logger is for testing only */); err != nil { + if err := checkProvidersCompliance(ctx, providers, compat, dex.Disabled /* logger is for testing only */); err != nil { return err } } @@ -739,15 +798,16 @@ func failedProviders(succeeded []*provider, tried []string) string { return strings.Join(notOK, " ") } -func (m *multiRPCClient) reconfigure(ctx context.Context, settings map[string]string, walletDir string) error { +func (m *multiRPCClient) reconfigure(ctx context.Context, settings map[string]string, compat *CompatibilityData, walletDir string) error { providerDef := settings[providersKey] if len(providerDef) == 0 { return errors.New("no providers specified") } endpoints := strings.Split(providerDef, " ") - if err := createAndCheckProviders(ctx, walletDir, endpoints, m.chainID, m.net, m.log); err != nil { + if err := createAndCheckProviders(ctx, walletDir, endpoints, m.chainID, compat, m.net, m.log); err != nil { return fmt.Errorf("create and check providers: %v", err) } + // TODO: If endpoints haven't change, do nothing. providers, err := connectProviders(ctx, endpoints, m.log, m.chainID, m.net) if err != nil { return err @@ -1519,67 +1579,9 @@ type rpcTest struct { // newCompatibilityTests returns a list of RPC tests to run to determine API // compatibility. -func newCompatibilityTests(cb bind.ContractBackend, chainID *big.Int, net dex.Network, log dex.Logger) []*rpcTest { - // NOTE: The logger is intended for use the execution of the compatibility - // tests, and it will generally be dex.Disabled in production. - var ( - // Vitalik's address from https://twitter.com/VitalikButerin/status/1050126908589887488 - mainnetAddr = common.HexToAddress("0xab5801a7d398351b8be11c439e05c5b3259aec9b") - mainnetUSDC = common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") - mainnetTxHash = common.HexToHash("0xea1a717af9fad5702f189d6f760bb9a5d6861b4ee915976fe7732c0c95cd8a0e") - mainnetBlockHash = common.HexToHash("0x44ebd6f66b4fd546bccdd700869f6a433ef9a47e296a594fa474228f86eeb353") - - testnetAddr = common.HexToAddress("0x8879F72728C5eaf5fB3C55e6C3245e97601FBa32") - testnetUSDC = common.HexToAddress("0x07865c6E87B9F70255377e024ace6630C1Eaa37F") - testnetTxHash = common.HexToHash("0x4e1d455f7eac7e3a5f7c1e0989b637002755eaee3a262f90b0f3aef1f1c4dcf0") - testnetBlockHash = common.HexToHash("0x8896021c2666303a85b7e4a6a6f2b075bc705d4e793bf374cd44b83bca23ef9a") - - simnetAddr = common.HexToAddress("18d65fb8d60c1199bb1ad381be47aa692b482605") - addr, usdc common.Address - txHash, blockHash common.Hash - ) - - switch net { - case dex.Mainnet: - addr = mainnetAddr - usdc = mainnetUSDC - txHash = mainnetTxHash - blockHash = mainnetBlockHash - case dex.Testnet: - addr = testnetAddr - usdc = testnetUSDC - txHash = testnetTxHash - blockHash = testnetBlockHash - case dex.Simnet: - if big.NewInt(dexpolygon.SimnetChainID).Cmp(chainID) == 0 { - break // TODO: add simnet tests for polygon this will require the ~/dextest/polygon dir to be populated with the files below. - } - - tDir, err := simnetDataDir(chainID.Int64()) - if err != nil { - panic(fmt.Sprintf("Problem getting simnet data dir: %v", err)) - } - - addr = simnetAddr - var ( - tTxHashFile = filepath.Join(tDir, "test_tx_hash.txt") - tBlockHashFile = filepath.Join(tDir, "test_block10_hash.txt") - tContractFile = filepath.Join(tDir, "test_token_contract_address.txt") - ) - readIt := func(path string) string { - b, err := os.ReadFile(path) - if err != nil { - panic(fmt.Sprintf("Problem reading simnet testing file %q: %v", path, err)) - } - return strings.TrimSpace(string(b)) // mainly the trailing "\r\n" - } - usdc = common.HexToAddress(readIt(tContractFile)) - txHash = common.HexToHash(readIt(tTxHashFile)) - blockHash = common.HexToHash(readIt(tBlockHashFile)) - default: // caller should have checked though - panic(fmt.Sprintf("Unknown net %v in compatibility tests. Testing data not initiated.", net)) - } - +// NOTE: The logger is intended for use the execution of the compatibility +// tests, and it will generally be dex.Disabled in production. +func newCompatibilityTests(cb bind.ContractBackend, compat *CompatibilityData, log dex.Logger) []*rpcTest { return []*rpcTest{ { name: "HeaderByNumber", @@ -1591,21 +1593,21 @@ func newCompatibilityTests(cb bind.ContractBackend, chainID *big.Int, net dex.Ne { name: "HeaderByHash", f: func(ctx context.Context, p *provider) error { - _, err := p.ec.HeaderByHash(ctx, blockHash) + _, err := p.ec.HeaderByHash(ctx, compat.BlockHash) return err }, }, { name: "TransactionReceipt", f: func(ctx context.Context, p *provider) error { - _, err := p.ec.TransactionReceipt(ctx, txHash) + _, err := p.ec.TransactionReceipt(ctx, compat.TxHash) return err }, }, { name: "PendingNonceAt", f: func(ctx context.Context, p *provider) error { - _, err := p.ec.PendingNonceAt(ctx, addr) + _, err := p.ec.PendingNonceAt(ctx, compat.Addr) return err }, }, @@ -1623,7 +1625,7 @@ func newCompatibilityTests(cb bind.ContractBackend, chainID *big.Int, net dex.Ne { name: "BalanceAt", f: func(ctx context.Context, p *provider) error { - bal, err := p.ec.BalanceAt(ctx, addr, nil) + bal, err := p.ec.BalanceAt(ctx, compat.Addr, nil) if err != nil { return err } @@ -1634,7 +1636,11 @@ func newCompatibilityTests(cb bind.ContractBackend, chainID *big.Int, net dex.Ne { name: "CodeAt", f: func(ctx context.Context, p *provider) error { - code, err := p.ec.CodeAt(ctx, usdc, nil) + if compat.TokenAddr == (common.Address{}) { + log.Debug("#### Skipping CodeAt. No token address provided") + return nil + } + code, err := p.ec.CodeAt(ctx, compat.TokenAddr, nil) if err != nil { return err } @@ -1645,14 +1651,18 @@ func newCompatibilityTests(cb bind.ContractBackend, chainID *big.Int, net dex.Ne { name: "CallContract(balanceOf)", f: func(ctx context.Context, p *provider) error { - caller, err := erc20.NewIERC20(usdc, cb) + if compat.TokenAddr == (common.Address{}) { + log.Debug("#### Skipping CallContract. No token address provided") + return nil + } + caller, err := erc20.NewIERC20(compat.TokenAddr, cb) if err != nil { return err } bal, err := caller.BalanceOf(&bind.CallOpts{ - From: addr, + From: compat.Addr, Context: ctx, - }, addr) + }, compat.Addr) if err != nil { return err } @@ -1677,7 +1687,7 @@ func newCompatibilityTests(cb bind.ContractBackend, chainID *big.Int, net dex.Ne { name: "getRPCTransaction", f: func(ctx context.Context, p *provider) error { - rpcTx, err := getRPCTransaction(ctx, p, txHash) + rpcTx, err := getRPCTransaction(ctx, p, compat.TxHash) if err != nil { return err } @@ -1763,15 +1773,15 @@ func domain(addr string) (string, error) { // requires by sending a series of requests and verifying the responses. If a // provider is found to be compliant, their domain name is added to a list and // stored in a file on disk so that future checks can be short-circuited. -func checkProvidersCompliance(ctx context.Context, providers []*provider, log dex.Logger) error { +func checkProvidersCompliance(ctx context.Context, providers []*provider, compat *CompatibilityData, log dex.Logger) error { for _, p := range providers { // Need to run API tests on this endpoint. - for _, t := range newCompatibilityTests(p.ec, p.chainID, p.net, log) { + for _, t := range newCompatibilityTests(p.ec, compat, log) { ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) err := t.f(ctx, p) cancel() if err != nil { - return fmt.Errorf("RPC Provider @ %q has a non-compliant API: %v", p.host, err) + return fmt.Errorf("%s: RPC Provider @ %q has a non-compliant API: %v", t.name, p.host, err) } } } diff --git a/client/asset/eth/multirpc_live_test.go b/client/asset/eth/multirpc_live_test.go index 3149d30515..3cdecc2ccb 100644 --- a/client/asset/eth/multirpc_live_test.go +++ b/client/asset/eth/multirpc_live_test.go @@ -4,277 +4,55 @@ package eth import ( "context" - "encoding/json" - "fmt" - "math/big" - "math/rand" "os" - "os/exec" - "path/filepath" - "strings" "testing" - "time" - "decred.org/dcrdex/client/asset" "decred.org/dcrdex/dex" - "decred.org/dcrdex/dex/encode" - dexeth "decred.org/dcrdex/dex/networks/eth" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/eth/ethconfig" - "github.com/ethereum/go-ethereum/params" ) const ( - deltaHTTPPort = "38556" - deltaWSPort = "38557" + alphaHTTPPort = "38556" + alphaWSPort = "38557" ) -var ( - dextestDir = filepath.Join(os.Getenv("HOME"), "dextest") - harnessDir = filepath.Join(dextestDir, "eth", "harness-ctl") - ctx = context.Background() -) - -func harnessCmd(ctx context.Context, exe string, args ...string) (string, error) { - cmd := exec.CommandContext(ctx, exe, args...) - cmd.Dir = harnessDir - op, err := cmd.CombinedOutput() - return string(op), err -} - -func mine(ctx context.Context) error { - _, err := harnessCmd(ctx, "./mine-alpha", "1") - return err -} - -func testEndpoint(endpoints []string, syncBlocks uint64, tFunc func(context.Context, *multiRPCClient)) error { - dir, _ := os.MkdirTemp("", "") - defer os.RemoveAll(dir) - - cl, err := tRPCClient(dir, encode.RandomBytes(32), endpoints, dex.Simnet, false) - if err != nil { - return err - } - - fmt.Println("######## Address:", cl.creds.addr) +var mt *MRPCTest - if err := cl.connect(ctx); err != nil { - return fmt.Errorf("connect error: %v", err) +func TestMain(m *testing.M) { + ctx, shutdown := context.WithCancel(context.Background()) + mt = NewMRPCTest(ctx, ChainConfig, NetworkCompatibilityData, "eth") + doIt := func() int { + defer shutdown() + return m.Run() } - - startHdr, err := cl.bestHeader(ctx) - if err != nil { - return fmt.Errorf("error getting initial header: %v", err) - } - - // mine headers - start := time.Now() - for { - mine(ctx) - hdr, err := cl.bestHeader(ctx) - if err != nil { - return fmt.Errorf("error getting best header: %v", err) - } - blocksMined := hdr.Number.Uint64() - startHdr.Number.Uint64() - if blocksMined > syncBlocks { - break - } - if time.Since(start) > time.Minute { - return fmt.Errorf("timed out") - } - select { - case <-time.After(time.Second * 5): - // mine and check again - case <-ctx.Done(): - return context.Canceled - } - } - - if tFunc != nil { - tFunc(ctx, cl) - } - - time.Sleep(time.Second) - - return nil + os.Exit(doIt()) } func TestHTTP(t *testing.T) { - if err := testEndpoint([]string{"http://localhost:" + deltaHTTPPort}, 2, nil); err != nil { - t.Fatal(err) - } + mt.TestHTTP(t, alphaHTTPPort) } func TestWS(t *testing.T) { - if err := testEndpoint([]string{"ws://localhost:" + deltaWSPort}, 2, nil); err != nil { - t.Fatal(err) - } + mt.TestWS(t, alphaWSPort) } func TestWSTxLogs(t *testing.T) { - if err := testEndpoint([]string{"ws://localhost:" + deltaWSPort}, 2, func(ctx context.Context, cl *multiRPCClient) { - for i := 0; i < 3; i++ { - time.Sleep(time.Second) - harnessCmd(ctx, "./sendtoaddress", cl.creds.addr.String(), "1") - mine(ctx) - - } - bal, _ := cl.addressBalance(ctx, cl.creds.addr) - fmt.Println("Balance after", bal) - }); err != nil { - t.Fatal(err) - } + mt.TestWSTxLogs(t, alphaWSPort) } func TestSimnetMultiRPCClient(t *testing.T) { - endpoints := []string{ - "ws://localhost:" + deltaWSPort, - "http://localhost:" + deltaHTTPPort, - } - - nonceProviderStickiness = time.Second / 2 - - rand.Seed(time.Now().UnixNano()) - - if err := testEndpoint(endpoints, 2, func(ctx context.Context, cl *multiRPCClient) { - // Get some funds - harnessCmd(ctx, "./sendtoaddress", cl.creds.addr.String(), "3") - time.Sleep(time.Second) - mine(ctx) - time.Sleep(time.Second) - - if err := cl.unlock("abc"); err != nil { - t.Fatalf("error unlocking: %v", err) - } - - const amt = 1e8 // 0.1 ETH - var alphaAddr = common.HexToAddress("0x18d65fb8d60c1199bb1ad381be47aa692b482605") - - for i := 0; i < 10; i++ { - txOpts, err := cl.txOpts(ctx, amt, defaultSendGasLimit, nil, nil) - if err != nil { - t.Fatal(err) - } - // Send two in a row. They should use each provider, preferred first. - for j := 0; j < 2; j++ { - if _, err := cl.sendTransaction(ctx, txOpts, alphaAddr, nil); err != nil { - t.Fatalf("error sending tx %d-%d: %v", i, j, err) - } - } - _, err = cl.bestHeader(ctx) - // Let nonce provider expire. The pair should use the same - // provider, but different pairs can use different providers. Look - // for the variation. - mine(ctx) - time.Sleep(time.Second) - } - }); err != nil { - t.Fatal(err) - } -} - -// -// Create a providers.json file in your .dexc directory. -// 1. Seed can be anything. Just generate randomness. -// 2. Can connect to a host's websocket and http endpoints simultaneously. -// Actually nothing preventing you from connecting to a single provider -// 100 times, but that may be a guardrail added in the future. -// -// Example ~/.dexc/providers.json -/* -{ - "testnet": { - "seed": "9e0084387c3ba7ac4b5bb409c220c08d4ee74f7b8c73b03fff18c727c5ce9f47", - "providers": [ - "https://goerli.infura.io/v3/", - "https://.eth.rpc.rivet.cloud", - "https://eth-goerli.g.alchemy.com/v2/-" - ] - }, - "mainnet": { - "seed": "9e0084387c3ba7ac4b5bb409c220c08d4ee74f7b8c73b03fff18c727c5ce9f47", - "providers": [ - "wss://mainnet.infura.io/ws/v3/", - "https://.eth.rpc.rivet.cloud", - "https://eth-mainnet.g.alchemy.com/v2/" - ] - } -} -*/ - -type tProvider struct { - Seed dex.Bytes `json:"seed"` - Providers []string `json:"providers"` -} - -func readProviderFile(t *testing.T, net dex.Network) *tProvider { - t.Helper() - providersFile := filepath.Join(dextestDir, "providers.json") - b, err := os.ReadFile(providersFile) - if err != nil { - t.Fatalf("ReadFile(%q) error: %v", providersFile, err) - } - var accounts map[string]*tProvider - if err := json.Unmarshal(b, &accounts); err != nil { - t.Fatal(err) - } - accts := accounts[net.String()] - if accts == nil { - t.Fatalf("no") - } - return accts + mt.TestSimnetMultiRPCClient(t, alphaWSPort, alphaHTTPPort) } func TestMonitorTestnet(t *testing.T) { - testMonitorNet(t, dex.Testnet) + mt.TestMonitorNet(t, dex.Testnet) } func TestMonitorMainnet(t *testing.T) { - testMonitorNet(t, dex.Mainnet) -} - -func testMonitorNet(t *testing.T, net dex.Network) { - providerFile := readProviderFile(t, net) - dir, _ := os.MkdirTemp("", "") - - cl, err := tRPCClient(dir, providerFile.Seed, providerFile.Providers, net, true) - if err != nil { - t.Fatal(err) - } - - ctx, cancel := context.WithTimeout(ctx, time.Hour) - defer cancel() - - if err := cl.connect(ctx); err != nil { - t.Fatalf("Connection error: %v", err) - } - <-ctx.Done() + mt.TestMonitorNet(t, dex.Mainnet) } func TestRPC(t *testing.T) { - endpoint := os.Getenv("PROVIDER") - if endpoint == "" { - t.Fatalf("specify a provider in the PROVIDER environmental variable") - } - dir, _ := os.MkdirTemp("", "") - defer os.RemoveAll(dir) - cl, err := tRPCClient(dir, encode.RandomBytes(32), []string{endpoint}, dex.Mainnet, true) - if err != nil { - t.Fatal(err) - } - - if err := cl.connect(ctx); err != nil { - t.Fatalf("connect error: %v", err) - } - - for _, tt := range newCompatibilityTests(cl, big.NewInt(dexeth.ChainIDs[dex.Mainnet]), dex.Mainnet, cl.log) { - tStart := time.Now() - if err := cl.withAny(ctx, tt.f); err != nil { - t.Fatalf("%q: %v", tt.name, err) - } - fmt.Printf("### %q: %s \n", tt.name, time.Since(tStart)) - } + mt.TestRPC(t, dex.Mainnet) } var freeServers = []string{ @@ -288,94 +66,9 @@ var freeServers = []string{ } func TestFreeServers(t *testing.T) { - runTest := func(endpoint string) error { - dir, _ := os.MkdirTemp("", "") - defer os.RemoveAll(dir) - cl, err := tRPCClient(dir, encode.RandomBytes(32), []string{endpoint}, dex.Mainnet, true) - if err != nil { - return fmt.Errorf("tRPCClient error: %v", err) - } - if err := cl.connect(ctx); err != nil { - return fmt.Errorf("connect error: %v", err) - } - for _, tt := range newCompatibilityTests(cl, big.NewInt(dexeth.ChainIDs[dex.Mainnet]), dex.Mainnet, cl.log) { - if err := cl.withAny(ctx, tt.f); err != nil { - return fmt.Errorf("%q error: %v", tt.name, err) - } - fmt.Printf("#### %q passed %q \n", endpoint, tt.name) - } - return nil - } - - passes, fails := make([]string, 0), make(map[string]error, 0) - for _, endpoint := range freeServers { - if err := runTest(endpoint); err != nil { - fails[endpoint] = err - } else { - passes = append(passes, endpoint) - } - } - for _, pass := range passes { - fmt.Printf("!!!! %q PASSED \n", pass) - } - for endpoint, err := range fails { - fmt.Printf("XXXX %q FAILED : %v \n", endpoint, err) - } + mt.TestFreeServers(t, freeServers, dex.Testnet) } func TestMainnetCompliance(t *testing.T) { - providerFile := readProviderFile(t, dex.Mainnet) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - log := dex.StdOutLogger("T", dex.LevelTrace) - providers, err := connectProviders(ctx, providerFile.Providers, log, big.NewInt(dexeth.ChainIDs[dex.Mainnet]), dex.Mainnet) - if err != nil { - t.Fatal(err) - } - err = checkProvidersCompliance(ctx, providers, log) - if err != nil { - t.Fatal(err) - } -} - -func tRPCClient(dir string, seed []byte, endpoints []string, net dex.Network, skipConnect bool) (*multiRPCClient, error) { - wDir := getWalletDir(dir, net) - err := os.MkdirAll(wDir, 0755) - if err != nil { - return nil, fmt.Errorf("os.Mkdir error: %w", err) - } - - log := dex.StdOutLogger("T", dex.LevelTrace) - - chainID := dexeth.ChainIDs[net] - if err := CreateEVMWallet(chainID, &asset.CreateWalletParams{ - Type: walletTypeRPC, - Seed: seed, - Pass: []byte("abc"), - Settings: map[string]string{ - "providers": strings.Join(endpoints, " "), - }, - DataDir: dir, - Net: net, - Logger: dex.StdOutLogger("T", dex.LevelTrace), - }, skipConnect); err != nil { - return nil, fmt.Errorf("error creating wallet: %v", err) - } - - var c ethconfig.Config - switch net { - case dex.Mainnet: - c = ethconfig.Defaults - c.Genesis = core.DefaultGenesisBlock() - c.NetworkId = params.MainnetChainConfig.ChainID.Uint64() - default: - c, err = chainConfig(chainID, net) - if err != nil { - return nil, fmt.Errorf("error getting chain config: %v", err) - } - c.NetworkId = c.Genesis.Config.ChainID.Uint64() - } - - return newMultiRPCClient(dir, endpoints, log, c.Genesis.Config, big.NewInt(chainID), net) + mt.TestMainnetCompliance(t) } diff --git a/client/asset/eth/multirpc_test_util.go b/client/asset/eth/multirpc_test_util.go new file mode 100644 index 0000000000..4901b93f8c --- /dev/null +++ b/client/asset/eth/multirpc_test_util.go @@ -0,0 +1,363 @@ +// //go:build rpclive + +package eth + +import ( + "context" + "flag" + "fmt" + "math/rand" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "decred.org/dcrdex/client/asset" + "decred.org/dcrdex/dex" + "decred.org/dcrdex/dex/encode" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +type MRPCTest struct { + ctx context.Context + chain string + chainConfigLookup func(net dex.Network) (*params.ChainConfig, error) + compatDataLookup func(net dex.Network) (CompatibilityData, error) + harnessDirectory string + credentialsFile string +} + +// NewMRPCTest creates a new MRPCTest. +// Create a credntials.json file in your ~/dextest directory. +// See README for getgas for format +func NewMRPCTest( + ctx context.Context, + cfgLookup func(net dex.Network) (*params.ChainConfig, error), + compatLookup func(net dex.Network) (c CompatibilityData, err error), + chainSymbol string, +) *MRPCTest { + + var skipWS bool + flag.BoolVar(&skipWS, "skipws", false, "skip attempt to automatically resolve WebSocket URL from HTTP(S) URL") + flag.Parse() + if skipWS { + forceTryWS = false + } + + dextestDir := filepath.Join(os.Getenv("HOME"), "dextest") + return &MRPCTest{ + ctx: ctx, + chain: chainSymbol, + chainConfigLookup: cfgLookup, + compatDataLookup: compatLookup, + harnessDirectory: filepath.Join(dextestDir, chainSymbol, "harness-ctl"), + credentialsFile: filepath.Join(dextestDir, "credentials.json"), + } +} + +func (m *MRPCTest) rpcClient(dir string, seed []byte, endpoints []string, net dex.Network, skipConnect bool) (*multiRPCClient, error) { + wDir := getWalletDir(dir, net) + err := os.MkdirAll(wDir, 0755) + if err != nil { + return nil, fmt.Errorf("os.Mkdir error: %w", err) + } + + log := dex.StdOutLogger("T", dex.LevelTrace) + + cfg, err := m.chainConfigLookup(net) + if err != nil { + return nil, fmt.Errorf("chainConfigLookup error: %v", err) + } + + compat, err := m.compatDataLookup(net) + if err != nil { + return nil, fmt.Errorf("compatDataLookup error: %v", err) + } + + chainID := cfg.ChainID.Int64() + if err := CreateEVMWallet(chainID, &asset.CreateWalletParams{ + Type: walletTypeRPC, + Seed: seed, + Pass: []byte("abc"), + Settings: map[string]string{ + "providers": strings.Join(endpoints, " "), + }, + DataDir: dir, + Net: net, + Logger: dex.StdOutLogger("T", dex.LevelTrace), + }, &compat, skipConnect); err != nil { + return nil, fmt.Errorf("error creating wallet: %v", err) + } + + return newMultiRPCClient(dir, endpoints, log, cfg, net) +} + +func (m *MRPCTest) TestHTTP(t *testing.T, port string) { + if err := m.testSimnetEndpoint([]string{"http://localhost:" + port}, 2, nil); err != nil { + t.Fatal(err) + } +} + +func (m *MRPCTest) TestWS(t *testing.T, port string) { + if err := m.testSimnetEndpoint([]string{"ws://localhost:" + port}, 2, nil); err != nil { + t.Fatal(err) + } +} + +func (m *MRPCTest) TestWSTxLogs(t *testing.T, port string) { + if err := m.testSimnetEndpoint([]string{"ws://localhost:" + port}, 2, func(ctx context.Context, cl *multiRPCClient) { + for i := 0; i < 3; i++ { + time.Sleep(time.Second) + m.harnessCmd(ctx, "./sendtoaddress", cl.creds.addr.String(), "1") + m.mine(ctx) + + } + bal, _ := cl.addressBalance(ctx, cl.creds.addr) + fmt.Println("Balance after", bal) + }); err != nil { + t.Fatal(err) + } +} + +func (m *MRPCTest) TestSimnetMultiRPCClient(t *testing.T, wsPort, httpPort string) { + endpoints := []string{ + "ws://localhost:" + wsPort, + "http://localhost:" + httpPort, + } + + nonceProviderStickiness = time.Second / 2 + + rand.Seed(time.Now().UnixNano()) + + if err := m.testSimnetEndpoint(endpoints, 2, func(ctx context.Context, cl *multiRPCClient) { + // Get some funds + m.harnessCmd(ctx, "./sendtoaddress", cl.creds.addr.String(), "3") + + time.Sleep(time.Second) + m.mine(ctx) + time.Sleep(time.Second) + + if err := cl.unlock("abc"); err != nil { + t.Fatalf("error unlocking: %v", err) + } + + const amt = 1e8 // 0.1 ETH + var alphaAddr = common.HexToAddress("0x18d65fb8d60c1199bb1ad381be47aa692b482605") + + for i := 0; i < 10; i++ { + // Send two in a row. They should use each provider, preferred first. + for j := 0; j < 2; j++ { + txOpts, err := cl.txOpts(ctx, amt, defaultSendGasLimit, nil, nil) + if err != nil { + t.Fatal(err) + } + if _, err := cl.sendTransaction(ctx, txOpts, alphaAddr, nil); err != nil { + t.Fatalf("error sending tx %d-%d: %v", i, j, err) + } + } + _, err := cl.bestHeader(ctx) + if err != nil { + t.Fatalf("bestHeader error: %v", err) + } + // Let nonce provider expire. The pair should use the same + // provider, but different pairs can use different providers. Look + // for the variation. + m.mine(ctx) + time.Sleep(time.Second) + } + }); err != nil { + t.Fatal(err) + } +} + +func (m *MRPCTest) TestMonitorNet(t *testing.T, net dex.Network) { + seed, providers := m.readProviderFile(t, net) + dir, _ := os.MkdirTemp("", "") + + cl, err := m.rpcClient(dir, seed, providers, net, true) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(m.ctx, time.Hour) + defer cancel() + + if err := cl.connect(ctx); err != nil { + t.Fatalf("Connection error: %v", err) + } + <-ctx.Done() +} + +func (m *MRPCTest) TestRPC(t *testing.T, net dex.Network) { + // To skip automatic websocket resolution, pass flag --skipws. + + endpoint := os.Getenv("PROVIDER") + if endpoint == "" { + t.Fatalf("specify a provider in the PROVIDER environmental variable") + } + dir, _ := os.MkdirTemp("", "") + defer os.RemoveAll(dir) + cl, err := m.rpcClient(dir, encode.RandomBytes(32), []string{endpoint}, net, true) + if err != nil { + t.Fatal(err) + } + + if err := cl.connect(m.ctx); err != nil { + t.Fatalf("connect error: %v", err) + } + + compat, err := m.compatDataLookup(net) + if err != nil { + t.Fatalf("compatDataLookup error: %v", err) + } + + for _, tt := range newCompatibilityTests(cl, &compat, cl.log) { + tStart := time.Now() + if err := cl.withAny(m.ctx, tt.f); err != nil { + t.Fatalf("%q: %v", tt.name, err) + } + fmt.Printf("### %q: %s \n", tt.name, time.Since(tStart)) + } +} + +func (m *MRPCTest) TestFreeServers(t *testing.T, freeServers []string, net dex.Network) { + compat, err := m.compatDataLookup(net) + if err != nil { + t.Fatalf("compatDataLookup error: %v", err) + } + runTest := func(endpoint string) error { + dir, _ := os.MkdirTemp("", "") + defer os.RemoveAll(dir) + cl, err := m.rpcClient(dir, encode.RandomBytes(32), []string{endpoint}, net, true) + if err != nil { + return fmt.Errorf("tRPCClient error: %v", err) + } + if err := cl.connect(m.ctx); err != nil { + return fmt.Errorf("connect error: %v", err) + } + for _, tt := range newCompatibilityTests(cl, &compat, cl.log) { + if err := cl.withAny(m.ctx, tt.f); err != nil { + return fmt.Errorf("%q error: %v", tt.name, err) + } + fmt.Printf("#### %q passed %q \n", endpoint, tt.name) + } + return nil + } + + passes, fails := make([]string, 0), make(map[string]error, 0) + for _, endpoint := range freeServers { + if err := runTest(endpoint); err != nil { + fails[endpoint] = err + } else { + passes = append(passes, endpoint) + } + } + for _, pass := range passes { + fmt.Printf("!!!! %q PASSED \n", pass) + } + for endpoint, err := range fails { + fmt.Printf("XXXX %q FAILED : %v \n", endpoint, err) + } +} + +func (m *MRPCTest) TestMainnetCompliance(t *testing.T) { + _, providerLookup := m.readProviderFile(t, dex.Mainnet) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + cfg, err := m.chainConfigLookup(dex.Mainnet) + if err != nil { + t.Fatalf("chainConfigLookup error: %v", err) + } + + compat, err := m.compatDataLookup(dex.Mainnet) + if err != nil { + t.Fatalf("compatDataLookup error: %v", err) + } + + log := dex.StdOutLogger("T", dex.LevelTrace) + providers, err := connectProviders(ctx, providerLookup, log, cfg.ChainID, dex.Mainnet) + if err != nil { + t.Fatal(err) + } + err = checkProvidersCompliance(ctx, providers, &compat, log) + if err != nil { + t.Fatal(err) + } +} + +func (m *MRPCTest) testSimnetEndpoint(endpoints []string, syncBlocks uint64, tFunc func(context.Context, *multiRPCClient)) error { + dir, _ := os.MkdirTemp("", "") + defer os.RemoveAll(dir) + + cl, err := m.rpcClient(dir, encode.RandomBytes(32), endpoints, dex.Simnet, false) + if err != nil { + return err + } + fmt.Println("######## Address:", cl.creds.addr) + + if err := cl.connect(m.ctx); err != nil { + return fmt.Errorf("connect error: %v", err) + } + + startHdr, err := cl.bestHeader(m.ctx) + if err != nil { + return fmt.Errorf("error getting initial header: %v", err) + } + + // mine headers + start := time.Now() + for { + m.mine(m.ctx) + hdr, err := cl.bestHeader(m.ctx) + if err != nil { + return fmt.Errorf("error getting best header: %v", err) + } + blocksMined := hdr.Number.Uint64() - startHdr.Number.Uint64() + if blocksMined > syncBlocks { + break + } + if time.Since(start) > time.Minute { + return fmt.Errorf("timed out") + } + select { + case <-time.After(time.Second * 5): + // mine and check again + case <-m.ctx.Done(): + return context.Canceled + } + } + + if tFunc != nil { + tFunc(m.ctx, cl) + } + + time.Sleep(time.Second) + + return nil +} + +func (m *MRPCTest) harnessCmd(ctx context.Context, exe string, args ...string) (string, error) { + cmd := exec.CommandContext(ctx, exe, args...) + cmd.Dir = m.harnessDirectory + op, err := cmd.CombinedOutput() + return string(op), err +} + +func (m *MRPCTest) mine(ctx context.Context) error { + _, err := m.harnessCmd(ctx, "./mine-alpha", "1") + return err +} + +func (m *MRPCTest) readProviderFile(t *testing.T, net dex.Network) (seed []byte, providers []string) { + t.Helper() + var err error + seed, providers, err = getFileCredentials(m.chain, m.credentialsFile, net) + if err != nil { + t.Fatalf("Error retreiving credentials from file at %q: %v", m.credentialsFile, err) + } + return +} diff --git a/client/asset/eth/node.go b/client/asset/eth/node.go index d0a2727f43..2bbb8ed1ec 100644 --- a/client/asset/eth/node.go +++ b/client/asset/eth/node.go @@ -190,7 +190,7 @@ func prepareNode(cfg *nodeConfig) (*node.Node, error) { // startNode starts a geth node. func startNode(chainID int64, node *node.Node, network dex.Network) (*les.LightEthereum, error) { - ethCfg, err := chainConfig(chainID, network) + ethCfg, err := ETHConfig(network) if err != nil { return nil, err } diff --git a/client/asset/eth/nodeclient.go b/client/asset/eth/nodeclient.go index 745bc55643..13e2ec46ed 100644 --- a/client/asset/eth/nodeclient.go +++ b/client/asset/eth/nodeclient.go @@ -406,45 +406,15 @@ func newTxOpts(ctx context.Context, from common.Address, val, maxGas uint64, max } } -func gases(parentID, assetID uint32, contractVer uint32, net dex.Network) *dexeth.Gases { - isToken := parentID != assetID - if !isToken { // ETH or EVM-compatible asset. - if contractVer != contractVersionNewest { - return dexeth.VersionedGases[contractVer] - } - var bestVer uint32 - var bestGases *dexeth.Gases - for ver, gases := range dexeth.VersionedGases { - if ver >= bestVer { - bestGases = gases - bestVer = ver - } - } - return bestGases - } - - token, found := dexeth.Tokens[assetID] - if !found { - return nil - } - netToken, found := token.NetTokens[net] - if !found { - return nil - } - +func gases(contractVer uint32, versionedGases map[uint32]*dexeth.Gases) *dexeth.Gases { if contractVer != contractVersionNewest { - contract, found := netToken.SwapContracts[contractVer] - if !found { - return nil - } - return &contract.Gas + return versionedGases[contractVer] } - var bestVer uint32 var bestGases *dexeth.Gases - for ver, contract := range netToken.SwapContracts { + for ver, gases := range versionedGases { if ver >= bestVer { - bestGases = &contract.Gas + bestGases = gases bestVer = ver } } diff --git a/client/asset/eth/nodeclient_harness_test.go b/client/asset/eth/nodeclient_harness_test.go index b0f4f5bc8f..56be55f344 100644 --- a/client/asset/eth/nodeclient_harness_test.go +++ b/client/asset/eth/nodeclient_harness_test.go @@ -32,6 +32,7 @@ import ( "os/signal" "path/filepath" "strconv" + "strings" "sync" "testing" "time" @@ -72,44 +73,40 @@ var ( homeDir = os.Getenv("HOME") simnetWalletDir = filepath.Join(homeDir, "dextest", "eth", "client_rpc_tests", "simnet") participantWalletDir = filepath.Join(homeDir, "dextest", "eth", "client_rpc_tests", "participant") - testnetWalletDir = filepath.Join(homeDir, "ethtest", "testnet_contract_tests", "walletA") - testnetParticipantWalletDir = filepath.Join(homeDir, "ethtest", "testnet_contract_tests", "walletB") - alphaNodeDir = filepath.Join(homeDir, "dextest", "eth", "alpha", "node") - alphaIPCFile = filepath.Join(alphaNodeDir, "geth.ipc") - betaNodeDir = filepath.Join(homeDir, "dextest", "eth", "beta", "node") - betaIPCFile = filepath.Join(betaNodeDir, "geth.ipc") - ctx context.Context - tLogger = dex.StdOutLogger("ETHTEST", dex.LevelWarn) - simnetWalletSeed = "0812f5244004217452059e2fd11603a511b5d0870ead753df76c966ce3c71531" - simnetAddr common.Address - simnetAcct *accounts.Account - ethClient ethFetcher - participantWalletSeed = "a897afbdcba037c8c735cc63080558a30d72851eb5a3d05684400ec4123a2d00" - participantAddr common.Address - participantAcct *accounts.Account - participantEthClient ethFetcher - ethSwapContractAddr common.Address - simnetContractor contractor - participantContractor contractor - simnetTokenContractor tokenContractor - participantTokenContractor tokenContractor - ethGases = dexeth.VersionedGases[0] - tokenGases *dexeth.Gases - testnetSecPerBlock = 15 * time.Second - // secPerBlock is one for simnet, because it takes one second to mine a - // block currently. Is set in code to testnetSecPerBlock if running on - // testnet. - secPerBlock = time.Second + testnetWalletDir string + testnetParticipantWalletDir string + + alphaNodeDir = filepath.Join(homeDir, "dextest", "eth", "alpha", "node") + alphaIPCFile = filepath.Join(alphaNodeDir, "geth.ipc") + betaNodeDir = filepath.Join(homeDir, "dextest", "eth", "beta", "node") + betaIPCFile = filepath.Join(betaNodeDir, "geth.ipc") + ctx context.Context + tLogger = dex.StdOutLogger("ETHTEST", dex.LevelWarn) + simnetWalletSeed = "0812f5244004217452059e2fd11603a511b5d0870ead753df76c966ce3c71531" + simnetAddr common.Address + simnetAcct *accounts.Account + ethClient ethFetcher + participantWalletSeed = "a897afbdcba037c8c735cc63080558a30d72851eb5a3d05684400ec4123a2d00" + participantAddr common.Address + participantAcct *accounts.Account + participantEthClient ethFetcher + ethSwapContractAddr common.Address + simnetContractor contractor + participantContractor contractor + simnetTokenContractor tokenContractor + participantTokenContractor tokenContractor + ethGases = dexeth.VersionedGases[0] + tokenGases *dexeth.Gases + secPerBlock = 15 * time.Second // If you are testing on testnet, you must specify the rpcNode. You can also // specify it in the testnet-credentials.json file. - rpcNode string + rpcProviders []string // useRPC can be set to true to test the RPC clients. useRPC bool // isTestnet can be set to true to perform tests on the goerli testnet. // May need some setup including sending testnet coins to the addresses - // and a lengthy sync. Wallet addresses are the same as simnet. All - // wait and lock times are multiplied by testnetSecPerBlock. Tests may + // and a lengthy sync. Wallet addresses are the same as simnet. Tests may // need to be run with a high --timeout=2h for the initial sync. // // Only for non-token tests, so run with --run=TestGroupName. @@ -117,14 +114,6 @@ var ( // TODO: Make this also work for token tests. isTestnet bool - // For testnet credentials, use a JSON file formatted like... - // { - // "key0": "deadbeef", - // "key1": "beefdead", - // "provider": "https://myprovider.com/MYAPIKEY" - // } - testnetCredentialsPath = filepath.Join(homeDir, "ethtest", "testnet-credentials.json") - // testnetWalletSeed and testnetParticipantWalletSeed are required for // use on testnet and can be any 256 bit hex. If the wallets created by // these seeds do not have enough funds to test, addresses that need @@ -211,14 +200,12 @@ func waitForMinedRPC() error { } } -// waitForMined will multiply the time limit by testnetSecPerBlock for +// waitForMined will multiply the time limit by secPerBlock for // testnet and mine blocks when on simnet. func waitForMined(nBlock int, waitTimeLimit bool) error { timesUp := time.After(time.Duration(nBlock) * secPerBlock) - if isTestnet && useRPC { - if err := waitForMinedRPC(); err != nil { - return err - } + if useRPC { + return waitForMinedRPC() } if !isTestnet { err := exec.Command("geth", "--datadir="+alphaNodeDir, "attach", "--exec", "miner.start()").Run() @@ -263,15 +250,13 @@ out: return nil } -func prepareRPCClient(name, dataDir, endpoint string, net dex.Network) (*multiRPCClient, *accounts.Account, error) { - chainID := dexeth.ChainIDs[net] - ethCfg, err := chainConfig(chainID, net) +func prepareRPCClient(name, dataDir string, providers []string, net dex.Network) (*multiRPCClient, *accounts.Account, error) { + cfg, err := ChainConfig(net) if err != nil { return nil, nil, err } - cfg := ethCfg.Genesis.Config - c, err := newMultiRPCClient(dataDir, []string{endpoint}, tLogger.SubLogger(name), cfg, big.NewInt(dexeth.ChainIDs[net]), net) + c, err := newMultiRPCClient(dataDir, providers, tLogger.SubLogger(name), cfg, net) if err != nil { return nil, nil, fmt.Errorf("(%s) newNodeClient error: %v", name, err) } @@ -281,28 +266,27 @@ func prepareRPCClient(name, dataDir, endpoint string, net dex.Network) (*multiRP return c, c.creds.acct, nil } -func rpcEndpoints(net dex.Network) (string, string) { +func rpcEndpoints(net dex.Network) ([]string, []string) { if net == dex.Testnet { - return rpcNode, rpcNode + return rpcProviders, rpcProviders } - return alphaIPCFile, betaIPCFile + return []string{alphaIPCFile}, []string{betaIPCFile} } func prepareTestRPCClients(initiatorDir, participantDir string, net dex.Network) (err error) { - initiatorEndpoint, participantEndpoint := rpcEndpoints(net) + initiatorEndpoints, participantEndpoints := rpcEndpoints(net) - ethClient, simnetAcct, err = prepareRPCClient("initiator", initiatorDir, initiatorEndpoint, net) + ethClient, simnetAcct, err = prepareRPCClient("initiator", initiatorDir, initiatorEndpoints, net) if err != nil { return err } + fmt.Println("initiator address is", ethClient.address()) - participantEthClient, participantAcct, err = prepareRPCClient("participant", participantDir, participantEndpoint, net) + participantEthClient, participantAcct, err = prepareRPCClient("participant", participantDir, participantEndpoints, net) if err != nil { ethClient.shutdown() return err } - - fmt.Println("initiator address is", ethClient.address()) fmt.Println("participant address is", participantEthClient.address()) return nil } @@ -367,25 +351,27 @@ func runSimnet(m *testing.M) (int, error) { return 1, fmt.Errorf("error creating participant wallet dir: %v", err) } - tokenGases = &dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas + const contractVer = 0 + + tokenGases = &dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[contractVer].Gas // ETH swap contract. masterToken = dexeth.Tokens[testTokenID] token := masterToken.NetTokens[dex.Simnet] - fmt.Printf("ETH swap contract address is %v\n", dexeth.ContractAddresses[0][dex.Simnet]) + fmt.Printf("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("Test token contract addr is %v\n", token.Address) - ethSwapContractAddr = dexeth.ContractAddresses[0][dex.Simnet] + ethSwapContractAddr = dexeth.ContractAddresses[contractVer][dex.Simnet] - initiatorRPC, participantRPC := rpcEndpoints(dex.Simnet) + initiatorProviders, participantProviders := rpcEndpoints(dex.Simnet) - err = setupWallet(simnetWalletDir, simnetWalletSeed, "localhost:30355", initiatorRPC, dex.Simnet) + err = setupWallet(simnetWalletDir, simnetWalletSeed, "localhost:30355", initiatorProviders, dex.Simnet) if err != nil { return 1, err } - err = setupWallet(participantWalletDir, participantWalletSeed, "localhost:30356", participantRPC, dex.Simnet) + err = setupWallet(participantWalletDir, participantWalletSeed, "localhost:30356", participantProviders, dex.Simnet) if err != nil { return 1, err } @@ -411,14 +397,19 @@ func runSimnet(m *testing.M) (int, error) { simnetAddr = simnetAcct.Address participantAddr = participantAcct.Address - if simnetContractor, err = newV0Contractor(dex.Simnet, simnetAddr, ethClient.contractBackend()); err != nil { + contractAddr, exists := dexeth.ContractAddresses[contractVer][dex.Simnet] + if !exists || contractAddr == (common.Address{}) { + return 1, fmt.Errorf("no contract address for 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(dex.Simnet, participantAddr, participantEthClient.contractBackend()); err != nil { + 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, testTokenID, simnetAddr, ethClient.contractBackend()); err != nil { + if simnetTokenContractor, err = newV0TokenContractor(dex.Simnet, dexeth.Tokens[testTokenID], simnetAddr, ethClient.contractBackend()); err != nil { return 1, fmt.Errorf("newV0TokenContractor error: %w", err) } @@ -427,7 +418,7 @@ func runSimnet(m *testing.M) (int, error) { // (*BoundContract).Call while calling (*ERC20Swap).TokenAddress. time.Sleep(time.Second) - if participantTokenContractor, err = newV0TokenContractor(dex.Simnet, testTokenID, participantAddr, participantEthClient.contractBackend()); err != nil { + if participantTokenContractor, err = newV0TokenContractor(dex.Simnet, dexeth.Tokens[testTokenID], participantAddr, participantEthClient.contractBackend()); err != nil { return 1, fmt.Errorf("participant newV0TokenContractor error: %w", err) } @@ -506,8 +497,8 @@ func runTestnet(m *testing.M) (int, error) { if err != nil { return 1, fmt.Errorf("error creating testnet participant wallet dir: %v", err) } - secPerBlock = testnetSecPerBlock - ethSwapContractAddr = dexeth.ContractAddresses[0][dex.Testnet] + const contractVer = 0 + ethSwapContractAddr = dexeth.ContractAddresses[contractVer][dex.Testnet] fmt.Printf("ETH swap contract address is %v\n", ethSwapContractAddr) initiatorRPC, participantRPC := rpcEndpoints(dex.Testnet) @@ -557,10 +548,15 @@ func runTestnet(m *testing.M) (int, error) { simnetAddr = simnetAcct.Address participantAddr = participantAcct.Address - if simnetContractor, err = newV0Contractor(dex.Testnet, simnetAddr, ethClient.contractBackend()); err != nil { + contractAddr, exists := dexeth.ContractAddresses[contractVer][dex.Testnet] + if !exists || contractAddr == (common.Address{}) { + return 1, fmt.Errorf("no contract address for 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(dex.Testnet, participantAddr, participantEthClient.contractBackend()); err != nil { + if participantContractor, err = newV0Contractor(contractAddr, participantAddr, participantEthClient.contractBackend()); err != nil { return 1, fmt.Errorf("participant newV0Contractor error: %w", err) } @@ -571,7 +567,7 @@ func runTestnet(m *testing.M) (int, error) { return 1, fmt.Errorf("error unlocking initiator client: %w", err) } - if simnetTokenContractor, err = newV0TokenContractor(dex.Testnet, usdcID, simnetAddr, ethClient.contractBackend()); err != nil { + if simnetTokenContractor, err = newV0TokenContractor(dex.Testnet, dexeth.Tokens[usdcID], simnetAddr, ethClient.contractBackend()); err != nil { return 1, fmt.Errorf("newV0TokenContractor error: %w", err) } @@ -580,7 +576,7 @@ func runTestnet(m *testing.M) (int, error) { // (*BoundContract).Call while calling (*ERC20Swap).TokenAddress. time.Sleep(time.Second) - if participantTokenContractor, err = newV0TokenContractor(dex.Testnet, usdcID, participantAddr, participantEthClient.contractBackend()); err != nil { + if participantTokenContractor, err = newV0TokenContractor(dex.Testnet, dexeth.Tokens[usdcID], participantAddr, participantEthClient.contractBackend()); err != nil { return 1, fmt.Errorf("participant newV0TokenContractor error: %w", err) } @@ -602,27 +598,21 @@ func runTestnet(m *testing.M) (int, error) { func useTestnet() error { isTestnet = true - b, err := os.ReadFile(testnetCredentialsPath) + b, err := os.ReadFile(filepath.Join(homeDir, "dextest", "credentials.json")) if err != nil { - if !os.IsNotExist(err) { - return fmt.Errorf("error reading credentials file: %v", err) - } - } else { - var creds struct { - Key0 string `json:"key0"` - Key1 string `json:"key1"` - Provider string `json:"provider"` - } - if err := json.Unmarshal(b, &creds); err != nil { - return fmt.Errorf("error parsing credentials file: %v", err) - } - if creds.Key0 == "" || creds.Key1 == "" { - return fmt.Errorf("must provide both keys in testnet credentials file") - } - testnetWalletSeed = creds.Key0 - testnetParticipantWalletSeed = creds.Key1 - rpcNode = creds.Provider + return fmt.Errorf("error reading credentials file: %v", err) + } + var creds providersFile + if err = json.Unmarshal(b, &creds); err != nil { + return fmt.Errorf("error decoding credential: %w", err) + } + if len(creds.Seed) == 0 { + return errors.New("no seed found in credentials file") } + seed2 := sha256.Sum256(creds.Seed) + testnetWalletSeed = hex.EncodeToString(creds.Seed) + testnetParticipantWalletSeed = hex.EncodeToString(seed2[:]) + rpcProviders = creds.Providers["eth"][dex.Testnet.String()] return nil } @@ -630,10 +620,19 @@ func TestMain(m *testing.M) { dexeth.MaybeReadSimnetAddrs() flag.BoolVar(&isTestnet, "testnet", false, "use testnet") - flag.BoolVar(&useRPC, "rpc", false, "use RPC") + flag.BoolVar(&useRPC, "rpc", true, "use RPC") flag.Parse() if isTestnet { + tmpDir, err := os.MkdirTemp("", "") + if err != nil { + fmt.Fprintf(os.Stderr, "error creating temporary directory: %v", err) + os.Exit(1) + } + testnetWalletDir = filepath.Join(tmpDir, "initiator") + defer os.RemoveAll(testnetWalletDir) + testnetParticipantWalletDir = filepath.Join(tmpDir, "participant") + defer os.RemoveAll(testnetParticipantWalletDir) if err := useTestnet(); err != nil { fmt.Fprintf(os.Stderr, "error loading testnet: %v", err) os.Exit(1) @@ -665,7 +664,7 @@ func TestMain(m *testing.M) { os.Exit(exitCode) } -func setupWallet(walletDir, seed, listenAddress, rpcAddr string, net dex.Network) error { +func setupWallet(walletDir, seed, listenAddress string, providers []string, net dex.Network) error { walletType := walletTypeGeth settings := map[string]string{ "nodelistenaddr": listenAddress, @@ -673,7 +672,7 @@ func setupWallet(walletDir, seed, listenAddress, rpcAddr string, net dex.Network if useRPC { walletType = walletTypeRPC settings = map[string]string{ - providersKey: rpcAddr, + providersKey: strings.Join(providers, " "), } } seedB, _ := hex.DecodeString(seed) @@ -686,7 +685,11 @@ func setupWallet(walletDir, seed, listenAddress, rpcAddr string, net dex.Network Net: net, Logger: tLogger, } - return CreateEVMWallet(dexeth.ChainIDs[net], &createWalletParams, true) + compat, err := NetworkCompatibilityData(net) + if err != nil { + return err + } + return CreateEVMWallet(dexeth.ChainIDs[net], &createWalletParams, &compat, true) } func prepareTokenClients(t *testing.T) { @@ -1122,17 +1125,21 @@ func testInitiateGas(t *testing.T, assetID uint32) { prepareTokenClients(t) } - c := simnetContractor - - if assetID != BipID { - c = simnetTokenContractor - } - net := dex.Simnet if isTestnet { net = dex.Testnet } - gases := gases(BipID, assetID, 0, net) + + 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 + } + } + gases := gases(0, versionedGases) var previousGas uint64 maxSwaps := 50 @@ -2302,7 +2309,7 @@ func testSignMessage(t *testing.T) { func TestTokenGasEstimates(t *testing.T) { ctx, cancel := context.WithCancel(ctx) defer cancel() - runSimnetMiner(ctx, tLogger) + runSimnetMiner(ctx, "eth", tLogger) prepareTokenClients(t) tLogger.SetLevel(dex.LevelInfo) if err := getGasEstimates(ctx, ethClient, participantEthClient, simnetTokenContractor, participantTokenContractor, 5, tokenGases, tLogger); err != nil { diff --git a/client/asset/interface.go b/client/asset/interface.go index a114c678a1..3303c83668 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -292,6 +292,10 @@ type WalletInfo struct { // 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. + IsAccountBased bool } // ConfigOption is a wallet configuration option. diff --git a/client/asset/polygon/chaincfg.go b/client/asset/polygon/chaincfg.go new file mode 100644 index 0000000000..14bc4425bd --- /dev/null +++ b/client/asset/polygon/chaincfg.go @@ -0,0 +1,120 @@ +// This code is available on the terms of the project LICENSE.md file, +// also available online at https://blueoakcouncil.org/license/1.0.0. + +package polygon + +import ( + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + + "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" + ethcore "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" +) + +var ( + mainnetCompatibilityData = eth.CompatibilityData{ + Addr: common.HexToAddress("0x5973918275C01F50555d44e92c9d9b353CaDAD54"), + TokenAddr: common.HexToAddress("0x2791bca1f2de4661ed88a30c99a7a9449aa84174"), // usdc + TxHash: common.HexToHash("0xc388210f83679f9841e34fb3cdee0294f885846de3e01211e50f77d508a0d6ec"), + BlockHash: common.HexToHash("0xa603d7354686269521e8d561d6ffa4aa92aad80e01f3b4cc9745fdb54342f85b"), + } + + testnetCompatibilityData = eth.CompatibilityData{ + Addr: common.HexToAddress("C26880A0AF2EA0c7E8130e6EC47Af756465452E8"), + TokenAddr: common.HexToAddress("0x734aeF51d427b2f745210Ec4BF1062ABd48Eceb6"), // weth + TxHash: common.HexToHash("0xc592ac8975a58bc7ad48381f9a05c07a53a67b2a4448ad821ed7ef2dcd1a878a"), + BlockHash: common.HexToHash("0x5a2d26b5bd9d1995c25e211379671bce893befa31cf2a9704ff89f8682b3c6cf"), + } +) + +// NetworkCompatibilityData returns the CompatibilityData for the specified +// network. If using simnet, make sure the simnet harness is running. +func NetworkCompatibilityData(net dex.Network) (c eth.CompatibilityData, err error) { + switch net { + case dex.Mainnet: + return mainnetCompatibilityData, nil + case dex.Testnet: + return testnetCompatibilityData, nil + case dex.Simnet: + default: + return c, fmt.Errorf("No compatibility data for network # %d", net) + } + // simnet + tDir, err := simnetDataDir() + if err != nil { + return + } + + addr := common.HexToAddress("18d65fb8d60c1199bb1ad381be47aa692b482605") + var ( + tTxHashFile = filepath.Join(tDir, "test_tx_hash.txt") + tBlockHashFile = filepath.Join(tDir, "test_block10_hash.txt") + tContractFile = filepath.Join(tDir, "test_token_contract_address.txt") + ) + readIt := func(path string) string { + b, err := os.ReadFile(path) + if err != nil { + panic(fmt.Sprintf("Problem reading simnet testing file %q: %v", path, err)) + } + return strings.TrimSpace(string(b)) // mainly the trailing "\r\n" + } + return eth.CompatibilityData{ + Addr: addr, + TokenAddr: common.HexToAddress(readIt(tContractFile)), + TxHash: common.HexToHash(readIt(tTxHashFile)), + BlockHash: common.HexToHash(readIt(tBlockHashFile)), + }, nil +} + +// simnetDataDir returns the data directory for Ethereum simnet. +func simnetDataDir() (string, error) { + u, err := user.Current() + if err != nil { + return "", fmt.Errorf("error getting current user: %w", err) + } + + return filepath.Join(u.HomeDir, "dextest", "polygon"), nil +} + +// ChainConfig returns the core configuration for the blockchain. +func ChainConfig(net dex.Network) (c *params.ChainConfig, err error) { + switch net { + case dex.Mainnet: + return dexpolygon.BorMainnetChainConfig, nil + case dex.Testnet: + return dexpolygon.MumbaiChainConfig, nil + case dex.Simnet: + default: + return c, fmt.Errorf("unknown network %d", net) + } + // simnet + g, err := readSimnetGenesisFile() + if err != nil { + return c, fmt.Errorf("readSimnetGenesisFile error: %w", err) + } + return g.Config, nil +} + +// readSimnetGenesisFile reads the simnet genesis file. +func readSimnetGenesisFile() (*ethcore.Genesis, error) { + dataDir, err := simnetDataDir() + if err != nil { + return nil, err + } + + genesisFile := filepath.Join(dataDir, "genesis.json") + genesisCfg, err := dexeth.LoadGenesisFile(genesisFile) + if err != nil { + return nil, fmt.Errorf("error reading genesis file: %v", err) + } + + return genesisCfg, nil +} diff --git a/client/asset/polygon/multirpc_live_test.go b/client/asset/polygon/multirpc_live_test.go new file mode 100644 index 0000000000..e8517737bc --- /dev/null +++ b/client/asset/polygon/multirpc_live_test.go @@ -0,0 +1,126 @@ +//go:build rpclive + +package polygon + +import ( + "context" + "os" + "testing" + + "decred.org/dcrdex/client/asset/eth" + "decred.org/dcrdex/dex" +) + +const ( + alphaHTTPPort = "48296" + alphaWSPort = "34983" +) + +var mt *eth.MRPCTest + +func TestMain(m *testing.M) { + ctx, shutdown := context.WithCancel(context.Background()) + mt = eth.NewMRPCTest(ctx, ChainConfig, NetworkCompatibilityData, "polygon") + doIt := func() int { + defer shutdown() + return m.Run() + } + os.Exit(doIt()) +} + +func TestHTTP(t *testing.T) { + mt.TestHTTP(t, alphaHTTPPort) +} + +func TestWS(t *testing.T) { + mt.TestWS(t, alphaWSPort) +} + +func TestWSTxLogs(t *testing.T) { + mt.TestWSTxLogs(t, alphaWSPort) +} + +func TestSimnetMultiRPCClient(t *testing.T) { + mt.TestSimnetMultiRPCClient(t, alphaWSPort, alphaHTTPPort) +} + +func TestMonitorTestnet(t *testing.T) { + mt.TestMonitorNet(t, dex.Testnet) +} + +func TestMonitorMainnet(t *testing.T) { + mt.TestMonitorNet(t, dex.Mainnet) +} + +func TestRPCMainnet(t *testing.T) { + mt.TestRPC(t, dex.Mainnet) +} + +func TestRPCTestnet(t *testing.T) { + mt.TestRPC(t, dex.Testnet) +} + +func TestFreeServers(t *testing.T) { + // https://wiki.polygon.technology/docs/pos/reference/rpc-endpoints/ + // https://www.alchemy.com/chain-connect/chain/polygon-pos + // https://chainlist.org/?search=Polygon+Mainnet + freeServers := []string{ + // Passing + "https://rpc-mainnet.maticvigil.com" + "https://rpc.ankr.com/polygon" + "https://polygon.blockpi.network/v1/rpc/public" + "https://1rpc.io/matic" + "https://polygon.api.onfinality.io/public" + "https://rpc-mainnet.matic.quiknode.pro" + "https://polygon.drpc.org" + // Not passing + "https://matic-mainnet-full-rpc.bwarelabs.com" + "https://polygon-rpc.com" + "https://polygon-mainnet.rpcfast.com?api_key=xbhWBI1Wkguk8SNMu1bvvLurPGLXmgwYeC4S6g2H7WdwFigZSmPWVZRxrskEQwIf" + "https://polygon.rpc.blxrbdn.com" + "https://rpc-mainnet.matic.network" + "https://endpoints.omniatech.io/v1/matic/mainnet/public" + "https://matic-mainnet.chainstacklabs.com" + "https://polygon-bor.publicnode.com" + "https://polygon.llamarpc.com" + "https://polygon-mainnet.public.blastapi.io" + "https://poly-rpc.gateway.pokt.network" + "https://polygon-mainnet-public.unifra.io" + "https://g.w.lavanet.xyz:443/gateway/polygon1/rpc-http/f7ee0000000000000000000000000000" + "https://matic-mainnet-archive-rpc.bwarelabs.com" + "https://polygonapi.terminet.io/rpc" + "https://polygon.meowrpc.com" + + // DEPRECATED + // "https://matic-mainnet-archive-rpc.bwarelabs.com", + // "https://matic-mainnet-full-rpc.bwarelabs.com", + } + mt.TestFreeServers(t, freeServers, dex.Mainnet) +} + +func TestFreeTestnetServers(t *testing.T) { + // https://wiki.polygon.technology/docs/pos/reference/rpc-endpoints/ + // https://www.alchemy.com/chain-connect/chain/mumbai + // https://chainlist.org/chain/80001 + freeServers := []string{ + // Passing + "https://rpc.ankr.com/polygon_mumbai", + "https://polygon-testnet.public.blastapi.io", + "https://polygon-mumbai.blockpi.network/v1/rpc/public", + "https://rpc-mumbai.maticvigil.com", + + // Not passing + "https://polygon-mumbai-bor.publicnode.com", + "https://endpoints.omniatech.io/v1/matic/mumbai/public", + "https://polygontestapi.terminet.io/rpc", + "https://matic-mumbai.chainstacklabs.com", + "https://matic-testnet-archive-rpc.bwarelabs.com", + "https://g.w.lavanet.xyz:443/gateway/polygon1t/rpc-http/f7ee0000000000000000000000000000", + "https://api.zan.top/node/v1/polygon/mumbai/public", + } + mt.TestFreeServers(t, freeServers, dex.Testnet) +} + +func TestMainnetCompliance(t *testing.T) { + mt.TestMainnetCompliance(t) +} diff --git a/client/asset/polygon/polygon.go b/client/asset/polygon/polygon.go index f49358fa9f..d3a7c9b246 100644 --- a/client/asset/polygon/polygon.go +++ b/client/asset/polygon/polygon.go @@ -5,50 +5,76 @@ package polygon import ( "fmt" + "strconv" "decred.org/dcrdex/client/asset" "decred.org/dcrdex/client/asset/eth" "decred.org/dcrdex/dex" dexpolygon "decred.org/dcrdex/dex/networks/polygon" + "github.com/ethereum/go-ethereum/common" ) +func registerToken(tokenID uint32, desc string, nets ...dex.Network) { + token, found := dexpolygon.Tokens[tokenID] + if !found { + panic("token " + strconv.Itoa(int(tokenID)) + " not known") + } + asset.RegisterToken(tokenID, token.Token, &asset.WalletDefinition{ + Type: walletTypeToken, + Tab: "Polygon token", + Description: desc, + }, nets...) +} + func init() { asset.Register(BipID, &Driver{}) + registerToken(simnetTokenID, "A token wallet for the DEX test token. Used for testing DEX software.", dex.Simnet) + registerToken(usdcTokenID, "The USDC Ethereum ERC20 token.", dex.Mainnet, dex.Testnet) } const ( // BipID is the BIP-0044 asset ID for Polygon. - BipID = 966 - walletTypeRPC = "rpc" + BipID = 966 + defaultGasFeeLimit = 1000 + walletTypeRPC = "rpc" + walletTypeToken = "token" ) var ( + simnetTokenID, _ = dex.BipSymbolID("dextt.polygon") + usdcTokenID, _ = dex.BipSymbolID("usdc.polygon") // WalletInfo defines some general information about a Polygon Wallet(EVM // Compatible). - WalletInfo = &asset.WalletInfo{ + + walletOpts = []*asset.ConfigOption{ + { + Key: "gasfeelimit", + DisplayName: "Gas Fee Limit", + Description: "This is the highest network fee rate you are willing to " + + "pay on swap transactions. If gasfeelimit is lower than a market's " + + "maxfeerate, you will not be able to trade on that market with this " + + "wallet. Units: gwei / gas", + DefaultValue: defaultGasFeeLimit, + }, + } + WalletInfo = asset.WalletInfo{ Name: "Polygon", Version: 0, SupportedVersions: []uint32{0}, UnitInfo: dexpolygon.UnitInfo, AvailableWallets: []*asset.WalletDefinition{ - // { - // Type: walletTypeGeth, - // Tab: "Native", - // Description: "Use the built-in DEX wallet (geth light node)", - // ConfigOpts: WalletOpts, - // Seeded: true, - // }, { Type: walletTypeRPC, Tab: "External", Description: "Infrastructure providers (e.g. Infura) or local nodes", - ConfigOpts: append(eth.RPCOpts, eth.WalletOpts...), + ConfigOpts: append(eth.RPCOpts, walletOpts...), 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, } ) @@ -56,7 +82,36 @@ type Driver struct{} // Open opens the Polygon exchange wallet. Start the wallet with its Run method. func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (asset.Wallet, error) { - return eth.NewEVMWallet(BipID, dexpolygon.ChainIDs[net], cfg, logger, net) + chainCfg, err := ChainConfig(net) + if err != nil { + return nil, fmt.Errorf("failed to locate Polygon genesis configuration for network %s", net) + } + compat, err := NetworkCompatibilityData(net) + if err != nil { + return nil, fmt.Errorf("failed to locate Polygon compatibility data: %s", net) + } + contracts := make(map[uint32]common.Address, 1) + for ver, netAddrs := range dexpolygon.ContractAddresses { + for netw, addr := range netAddrs { + if netw == net { + contracts[ver] = addr + break + } + } + } + // BipID, chainCfg, cfg, &t, dexpolygon.VersionedGases, dexpolygon.Tokens, logger, net + return eth.NewEVMWallet(ð.EVMWalletConfig{ + BaseChainID: BipID, + ChainCfg: chainCfg, + AssetCfg: cfg, + CompatData: &compat, + VersionedGases: dexpolygon.VersionedGases, + Tokens: dexpolygon.Tokens, + Logger: logger, + BaseChainContracts: contracts, + WalletInfo: WalletInfo, + Net: net, + }) } func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { @@ -64,7 +119,8 @@ func (d *Driver) DecodeCoinID(coinID []byte) (string, error) { } func (d *Driver) Info() *asset.WalletInfo { - return WalletInfo + wi := WalletInfo + return &wi } func (d *Driver) Exists(walletType, dataDir string, settings map[string]string, net dex.Network) (bool, error) { @@ -75,5 +131,9 @@ func (d *Driver) Exists(walletType, dataDir string, settings map[string]string, } func (d *Driver) Create(cfg *asset.CreateWalletParams) error { - return eth.CreateEVMWallet(dexpolygon.ChainIDs[cfg.Net], cfg, false) + compat, err := NetworkCompatibilityData(cfg.Net) + if err != nil { + return fmt.Errorf("error finding compatibility data: %v", err) + } + return eth.CreateEVMWallet(dexpolygon.ChainIDs[cfg.Net], cfg, &compat, false) } diff --git a/client/cmd/assetseed/main.go b/client/cmd/assetseed/main.go index cf31841248..85da557d77 100644 --- a/client/cmd/assetseed/main.go +++ b/client/cmd/assetseed/main.go @@ -9,6 +9,7 @@ import ( "fmt" "os" + "decred.org/dcrdex/client/asset" "decred.org/dcrdex/client/core" ) @@ -36,6 +37,11 @@ func main() { os.Exit(1) } + if tkn := asset.TokenInfo(uint32(assetID)); tkn != nil { + fmt.Fprintf(os.Stderr, "this is a token. did you want asset ID %d for %s?\n", tkn.ParentID, asset.Asset(tkn.ParentID).Info.Name) + os.Exit(1) + } + seed, _ := core.AssetSeedAndPass(uint32(assetID), appSeedB) fmt.Printf("%x\n", seed) diff --git a/client/cmd/simnet-trade-tests/main.go b/client/cmd/simnet-trade-tests/main.go index 8bbd39e4c9..95664c3866 100644 --- a/client/cmd/simnet-trade-tests/main.go +++ b/client/cmd/simnet-trade-tests/main.go @@ -11,6 +11,7 @@ import ( "decred.org/dcrdex/client/core" "decred.org/dcrdex/dex" dexeth "decred.org/dcrdex/dex/networks/eth" + dexpolygon "decred.org/dcrdex/dex/networks/polygon" "github.com/fatih/color" ) @@ -221,4 +222,5 @@ func (f *flagArray) Set(value string) error { func init() { dexeth.MaybeReadSimnetAddrs() + dexpolygon.MaybeReadSimnetAddrs() } diff --git a/client/cmd/simnet-trade-tests/run b/client/cmd/simnet-trade-tests/run index 37483fd9bb..92bf27e74d 100755 --- a/client/cmd/simnet-trade-tests/run +++ b/client/cmd/simnet-trade-tests/run @@ -80,6 +80,14 @@ case $1 in ./simnet-trade-tests --base1node trading1 --base2node trading2 --quote dextt.eth ${@:2} ;; + polygondcr) + ./simnet-trade-tests --quote1node trading1 --quote2node trading2 --quote dcr --base polygon --regasset dcr ${@:2} + ;; + + polydexttdcr) + ./simnet-trade-tests --quote1node trading1 --quote2node trading2 --base dextt.polygon --quote dcr --regasset dcr ${@:2} + ;; + help|--help|-h) ./simnet-trade-tests --help cat < 0 { + break mined + } + s.log.Infof("Waiting for %s funding tx to be mined", unbip(form.AssetID)) + select { + case <-time.After(time.Second * 2): + case <-s.ctx.Done(): + return s.ctx.Err() + } + } + // Tip change after block filtering scan takes the wallet time. time.Sleep(2 * time.Second * sleepFactor) + + w, _ := c.core.wallet(form.AssetID) + approved := make(chan struct{}) + if approver, is := w.Wallet.(asset.TokenApprover); is { + const contractVer = 0 + if _, err := approver.ApproveToken(contractVer, func() { + close(approved) + }); err != nil { + return fmt.Errorf("error approving %s token: %w", unbip(form.AssetID), err) + } + out: + for { + s.log.Infof("Mining more blocks to get approval tx confirmed") + hctrl.mineBlocks(s.ctx, 2) + select { + case <-approved: + c.log.Infof("%s approved", unbip(form.AssetID)) + break out + case <-time.After(time.Second * 5): + case <-s.ctx.Done(): + return s.ctx.Err() + } + } + } } + return nil } @@ -1525,24 +1570,28 @@ func (hc *harnessCtrl) fund(ctx context.Context, address string, amts []int) err } func newHarnessCtrl(assetID uint32) *harnessCtrl { - + symbolParts := strings.Split(dex.BipIDSymbol(assetID), ".") + baseChainSymbol := symbolParts[0] + if len(symbolParts) == 2 { + baseChainSymbol = symbolParts[1] + } switch assetID { case dcr.BipID, btc.BipID, ltc.BipID, bch.BipID, doge.BipID, firo.BipID, zec.BipID, dgb.BipID, dash.BipID: return &harnessCtrl{ - dir: filepath.Join(dextestDir, dex.BipIDSymbol(assetID), "harness-ctl"), + dir: filepath.Join(dextestDir, baseChainSymbol, "harness-ctl"), fundCmd: "./alpha", fundStr: "sendtoaddress_%s_%d", } - case eth.BipID: + case eth.BipID, polygon.BipID: // Sending with values of .1 eth. return &harnessCtrl{ - dir: filepath.Join(dextestDir, dex.BipIDSymbol(assetID), "harness-ctl"), + dir: filepath.Join(dextestDir, baseChainSymbol, "harness-ctl"), fundCmd: "./sendtoaddress", fundStr: "%s_%d", } - case testTokenID: + case ethDexttID, polygonDexttID: return &harnessCtrl{ - dir: filepath.Join(dextestDir, dex.BipIDSymbol(eth.BipID), "harness-ctl"), + dir: filepath.Join(dextestDir, baseChainSymbol, "harness-ctl"), fundCmd: "./sendTokens", fundStr: "%s_%d", } @@ -1572,8 +1621,10 @@ var cloneTypes = map[uint32]string{ // accountBIPs is a map of account based assets. Used in fee estimation. var accountBIPs = map[uint32]bool{ - eth.BipID: true, - 60000: true, // dextt test token + eth.BipID: true, + ethDexttID: true, // dextt test token + polygon.BipID: true, + polygonDexttID: true, } func dcrWallet(wt SimWalletType, node string) (*tWallet, error) { @@ -1620,7 +1671,15 @@ func ethWallet() (*tWallet, error) { return &tWallet{ fund: true, walletType: "rpc", - config: map[string]string{"providers": alphaIPCFile}, + config: map[string]string{"providers": ethAlphaIPCFile}, + }, nil +} + +func polygonWallet() (*tWallet, error) { + return &tWallet{ + fund: true, + walletType: "rpc", + config: map[string]string{"providers": polygonAlphaIPCFile}, }, nil } @@ -1631,7 +1690,19 @@ func dexttWallet() (*tWallet, error) { parent: &WalletForm{ Type: "rpc", AssetID: eth.BipID, - Config: map[string]string{"providers": alphaIPCFile}, + Config: map[string]string{"providers": ethAlphaIPCFile}, + }, + }, nil +} + +func polyDexttWallet() (*tWallet, error) { + return &tWallet{ + fund: true, + walletType: "token", + parent: &WalletForm{ + Type: "rpc", + AssetID: polygon.BipID, + Config: map[string]string{"providers": polygonAlphaIPCFile}, }, }, nil } @@ -1741,8 +1812,12 @@ func (s *simulationTest) newClient(name string, cl *SimClient) (*simulationClien tw, err = btcWallet(wt, node) case eth.BipID: tw, err = ethWallet() - case testTokenID: + case ethDexttID: tw, err = dexttWallet() + case polygon.BipID: + tw, err = polygonWallet() + case polygonDexttID: + tw, err = polyDexttWallet() case ltc.BipID: tw, err = ltcWallet(wt, node) case bch.BipID: diff --git a/client/webserver/live_test.go b/client/webserver/live_test.go index f659ed5e3c..6b3de8b132 100644 --- a/client/webserver/live_test.go +++ b/client/webserver/live_test.go @@ -1389,7 +1389,7 @@ var winfos = map[uint32]*asset.WalletInfo{ ConfigOpts: configOpts, }}, }, - 60: eth.WalletInfo, + 60: ð.WalletInfo, 145: { Version: 0, SupportedVersions: []uint32{0}, diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 2a178aadbb..28eb76fe20 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -179,6 +179,7 @@ var EnUS = map[string]string{ "Restore from seed": "Restore from seed", "Import Account": "Import Account", "no_wallet": "no wallet", + "Token on": "Token on", "create_a_x_wallet": "Create a Wallet", "dont_share": "Don't share it. Don't lose it.", "Show Me": "Show Me", diff --git a/client/webserver/site/src/css/main.scss b/client/webserver/site/src/css/main.scss index ed395a96e8..544d275660 100644 --- a/client/webserver/site/src/css/main.scss +++ b/client/webserver/site/src/css/main.scss @@ -507,7 +507,14 @@ div.form-closer { white-space: pre-line; } -span.token-aware-symbol sup { +img.token-parent { + width: 0.7em; + height: 0.7em; + position: relative; + top: -0.4em; +} + +sup.token-parent { font-size: 0.6em; position: relative; top: -0.6em; diff --git a/client/webserver/site/src/css/market.scss b/client/webserver/site/src/css/market.scss index 777422568b..d48a61f3f4 100644 --- a/client/webserver/site/src/css/market.scss +++ b/client/webserver/site/src/css/market.scss @@ -340,7 +340,7 @@ div[data-handler=markets] { margin-bottom: 10px; width: 100%; - img { + img.market-tmpl-pair-img { width: 20px; height: 20px; position: relative; @@ -707,7 +707,7 @@ div[data-handler=markets] { } } - img { + img.market-stats-pair-img { width: 30px; height: 30px; diff --git a/client/webserver/site/src/css/wallets.scss b/client/webserver/site/src/css/wallets.scss index 9dd5ee9157..a55b888a79 100644 --- a/client/webserver/site/src/css/wallets.scss +++ b/client/webserver/site/src/css/wallets.scss @@ -47,14 +47,25 @@ cursor: pointer; min-height: fit-content; - & > img { + img[data-tmpl=img] { width: 40px; height: 40px; } + + img[data-tmpl=parentImg] { + position: absolute; + left: 20px; + bottom: 20px; + width: 28px; + height: 28px; + border-radius: 15px; + background-color: $light_body_bg; + border: 3px solid $light_body_bg; + } } .icon-select.nowallet { - opacity: 0.6; + opacity: 0.7; [data-tmpl=balance] { display: none; @@ -68,11 +79,6 @@ } } - #assetLogo { - width: 35px; - height: 35px; - } - #assetName { font-size: 35px; } @@ -182,6 +188,16 @@ #walletDetailsBox { border-bottom: $light_border_color; + + #assetLogo { + width: 35px; + height: 35px; + } + + #tokenParentLogo { + width: 18px; + height: 18px; + } } .peers-table-icon { diff --git a/client/webserver/site/src/css/wallets_dark.scss b/client/webserver/site/src/css/wallets_dark.scss index 91c60d313c..d279eabdaf 100644 --- a/client/webserver/site/src/css/wallets_dark.scss +++ b/client/webserver/site/src/css/wallets_dark.scss @@ -38,4 +38,13 @@ body.dark { } } } + + #assetSelect { + .icon-select { + img[data-tmpl=parentImg] { + background-color: $dark_body_bg; + border-color: $dark_body_bg; + } + } + } } diff --git a/client/webserver/site/src/html/bodybuilder.tmpl b/client/webserver/site/src/html/bodybuilder.tmpl index e518d94812..52e93e7324 100644 --- a/client/webserver/site/src/html/bodybuilder.tmpl +++ b/client/webserver/site/src/html/bodybuilder.tmpl @@ -9,7 +9,7 @@ {{.Title}} - +
[[[add_a_x_wallet]]] +
+ [[[Token on]]] + + +
@@ -128,10 +133,15 @@ {{define "depositAddress"}}
-
+
[[[Deposit]]] - + +
+ [[[Token on]]] + + +
diff --git a/client/webserver/site/src/html/markets.tmpl b/client/webserver/site/src/html/markets.tmpl index b6380764a4..2417968732 100644 --- a/client/webserver/site/src/html/markets.tmpl +++ b/client/webserver/site/src/html/markets.tmpl @@ -30,8 +30,8 @@
- - + +
@@ -64,7 +64,7 @@
- +
- @@ -270,11 +270,11 @@
@@ -284,12 +284,12 @@
[[[:title:lot_size]]]: - +
[[[Rate Step]]]: - +
@@ -315,7 +315,7 @@
- / + /
@@ -327,25 +327,25 @@
{{/* spacer */}}
- +
{{- /* MARKET BUY ORDER QUANTITY INPUT */ -}}
- [[[min trade is about]]] + [[[min trade is about]]]
- +
- ~ 0 + ~ 0 @ 0 [[[:title:lots]]]
@@ -401,7 +401,7 @@
-
+
diff --git a/client/webserver/site/src/html/order.tmpl b/client/webserver/site/src/html/order.tmpl index cf725676a1..ca1ad702b2 100644 --- a/client/webserver/site/src/html/order.tmpl +++ b/client/webserver/site/src/html/order.tmpl @@ -1,5 +1,5 @@ {{define "microIcon"}} - + {{end}} {{define "order"}} @@ -40,7 +40,7 @@
[[[Offering]]]
-
{{$ord.OfferString}} {{$ord.FromSymbol}} {{template "microIcon" $ord.FromSymbol}}
+
{{$ord.OfferString}} {{$ord.FromTicker}} {{template "microIcon" $ord.FromTicker}}
[[[Asking]]]
diff --git a/client/webserver/site/src/html/wallets.tmpl b/client/webserver/site/src/html/wallets.tmpl index 6842b422e6..fba58bb7b3 100644 --- a/client/webserver/site/src/html/wallets.tmpl +++ b/client/webserver/site/src/html/wallets.tmpl @@ -9,7 +9,10 @@ {{- /* ASSET SELECT */ -}}