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

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package eth
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"crypto/sha256"
    10  	"encoding/hex"
    11  	"encoding/json"
    12  	"errors"
    13  	"fmt"
    14  	"math/big"
    15  	"os"
    16  	"os/exec"
    17  	"os/user"
    18  	"path/filepath"
    19  	"sort"
    20  	"strconv"
    21  	"strings"
    22  	"sync"
    23  	"sync/atomic"
    24  	"time"
    25  
    26  	"decred.org/dcrdex/client/asset"
    27  	"decred.org/dcrdex/dex"
    28  	"decred.org/dcrdex/dex/config"
    29  	"decred.org/dcrdex/dex/encode"
    30  	"decred.org/dcrdex/dex/keygen"
    31  	"decred.org/dcrdex/dex/networks/erc20"
    32  	dexeth "decred.org/dcrdex/dex/networks/eth"
    33  	multibal "decred.org/dcrdex/dex/networks/eth/contracts/multibalance"
    34  	"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
    35  	"github.com/decred/dcrd/hdkeychain/v3"
    36  	"github.com/ethereum/go-ethereum"
    37  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    38  	"github.com/ethereum/go-ethereum/accounts/keystore"
    39  	"github.com/ethereum/go-ethereum/common"
    40  	ethmath "github.com/ethereum/go-ethereum/common/math"
    41  	"github.com/ethereum/go-ethereum/core/types"
    42  	"github.com/ethereum/go-ethereum/crypto"
    43  	"github.com/ethereum/go-ethereum/eth/ethconfig"
    44  	"github.com/ethereum/go-ethereum/params"
    45  	"github.com/tyler-smith/go-bip39"
    46  )
    47  
    48  func init() {
    49  	dexeth.MaybeReadSimnetAddrs()
    50  }
    51  
    52  func registerToken(tokenID uint32, desc string) {
    53  	token, found := dexeth.Tokens[tokenID]
    54  	if !found {
    55  		panic("token " + strconv.Itoa(int(tokenID)) + " not known")
    56  	}
    57  	netAddrs := make(map[dex.Network]string)
    58  	for net, netToken := range token.NetTokens {
    59  		netAddrs[net] = netToken.Address.String()
    60  	}
    61  	asset.RegisterToken(tokenID, token.Token, &asset.WalletDefinition{
    62  		Type:        walletTypeToken,
    63  		Tab:         "Ethereum token",
    64  		Description: desc,
    65  	}, netAddrs)
    66  }
    67  
    68  func init() {
    69  	asset.Register(BipID, &Driver{})
    70  	registerToken(usdcTokenID, "The USDC Ethereum ERC20 token.")
    71  	registerToken(usdtTokenID, "The USDT Ethereum ERC20 token.")
    72  	registerToken(maticTokenID, "The MATIC Ethereum ERC20 token.")
    73  }
    74  
    75  const (
    76  	// BipID is the BIP-0044 asset ID for Ethereum.
    77  	BipID               = 60
    78  	defaultGasFee       = 82  // gwei
    79  	defaultGasFeeLimit  = 200 // gwei
    80  	defaultSendGasLimit = 21_000
    81  
    82  	walletTypeGeth  = "geth"
    83  	walletTypeRPC   = "rpc"
    84  	walletTypeToken = "token"
    85  
    86  	providersKey = "providers"
    87  
    88  	// onChainDataFetchTimeout is the max amount of time allocated to fetching
    89  	// on-chain data. Testing on testnet has shown spikes up to 2.5 seconds
    90  	// (but on internet, with Tor, it could actually take up to 30 seconds easily).
    91  	// Setting it to 10 seconds for now until https://github.com/decred/dcrdex/issues/3184
    92  	// is resolved.
    93  	onChainDataFetchTimeout = 10 * time.Second
    94  
    95  	// coinIDTakerFoundMakerRedemption is a prefix to identify one of CoinID formats,
    96  	// see DecodeCoinID func for details.
    97  	coinIDTakerFoundMakerRedemption = "TakerFoundMakerRedemption:"
    98  
    99  	// maxTxFeeGwei is the default max amount of eth that can be used in one
   100  	// transaction. This is set by the host in the case of providers. The
   101  	// internal node currently has no max but also cannot be used since the
   102  	// merge.
   103  	//
   104  	// TODO: Find a way to ask the host about their config set max fee and
   105  	// gas values.
   106  	maxTxFeeGwei = 1_000_000_000
   107  
   108  	LiveEstimateFailedError = dex.ErrorKind("live gas estimate failed")
   109  
   110  	// txAgeOut is the amount of time after which we forego any tx
   111  	// synchronization efforts for unconfirmed pending txs.
   112  	txAgeOut = 2 * time.Hour
   113  	// stateUpdateTick is the minimum amount of time between checks for
   114  	// new block and updating of pending txs, counter-party redemptions and
   115  	// approval txs.
   116  	// HTTP RPC clients meter tip header calls to minimum 10 seconds.
   117  	// WebSockets will stay up-to-date, so can expect new blocks often.
   118  	// A shorter blockTicker would be too much for e.g. Polygon where the block
   119  	// time is 2 or 3 seconds. We'd be doing a ton of calls for pending tx
   120  	// updates.
   121  	stateUpdateTick = time.Second * 5
   122  	// maxUnindexedTxs is the number of pending txs we will allow to be
   123  	// unverified on-chain before we halt broadcasting of new txs.
   124  	maxUnindexedTxs       = 10
   125  	peerCountTicker       = 5 * time.Second // no rpc calls here
   126  	contractVersionNewest = ^uint32(0)
   127  )
   128  
   129  var (
   130  	usdcTokenID, _  = dex.BipSymbolID("usdc.eth")
   131  	usdtTokenID, _  = dex.BipSymbolID("usdt.eth")
   132  	maticTokenID, _ = dex.BipSymbolID("matic.eth")
   133  	walletOpts      = []*asset.ConfigOption{
   134  		{
   135  			Key:         "gasfeelimit",
   136  			DisplayName: "Gas Fee Limit",
   137  			Description: "This is the highest network fee rate you are willing to " +
   138  				"pay on swap transactions. If gasfeelimit is lower than a market's " +
   139  				"maxfeerate, you will not be able to trade on that market with this " +
   140  				"wallet.  Units: gwei / gas",
   141  			DefaultValue: strconv.FormatUint(defaultGasFeeLimit, 10),
   142  		},
   143  	}
   144  	RPCOpts = []*asset.ConfigOption{
   145  		{
   146  			Key:         providersKey,
   147  			DisplayName: "RPC Provider",
   148  			Description: "Specify one or more RPC providers. For infrastructure " +
   149  				"providers, prefer using wss address. Only url-based authentication " +
   150  				"is supported. For a local node, use the filepath to an IPC file.",
   151  			Repeatable:   providerDelimiter,
   152  			RepeatN:      2,
   153  			DefaultValue: "",
   154  		},
   155  	}
   156  	// WalletInfo defines some general information about a Ethereum wallet.
   157  	WalletInfo = asset.WalletInfo{
   158  		Name: "Ethereum",
   159  		// SupportedVersions: For Ethereum, the server backend maintains a
   160  		// single protocol version, so tokens and ETH have the same set of
   161  		// supported versions. Even though the SupportedVersions are made
   162  		// accessible for tokens via (*TokenWallet).Info, the versions are not
   163  		// exposed though any Driver methods or assets/driver functions. Use the
   164  		// parent wallet's WalletInfo via (*Driver).Info if you need a token's
   165  		// supported versions before a wallet is available.
   166  		SupportedVersions: []uint32{0},
   167  		UnitInfo:          dexeth.UnitInfo,
   168  		AvailableWallets: []*asset.WalletDefinition{
   169  			// {
   170  			// 	Type:        walletTypeGeth,
   171  			// 	Tab:         "Native",
   172  			// 	Description: "Use the built-in DEX wallet (geth light node)",
   173  			// 	ConfigOpts:  WalletOpts,
   174  			// 	Seeded:      true,
   175  			// },
   176  			{
   177  				Type:        walletTypeRPC,
   178  				Tab:         "RPC",
   179  				Description: "Infrastructure providers (e.g. Infura) or local nodes",
   180  				ConfigOpts:  append(RPCOpts, walletOpts...),
   181  				Seeded:      true,
   182  				GuideLink:   "https://github.com/decred/dcrdex/blob/master/docs/wiki/Ethereum.md",
   183  			},
   184  			// MaxSwapsInTx and MaxRedeemsInTx are set in (Wallet).Info, since
   185  			// the value cannot be known until we connect and get network info.
   186  		},
   187  		IsAccountBased: true,
   188  	}
   189  
   190  	// unlimitedAllowance is the maximum supported allowance for an erc20
   191  	// contract, and is effectively unlimited.
   192  	unlimitedAllowance = ethmath.MaxBig256
   193  	// unlimitedAllowanceReplenishThreshold is the threshold below which we will
   194  	// require a new approval. In practice, this will never be hit, but any
   195  	// allowance below this will signal that WE didn't set it, and we'll require
   196  	// an upgrade to unlimited (since we don't support limited allowance yet).
   197  	unlimitedAllowanceReplenishThreshold = new(big.Int).Div(unlimitedAllowance, big.NewInt(2))
   198  
   199  	seedDerivationPath = []uint32{
   200  		hdkeychain.HardenedKeyStart + 44, // purpose 44' for HD wallets
   201  		hdkeychain.HardenedKeyStart + 60, // eth coin type 60'
   202  		hdkeychain.HardenedKeyStart,      // account 0'
   203  		0,                                // branch 0
   204  		0,                                // index 0
   205  	}
   206  )
   207  
   208  // perTxGasLimit is the most gas we can use on a transaction. It is the lower of
   209  // either the per tx or per block gas limit.
   210  func perTxGasLimit(gasFeeLimit uint64) uint64 {
   211  	// maxProportionOfBlockGasLimitToUse sets the maximum proportion of a
   212  	// block's gas limit that a swap and redeem transaction will use. Since it
   213  	// is set to 4, the max that will be used is 25% (1/4) of the block's gas
   214  	// limit.
   215  	const maxProportionOfBlockGasLimitToUse = 4
   216  
   217  	// blockGasLimit is the amount of gas we can use in one transaction
   218  	// according to the block gas limit.
   219  
   220  	// Ethereum GasCeil: 30_000_000, Polygon: 8_000_000
   221  	blockGasLimit := ethconfig.Defaults.Miner.GasCeil / maxProportionOfBlockGasLimitToUse
   222  
   223  	// txGasLimit is the amount of gas we can use in one transaction
   224  	// according to the default transaction gas fee limit.
   225  	txGasLimit := maxTxFeeGwei / gasFeeLimit
   226  
   227  	if blockGasLimit > txGasLimit {
   228  		return txGasLimit
   229  	}
   230  	return blockGasLimit
   231  }
   232  
   233  // safeConfs returns the confirmations for a given tip and block number,
   234  // returning 0 if the block number is zero or if the tip is lower than the
   235  // block number.
   236  func safeConfs(tip, blockNum uint64) uint64 {
   237  	if blockNum == 0 {
   238  		return 0
   239  	}
   240  	if tip < blockNum {
   241  		return 0
   242  	}
   243  	return tip - blockNum + 1
   244  }
   245  
   246  // safeConfsBig is safeConfs but with a *big.Int blockNum. A nil blockNum will
   247  // result in a zero.
   248  func safeConfsBig(tip uint64, blockNum *big.Int) uint64 {
   249  	if blockNum == nil {
   250  		return 0
   251  	}
   252  	return safeConfs(tip, blockNum.Uint64())
   253  }
   254  
   255  // WalletConfig are wallet-level configuration settings.
   256  type WalletConfig struct {
   257  	GasFeeLimit uint64 `ini:"gasfeelimit"`
   258  }
   259  
   260  // parseWalletConfig parses the settings map into a *WalletConfig.
   261  func parseWalletConfig(settings map[string]string) (cfg *WalletConfig, err error) {
   262  	cfg = new(WalletConfig)
   263  	err = config.Unmapify(settings, &cfg)
   264  	if err != nil {
   265  		return nil, fmt.Errorf("error parsing wallet config: %w", err)
   266  	}
   267  	return cfg, nil
   268  }
   269  
   270  // Driver implements asset.Driver.
   271  type Driver struct{}
   272  
   273  // Check that Driver implements Driver and Creator.
   274  var _ asset.Driver = (*Driver)(nil)
   275  var _ asset.Creator = (*Driver)(nil)
   276  
   277  // Open opens the ETH exchange wallet. Start the wallet with its Run method.
   278  func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, network dex.Network) (asset.Wallet, error) {
   279  	return newWallet(cfg, logger, network)
   280  }
   281  
   282  // DecodeCoinID creates a human-readable representation of a coin ID for Ethereum.
   283  // These are supported coin ID formats:
   284  //  1. A transaction hash. 32 bytes
   285  //  2. An encoded ETH funding coin id which includes the account address and
   286  //     amount. 20 + 8 = 28 bytes
   287  //  3. An encoded token funding coin id which includes the account address,
   288  //     a token value, and fees. 20 + 8 + 8 = 36 bytes
   289  //  4. A byte encoded string of the account address. 40 or 42 (with 0x) bytes
   290  //  5. A byte encoded string which represents specific case where Taker found
   291  //     Maker redemption on his own (while Maker failed to notify him about it
   292  //     first). 26 (`TakerFoundMakerRedemption:` prefix) + 42 (Maker address
   293  //     with 0x) bytes
   294  func (d *Driver) DecodeCoinID(coinID []byte) (string, error) {
   295  	switch len(coinID) {
   296  	case common.HashLength:
   297  		var txHash common.Hash
   298  		copy(txHash[:], coinID)
   299  		return txHash.String(), nil
   300  	case fundingCoinIDSize:
   301  		c, err := decodeFundingCoin(coinID)
   302  		if err != nil {
   303  			return "", err
   304  		}
   305  		return c.String(), nil
   306  	case tokenFundingCoinIDSize:
   307  		c, err := decodeTokenFundingCoin(coinID)
   308  		if err != nil {
   309  			return "", err
   310  		}
   311  		return c.String(), nil
   312  	case common.AddressLength * 2, common.AddressLength*2 + 2:
   313  		hexAddr := string(coinID)
   314  		if !common.IsHexAddress(hexAddr) {
   315  			return "", fmt.Errorf("invalid hex address %q", coinID)
   316  		}
   317  		return common.HexToAddress(hexAddr).String(), nil
   318  	case len(coinIDTakerFoundMakerRedemption) + common.AddressLength*2 + 2:
   319  		coinIDStr := string(coinID)
   320  		if !strings.HasPrefix(coinIDStr, coinIDTakerFoundMakerRedemption) {
   321  			return "", fmt.Errorf("coinID %q has no %s prefix", coinID, coinIDTakerFoundMakerRedemption)
   322  		}
   323  		return coinIDStr, nil
   324  	}
   325  
   326  	return "", fmt.Errorf("unknown coin ID format: %x", coinID)
   327  }
   328  
   329  // Info returns basic information about the wallet and asset.
   330  func (d *Driver) Info() *asset.WalletInfo {
   331  	wi := WalletInfo
   332  	return &wi
   333  }
   334  
   335  // Exists checks the existence of the wallet.
   336  func (d *Driver) Exists(walletType, dataDir string, settings map[string]string, net dex.Network) (bool, error) {
   337  	switch walletType {
   338  	case walletTypeGeth, walletTypeRPC:
   339  	default:
   340  		return false, fmt.Errorf("wallet type %q unrecognized", walletType)
   341  	}
   342  
   343  	keyStoreDir := filepath.Join(getWalletDir(dataDir, net), "keystore")
   344  	ks := keystore.NewKeyStore(keyStoreDir, keystore.LightScryptN, keystore.LightScryptP)
   345  	// NOTE: If the keystore did not exist, a warning from an internal KeyStore
   346  	// goroutine may be printed to this effect. Not an issue.
   347  	return len(ks.Wallets()) > 0, nil
   348  }
   349  
   350  func (d *Driver) Create(cfg *asset.CreateWalletParams) error {
   351  	comp, err := NetworkCompatibilityData(cfg.Net)
   352  	if err != nil {
   353  		return fmt.Errorf("error finding compatibility data: %v", err)
   354  	}
   355  	return CreateEVMWallet(dexeth.ChainIDs[cfg.Net], cfg, &comp, false)
   356  }
   357  
   358  // Balance is the current balance, including information about the pending
   359  // balance.
   360  type Balance struct {
   361  	Current, PendingIn, PendingOut *big.Int
   362  }
   363  
   364  // ethFetcher represents a blockchain information fetcher. In practice, it is
   365  // satisfied by *nodeClient. For testing, it can be satisfied by a stub.
   366  type ethFetcher interface {
   367  	address() common.Address
   368  	addressBalance(ctx context.Context, addr common.Address) (*big.Int, error)
   369  	bestHeader(ctx context.Context) (*types.Header, error)
   370  	chainConfig() *params.ChainConfig
   371  	connect(ctx context.Context) error
   372  	peerCount() uint32
   373  	contractBackend() bind.ContractBackend
   374  	headerByHash(ctx context.Context, txHash common.Hash) (*types.Header, error)
   375  	lock() error
   376  	locked() bool
   377  	shutdown()
   378  	sendSignedTransaction(ctx context.Context, tx *types.Transaction, filts ...acceptabilityFilter) error
   379  	sendTransaction(ctx context.Context, txOpts *bind.TransactOpts, to common.Address, data []byte, filts ...acceptabilityFilter) (*types.Transaction, error)
   380  	signData(data []byte) (sig, pubKey []byte, err error)
   381  	syncProgress(context.Context) (progress *ethereum.SyncProgress, tipTime uint64, err error)
   382  	transactionConfirmations(context.Context, common.Hash) (uint32, error)
   383  	getTransaction(context.Context, common.Hash) (*types.Transaction, int64, error)
   384  	txOpts(ctx context.Context, val, maxGas uint64, maxFeeRate, tipCap, nonce *big.Int) (*bind.TransactOpts, error)
   385  	currentFees(ctx context.Context) (baseFees, tipCap *big.Int, err error)
   386  	unlock(pw string) error
   387  	getConfirmedNonce(context.Context) (uint64, error)
   388  	transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
   389  	transactionAndReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, *types.Transaction, error)
   390  	nonce(ctx context.Context) (confirmed, next *big.Int, err error)
   391  }
   392  
   393  // txPoolFetcher can be implemented by node types that support fetching of
   394  // txpool transactions.
   395  type txPoolFetcher interface {
   396  	pendingTransactions() ([]*types.Transaction, error)
   397  }
   398  
   399  type pendingApproval struct {
   400  	txHash    common.Hash
   401  	onConfirm func()
   402  }
   403  
   404  type cachedBalance struct {
   405  	stamp  time.Time
   406  	height uint64
   407  	bal    *big.Int
   408  }
   409  
   410  // Check that assetWallet satisfies the asset.Wallet interface.
   411  var _ asset.Wallet = (*ETHWallet)(nil)
   412  var _ asset.Wallet = (*TokenWallet)(nil)
   413  var _ asset.AccountLocker = (*ETHWallet)(nil)
   414  var _ asset.AccountLocker = (*TokenWallet)(nil)
   415  var _ asset.TokenMaster = (*ETHWallet)(nil)
   416  var _ asset.WalletRestorer = (*ETHWallet)(nil)
   417  var _ asset.LiveReconfigurer = (*ETHWallet)(nil)
   418  var _ asset.LiveReconfigurer = (*TokenWallet)(nil)
   419  var _ asset.TxFeeEstimator = (*ETHWallet)(nil)
   420  var _ asset.TxFeeEstimator = (*TokenWallet)(nil)
   421  var _ asset.DynamicSwapper = (*ETHWallet)(nil)
   422  var _ asset.DynamicSwapper = (*TokenWallet)(nil)
   423  var _ asset.Authenticator = (*ETHWallet)(nil)
   424  var _ asset.TokenApprover = (*TokenWallet)(nil)
   425  var _ asset.WalletHistorian = (*ETHWallet)(nil)
   426  var _ asset.WalletHistorian = (*TokenWallet)(nil)
   427  
   428  type baseWallet struct {
   429  	// The asset subsystem starts with Connect(ctx). This ctx will be initialized
   430  	// in parent ETHWallet once and re-used in child TokenWallet instances.
   431  	ctx        context.Context
   432  	net        dex.Network
   433  	node       ethFetcher
   434  	addr       common.Address
   435  	log        dex.Logger
   436  	dir        string
   437  	walletType string
   438  
   439  	finalizeConfs uint64
   440  
   441  	multiBalanceAddress  common.Address
   442  	multiBalanceContract *multibal.MultiBalanceV0
   443  
   444  	baseChainID uint32
   445  	chainCfg    *params.ChainConfig
   446  	chainID     int64
   447  	compat      *CompatibilityData
   448  	tokens      map[uint32]*dexeth.Token
   449  
   450  	startingBlocks atomic.Uint64
   451  
   452  	tipMtx     sync.RWMutex
   453  	currentTip *types.Header
   454  
   455  	settingsMtx sync.RWMutex
   456  	settings    map[string]string
   457  
   458  	gasFeeLimitV uint64 // atomic
   459  
   460  	walletsMtx sync.RWMutex
   461  	wallets    map[uint32]*assetWallet
   462  
   463  	nonceMtx            sync.RWMutex
   464  	pendingTxs          []*extendedWalletTx
   465  	confirmedNonceAt    *big.Int
   466  	pendingNonceAt      *big.Int
   467  	recoveryRequestSent bool
   468  
   469  	balances struct {
   470  		sync.Mutex
   471  		m map[uint32]*cachedBalance
   472  	}
   473  
   474  	currentFees struct {
   475  		sync.Mutex
   476  		blockNum uint64
   477  		baseRate *big.Int
   478  		tipRate  *big.Int
   479  	}
   480  
   481  	txDB txDB
   482  }
   483  
   484  // assetWallet is a wallet backend for Ethereum and Eth tokens. The backend is
   485  // how Bison Wallet communicates with the Ethereum blockchain and wallet.
   486  // assetWallet satisfies the dex.Wallet interface.
   487  type assetWallet struct {
   488  	*baseWallet
   489  	assetID   uint32
   490  	emit      *asset.WalletEmitter
   491  	log       dex.Logger
   492  	ui        dex.UnitInfo
   493  	connected atomic.Bool
   494  	wi        asset.WalletInfo
   495  
   496  	versionedContracts map[uint32]common.Address
   497  	versionedGases     map[uint32]*dexeth.Gases
   498  
   499  	maxSwapGas   uint64
   500  	maxRedeemGas uint64
   501  
   502  	lockedFunds struct {
   503  		mtx                sync.RWMutex
   504  		initiateReserves   uint64
   505  		redemptionReserves uint64
   506  		refundReserves     uint64
   507  	}
   508  
   509  	findRedemptionMtx  sync.RWMutex
   510  	findRedemptionReqs map[[32]byte]*findRedemptionRequest
   511  
   512  	approvalsMtx     sync.RWMutex
   513  	pendingApprovals map[uint32]*pendingApproval
   514  	approvalCache    map[uint32]bool
   515  
   516  	lastPeerCount uint32
   517  	peersChange   func(uint32, error)
   518  
   519  	contractors map[uint32]contractor // version -> contractor
   520  
   521  	evmify  func(uint64) *big.Int
   522  	atomize func(*big.Int) uint64
   523  
   524  	// pendingTxCheckBal is protected by the nonceMtx. We use this field
   525  	// as a secondary check to see if we need to request confirmations for
   526  	// pending txs, since tips are cached for up to 10 seconds. We check the
   527  	// status of pending txs if the tip has changed OR if the balance has
   528  	// changed.
   529  	pendingTxCheckBal *big.Int
   530  }
   531  
   532  // ETHWallet implements some Ethereum-specific methods.
   533  type ETHWallet struct {
   534  	// 64-bit atomic variables first. See
   535  	// https://golang.org/pkg/sync/atomic/#pkg-note-BUG
   536  	tipAtConnect     int64
   537  	defaultProviders []string
   538  
   539  	*assetWallet
   540  }
   541  
   542  // TokenWallet implements some token-specific methods.
   543  type TokenWallet struct {
   544  	*assetWallet
   545  
   546  	cfg      *tokenWalletConfig
   547  	parent   *assetWallet
   548  	token    *dexeth.Token
   549  	netToken *dexeth.NetToken
   550  }
   551  
   552  func (w *assetWallet) maxSwapsAndRedeems() (maxSwaps, maxRedeems uint64) {
   553  	txGasLimit := perTxGasLimit(atomic.LoadUint64(&w.gasFeeLimitV))
   554  	return txGasLimit / w.maxSwapGas, txGasLimit / w.maxRedeemGas
   555  }
   556  
   557  // Info returns basic information about the wallet and asset.
   558  func (w *assetWallet) Info() *asset.WalletInfo {
   559  	wi := w.wi
   560  	maxSwaps, maxRedeems := w.maxSwapsAndRedeems()
   561  	wi.MaxSwapsInTx = maxSwaps
   562  	wi.MaxRedeemsInTx = maxRedeems
   563  	return &wi
   564  }
   565  
   566  // genWalletSeed uses the wallet seed passed from core as the entropy for
   567  // generating a BIP-39 mnemonic. Then it returns the wallet seed generated
   568  // from the mnemonic which can be used to derive a private key.
   569  func genWalletSeed(entropy []byte) ([]byte, error) {
   570  	if len(entropy) < 32 || len(entropy) > 64 {
   571  		return nil, fmt.Errorf("wallet entropy must be 32 to 64 bytes long")
   572  	}
   573  	mnemonic, err := bip39.NewMnemonic(entropy)
   574  	if err != nil {
   575  		return nil, fmt.Errorf("error deriving mnemonic: %w", err)
   576  	}
   577  	return bip39.NewSeed(mnemonic, ""), nil
   578  }
   579  
   580  func privKeyFromSeed(seed []byte) (pk []byte, zero func(), err error) {
   581  	walletSeed, err := genWalletSeed(seed)
   582  	if err != nil {
   583  		return nil, nil, err
   584  	}
   585  	defer encode.ClearBytes(walletSeed)
   586  
   587  	extKey, err := keygen.GenDeepChild(walletSeed, seedDerivationPath)
   588  	if err != nil {
   589  		return nil, nil, err
   590  	}
   591  	// defer extKey.Zero()
   592  	pk, err = extKey.SerializedPrivKey()
   593  	if err != nil {
   594  		extKey.Zero()
   595  		return nil, nil, err
   596  	}
   597  	return pk, extKey.Zero, nil
   598  }
   599  
   600  func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams, compat *CompatibilityData, skipConnect bool) error {
   601  	switch createWalletParams.Type {
   602  	case walletTypeGeth:
   603  		return asset.ErrWalletTypeDisabled
   604  	case walletTypeRPC:
   605  	default:
   606  		return fmt.Errorf("wallet type %q unrecognized", createWalletParams.Type)
   607  	}
   608  
   609  	walletDir := getWalletDir(createWalletParams.DataDir, createWalletParams.Net)
   610  
   611  	privateKey, zero, err := privKeyFromSeed(createWalletParams.Seed)
   612  	if err != nil {
   613  		return err
   614  	}
   615  	defer zero()
   616  
   617  	switch createWalletParams.Type {
   618  	// case walletTypeGeth:
   619  	// 	node, err := prepareNode(&nodeConfig{
   620  	// 		net:    createWalletParams.Net,
   621  	// 		appDir: walletDir,
   622  	// 	})
   623  	// 	if err != nil {
   624  	// 		return err
   625  	// 	}
   626  	// 	defer node.Close()
   627  	// 	return importKeyToNode(node, privateKey, createWalletParams.Pass)
   628  	case walletTypeRPC:
   629  		// Make the wallet dir if it does not exist, otherwise we may fail to
   630  		// write the compliant-providers.json file. Create the keystore
   631  		// subdirectory as well to avoid a "failed to watch keystore folder"
   632  		// error from the keystore's internal account cache supervisor.
   633  		keystoreDir := filepath.Join(walletDir, "keystore")
   634  		if err := os.MkdirAll(keystoreDir, 0700); err != nil {
   635  			return err
   636  		}
   637  
   638  		// TODO: This procedure may actually work for walletTypeGeth too.
   639  		ks := keystore.NewKeyStore(keystoreDir, keystore.LightScryptN, keystore.LightScryptP)
   640  
   641  		priv, err := crypto.ToECDSA(privateKey)
   642  		if err != nil {
   643  			return err
   644  		}
   645  
   646  		// If the user supplied endpoints, check them now.
   647  		providerDef := createWalletParams.Settings[providersKey]
   648  		if !skipConnect && len(providerDef) > 0 {
   649  			endpoints := strings.Split(providerDef, providerDelimiter)
   650  			if err := createAndCheckProviders(context.Background(), walletDir, endpoints,
   651  				big.NewInt(chainID), compat, createWalletParams.Net, createWalletParams.Logger, false); err != nil {
   652  				return fmt.Errorf("create and check providers: %v", err)
   653  			}
   654  		}
   655  		return importKeyToKeyStore(ks, priv, createWalletParams.Pass)
   656  	}
   657  
   658  	return fmt.Errorf("unknown wallet type %q", createWalletParams.Type)
   659  }
   660  
   661  // newWallet is the constructor for an Ethereum asset.Wallet.
   662  func newWallet(assetCFG *asset.WalletConfig, logger dex.Logger, net dex.Network) (w *ETHWallet, err error) {
   663  	chainCfg, err := ChainConfig(net)
   664  	if err != nil {
   665  		return nil, fmt.Errorf("failed to locate Ethereum genesis configuration for network %s", net)
   666  	}
   667  	comp, err := NetworkCompatibilityData(net)
   668  	if err != nil {
   669  		return nil, fmt.Errorf("failed to locate Ethereum compatibility data: %s", net)
   670  	}
   671  	contracts := make(map[uint32]common.Address, 1)
   672  	for ver, netAddrs := range dexeth.ContractAddresses {
   673  		for netw, addr := range netAddrs {
   674  			if netw == net {
   675  				contracts[ver] = addr
   676  				break
   677  			}
   678  		}
   679  	}
   680  
   681  	var defaultProviders []string
   682  	switch net {
   683  	case dex.Simnet:
   684  		u, _ := user.Current()
   685  		defaultProviders = []string{filepath.Join(u.HomeDir, "dextest", "eth", "alpha", "node", "geth.ipc")}
   686  	case dex.Testnet:
   687  		defaultProviders = []string{
   688  			"https://rpc.ankr.com/eth_sepolia",
   689  			"https://ethereum-sepolia.blockpi.network/v1/rpc/public",
   690  			"https://endpoints.omniatech.io/v1/eth/sepolia/public",
   691  			"https://rpc-sepolia.rockx.com",
   692  			"https://rpc.sepolia.org",
   693  			"https://eth-sepolia-public.unifra.io",
   694  			"https://sepolia.drpc.org",
   695  		}
   696  	case dex.Mainnet:
   697  		defaultProviders = []string{
   698  			"https://rpc.ankr.com/eth",
   699  			"https://ethereum.blockpi.network/v1/rpc/public",
   700  			"https://eth-mainnet.nodereal.io/v1/1659dfb40aa24bbb8153a677b98064d7",
   701  			"https://rpc.builder0x69.io",
   702  			"https://rpc.flashbots.net",
   703  			"wss://eth.llamarpc.com",
   704  			"https://eth.drpc.org",
   705  		}
   706  	}
   707  
   708  	return NewEVMWallet(&EVMWalletConfig{
   709  		BaseChainID:        BipID,
   710  		ChainCfg:           chainCfg,
   711  		AssetCfg:           assetCFG,
   712  		CompatData:         &comp,
   713  		VersionedGases:     dexeth.VersionedGases,
   714  		Tokens:             dexeth.Tokens,
   715  		FinalizeConfs:      3,
   716  		Logger:             logger,
   717  		BaseChainContracts: contracts,
   718  		MultiBalAddress:    dexeth.MultiBalanceAddresses[net],
   719  		WalletInfo:         WalletInfo,
   720  		Net:                net,
   721  		DefaultProviders:   defaultProviders,
   722  	})
   723  }
   724  
   725  // EVMWalletConfig is the configuration for an evm-compatible wallet.
   726  type EVMWalletConfig struct {
   727  	BaseChainID        uint32
   728  	ChainCfg           *params.ChainConfig
   729  	AssetCfg           *asset.WalletConfig
   730  	CompatData         *CompatibilityData
   731  	VersionedGases     map[uint32]*dexeth.Gases
   732  	Tokens             map[uint32]*dexeth.Token
   733  	FinalizeConfs      uint64
   734  	Logger             dex.Logger
   735  	BaseChainContracts map[uint32]common.Address
   736  	DefaultProviders   []string
   737  	MultiBalAddress    common.Address // If empty, separate calls for N tokens + 1
   738  	WalletInfo         asset.WalletInfo
   739  	Net                dex.Network
   740  }
   741  
   742  func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) {
   743  	assetID := cfg.BaseChainID
   744  	chainID := cfg.ChainCfg.ChainID.Int64()
   745  
   746  	// var cl ethFetcher
   747  	switch cfg.AssetCfg.Type {
   748  	case walletTypeGeth:
   749  		return nil, asset.ErrWalletTypeDisabled
   750  	case walletTypeRPC:
   751  		if providerDef := cfg.AssetCfg.Settings[providersKey]; len(providerDef) == 0 && len(cfg.DefaultProviders) == 0 {
   752  			return nil, errors.New("no providers specified")
   753  		}
   754  	default:
   755  		return nil, fmt.Errorf("unknown wallet type %q", cfg.AssetCfg.Type)
   756  	}
   757  
   758  	wCfg, err := parseWalletConfig(cfg.AssetCfg.Settings)
   759  	if err != nil {
   760  		return nil, err
   761  	}
   762  
   763  	gasFeeLimit := wCfg.GasFeeLimit
   764  	if gasFeeLimit == 0 {
   765  		gasFeeLimit = defaultGasFeeLimit
   766  	}
   767  	eth := &baseWallet{
   768  		net:                 cfg.Net,
   769  		baseChainID:         cfg.BaseChainID,
   770  		chainCfg:            cfg.ChainCfg,
   771  		chainID:             chainID,
   772  		compat:              cfg.CompatData,
   773  		tokens:              cfg.Tokens,
   774  		log:                 cfg.Logger,
   775  		dir:                 cfg.AssetCfg.DataDir,
   776  		walletType:          cfg.AssetCfg.Type,
   777  		finalizeConfs:       cfg.FinalizeConfs,
   778  		settings:            cfg.AssetCfg.Settings,
   779  		gasFeeLimitV:        gasFeeLimit,
   780  		wallets:             make(map[uint32]*assetWallet),
   781  		multiBalanceAddress: cfg.MultiBalAddress,
   782  	}
   783  
   784  	var maxSwapGas, maxRedeemGas uint64
   785  	for _, gases := range cfg.VersionedGases {
   786  		if gases.Swap > maxSwapGas {
   787  			maxSwapGas = gases.Swap
   788  		}
   789  		if gases.Redeem > maxRedeemGas {
   790  			maxRedeemGas = gases.Redeem
   791  		}
   792  	}
   793  
   794  	txGasLimit := perTxGasLimit(gasFeeLimit)
   795  
   796  	if maxSwapGas == 0 || txGasLimit < maxSwapGas {
   797  		return nil, errors.New("max swaps cannot be zero or undefined")
   798  	}
   799  	if maxRedeemGas == 0 || txGasLimit < maxRedeemGas {
   800  		return nil, errors.New("max redeems cannot be zero or undefined")
   801  	}
   802  
   803  	aw := &assetWallet{
   804  		baseWallet:         eth,
   805  		log:                cfg.Logger,
   806  		assetID:            assetID,
   807  		versionedContracts: cfg.BaseChainContracts,
   808  		versionedGases:     cfg.VersionedGases,
   809  		maxSwapGas:         maxSwapGas,
   810  		maxRedeemGas:       maxRedeemGas,
   811  		emit:               cfg.AssetCfg.Emit,
   812  		findRedemptionReqs: make(map[[32]byte]*findRedemptionRequest),
   813  		pendingApprovals:   make(map[uint32]*pendingApproval),
   814  		approvalCache:      make(map[uint32]bool),
   815  		peersChange:        cfg.AssetCfg.PeersChange,
   816  		contractors:        make(map[uint32]contractor),
   817  		evmify:             dexeth.GweiToWei,
   818  		atomize:            dexeth.WeiToGwei,
   819  		ui:                 dexeth.UnitInfo,
   820  		pendingTxCheckBal:  new(big.Int),
   821  		wi:                 cfg.WalletInfo,
   822  	}
   823  
   824  	maxSwaps, maxRedeems := aw.maxSwapsAndRedeems()
   825  
   826  	cfg.Logger.Debugf("ETH wallet will support a maximum of %d swaps and %d redeems per transaction.",
   827  		maxSwaps, maxRedeems)
   828  
   829  	aw.wallets = map[uint32]*assetWallet{
   830  		assetID: aw,
   831  	}
   832  
   833  	return &ETHWallet{
   834  		assetWallet:      aw,
   835  		defaultProviders: cfg.DefaultProviders,
   836  	}, nil
   837  }
   838  
   839  func getWalletDir(dataDir string, network dex.Network) string {
   840  	return filepath.Join(dataDir, network.String())
   841  }
   842  
   843  // Connect connects to the node RPC server. Satisfies dex.Connector.
   844  func (w *ETHWallet) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) {
   845  	var cl ethFetcher
   846  	switch w.walletType {
   847  	case walletTypeGeth:
   848  		// cl, err = newNodeClient(getWalletDir(w.dir, w.net), w.net, w.log.SubLogger("NODE"))
   849  		// if err != nil {
   850  		// 	return nil, err
   851  		// }
   852  		return nil, asset.ErrWalletTypeDisabled
   853  	case walletTypeRPC:
   854  		w.settingsMtx.RLock()
   855  		defer w.settingsMtx.RUnlock()
   856  		endpoints := w.defaultProviders
   857  		if providerDef, found := w.settings[providersKey]; found && len(providerDef) > 0 {
   858  			endpoints = strings.Split(providerDef, " ")
   859  		}
   860  		rpcCl, err := newMultiRPCClient(w.dir, endpoints, w.log.SubLogger("RPC"), w.chainCfg, w.finalizeConfs, w.net)
   861  		if err != nil {
   862  			return nil, err
   863  		}
   864  		rpcCl.finalizeConfs = w.finalizeConfs
   865  		cl = rpcCl
   866  	default:
   867  		return nil, fmt.Errorf("unknown wallet type %q", w.walletType)
   868  	}
   869  
   870  	w.node = cl
   871  	w.addr = cl.address()
   872  	w.ctx = ctx // TokenWallet will re-use this ctx.
   873  
   874  	err = w.node.connect(ctx)
   875  	if err != nil {
   876  		return nil, err
   877  	}
   878  
   879  	for ver, constructor := range contractorConstructors {
   880  		contractAddr, exists := w.versionedContracts[ver]
   881  		if !exists || contractAddr == (common.Address{}) {
   882  			return nil, fmt.Errorf("no contract address for version %d, net %s", ver, w.net)
   883  		}
   884  		c, err := constructor(contractAddr, w.addr, w.node.contractBackend())
   885  		if err != nil {
   886  			return nil, fmt.Errorf("error constructor version %d contractor: %v", ver, err)
   887  		}
   888  		w.contractors[ver] = c
   889  	}
   890  
   891  	if w.multiBalanceAddress != (common.Address{}) {
   892  		w.multiBalanceContract, err = multibal.NewMultiBalanceV0(w.multiBalanceAddress, cl.contractBackend())
   893  		if err != nil {
   894  			w.log.Errorf("Error loading MultiBalance contract: %v", err)
   895  		}
   896  	}
   897  
   898  	w.txDB, err = newBadgerTxDB(filepath.Join(w.dir, "txhistorydb"), w.log.SubLogger("TXDB"))
   899  	if err != nil {
   900  		return nil, err
   901  	}
   902  
   903  	txCM := dex.NewConnectionMaster(w.txDB)
   904  	if err := txCM.ConnectOnce(ctx); err != nil {
   905  		return nil, err
   906  	}
   907  
   908  	pendingTxs, err := w.txDB.getPendingTxs()
   909  	if err != nil {
   910  		return nil, err
   911  	}
   912  	sort.Slice(pendingTxs, func(i, j int) bool {
   913  		return pendingTxs[i].Nonce.Cmp(pendingTxs[j].Nonce) < 0
   914  	})
   915  
   916  	// Initialize the best block.
   917  	bestHdr, err := w.node.bestHeader(ctx)
   918  	if err != nil {
   919  		return nil, fmt.Errorf("error getting best block hash: %w", err)
   920  	}
   921  	confirmedNonce, nextNonce, err := w.node.nonce(ctx)
   922  	if err != nil {
   923  		return nil, fmt.Errorf("error establishing nonce: %w", err)
   924  	}
   925  
   926  	w.tipMtx.Lock()
   927  	w.currentTip = bestHdr
   928  	w.tipMtx.Unlock()
   929  	w.startingBlocks.Store(bestHdr.Number.Uint64())
   930  
   931  	w.nonceMtx.Lock()
   932  	w.pendingTxs = pendingTxs
   933  	w.confirmedNonceAt = confirmedNonce
   934  	w.pendingNonceAt = nextNonce
   935  	w.nonceMtx.Unlock()
   936  
   937  	if w.log.Level() <= dex.LevelDebug {
   938  		var highestPendingNonce, lowestPendingNonce uint64
   939  		for _, pendingTx := range pendingTxs {
   940  			n := pendingTx.Nonce.Uint64()
   941  			if n > highestPendingNonce {
   942  				highestPendingNonce = n
   943  			}
   944  			if lowestPendingNonce == 0 || n < lowestPendingNonce {
   945  				lowestPendingNonce = n
   946  			}
   947  		}
   948  		w.log.Debugf("Synced with header %s and confirmed nonce %s, pending nonce %s, %d pending txs from nonce %d to nonce %d",
   949  			bestHdr.Number, confirmedNonce, nextNonce, len(pendingTxs), highestPendingNonce, lowestPendingNonce)
   950  	}
   951  
   952  	height := w.currentTip.Number
   953  	// NOTE: We should be using the tipAtConnect to set Progress in SyncStatus.
   954  	atomic.StoreInt64(&w.tipAtConnect, height.Int64())
   955  	w.log.Infof("Connected to eth (%s), at height %d", w.walletType, height)
   956  
   957  	var wg sync.WaitGroup
   958  
   959  	wg.Add(1)
   960  	go func() {
   961  		defer wg.Done()
   962  		w.monitorBlocks(ctx)
   963  		w.node.shutdown()
   964  	}()
   965  
   966  	wg.Add(1)
   967  	go func() {
   968  		defer wg.Done()
   969  		w.monitorPeers(ctx)
   970  	}()
   971  
   972  	w.connected.Store(true)
   973  	go func() {
   974  		<-ctx.Done()
   975  		txCM.Wait()
   976  		w.connected.Store(false)
   977  	}()
   978  
   979  	return &wg, nil
   980  }
   981  
   982  // Connect waits for context cancellation and closes the WaitGroup. Satisfies
   983  // dex.Connector.
   984  func (w *TokenWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   985  	if w.parent.ctx == nil || w.parent.ctx.Err() != nil {
   986  		return nil, fmt.Errorf("parent wallet not connected")
   987  	}
   988  
   989  	err := w.loadContractors()
   990  	if err != nil {
   991  		return nil, err
   992  	}
   993  
   994  	w.connected.Store(true)
   995  
   996  	var wg sync.WaitGroup
   997  	wg.Add(1)
   998  	go func() {
   999  		defer wg.Done()
  1000  		select {
  1001  		case <-ctx.Done():
  1002  		case <-w.parent.ctx.Done():
  1003  			w.connected.Store(false)
  1004  		}
  1005  	}()
  1006  
  1007  	return &wg, nil
  1008  }
  1009  
  1010  // tipHeight gets the current best header's tip height.
  1011  func (w *baseWallet) tipHeight() uint64 {
  1012  	w.tipMtx.RLock()
  1013  	defer w.tipMtx.RUnlock()
  1014  	return w.currentTip.Number.Uint64()
  1015  }
  1016  
  1017  // Reconfigure attempts to reconfigure the wallet.
  1018  func (w *ETHWallet) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, currentAddress string) (restart bool, err error) {
  1019  	walletCfg, err := parseWalletConfig(cfg.Settings)
  1020  	if err != nil {
  1021  		return false, err
  1022  	}
  1023  
  1024  	gasFeeLimit := walletCfg.GasFeeLimit
  1025  	if walletCfg.GasFeeLimit == 0 {
  1026  		gasFeeLimit = defaultGasFeeLimit
  1027  	}
  1028  
  1029  	// For now, we only are supporting multiRPCClient nodes. If we re-implement
  1030  	// P2P nodes, we'll have to add protection to the node field to allow for
  1031  	// reconfiguration of type.
  1032  	// We also need to consider how to handle the pendingTxs if we switch node
  1033  	// types, since right now we only use that map for multiRPCClient.
  1034  
  1035  	if rpc, is := w.node.(*multiRPCClient); is {
  1036  		walletDir := getWalletDir(w.dir, w.net)
  1037  		providerDef := cfg.Settings[providersKey]
  1038  		var defaultProviders bool
  1039  		var endpoints []string
  1040  		if len(providerDef) > 0 {
  1041  			endpoints = strings.Split(providerDef, " ")
  1042  		} else {
  1043  			endpoints = w.defaultProviders
  1044  			defaultProviders = true
  1045  		}
  1046  
  1047  		if err := rpc.reconfigure(ctx, endpoints, w.compat, walletDir, defaultProviders); err != nil {
  1048  			return false, err
  1049  		}
  1050  	}
  1051  
  1052  	w.settingsMtx.Lock()
  1053  	w.settings = cfg.Settings
  1054  	w.settingsMtx.Unlock()
  1055  
  1056  	atomic.StoreUint64(&w.baseWallet.gasFeeLimitV, gasFeeLimit)
  1057  
  1058  	return false, nil
  1059  }
  1060  
  1061  // Reconfigure attempts to reconfigure the wallet. The token wallet has
  1062  // no configurations.
  1063  func (w *TokenWallet) Reconfigure(context.Context, *asset.WalletConfig, string) (bool, error) {
  1064  	return false, nil
  1065  }
  1066  
  1067  func (eth *baseWallet) connectedWallets() []*assetWallet {
  1068  	eth.walletsMtx.RLock()
  1069  	defer eth.walletsMtx.RUnlock()
  1070  
  1071  	m := make([]*assetWallet, 0, len(eth.wallets))
  1072  
  1073  	for _, w := range eth.wallets {
  1074  		if w.connected.Load() {
  1075  			m = append(m, w)
  1076  		}
  1077  	}
  1078  	return m
  1079  }
  1080  
  1081  func (eth *baseWallet) wallet(assetID uint32) *assetWallet {
  1082  	eth.walletsMtx.RLock()
  1083  	defer eth.walletsMtx.RUnlock()
  1084  	return eth.wallets[assetID]
  1085  }
  1086  
  1087  func (eth *baseWallet) gasFeeLimit() uint64 {
  1088  	return atomic.LoadUint64(&eth.gasFeeLimitV)
  1089  }
  1090  
  1091  // transactionGenerator is an action that uses a nonce and returns a tx, it's
  1092  // type specifier, and its value.
  1093  type transactionGenerator func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error)
  1094  
  1095  // withNonce is called with a function intended to generate a new transaction
  1096  // using the next available nonce. If the function returns a non-nil tx, the
  1097  // nonce will be treated as used, and an extendedWalletTransaction will be
  1098  // generated, stored, and queued for monitoring.
  1099  func (w *assetWallet) withNonce(ctx context.Context, f transactionGenerator) (err error) {
  1100  	w.nonceMtx.Lock()
  1101  	defer w.nonceMtx.Unlock()
  1102  	if err = nonceIsSane(w.pendingTxs, w.pendingNonceAt); err != nil {
  1103  		return err
  1104  	}
  1105  	nonce := func() *big.Int {
  1106  		n := new(big.Int).Set(w.confirmedNonceAt)
  1107  		for _, pendingTx := range w.pendingTxs {
  1108  			if pendingTx.Nonce.Cmp(n) < 0 {
  1109  				continue
  1110  			}
  1111  			if pendingTx.Nonce.Cmp(n) == 0 {
  1112  				n.Add(n, big.NewInt(1))
  1113  			} else {
  1114  				break
  1115  			}
  1116  		}
  1117  		return n
  1118  	}
  1119  
  1120  	n := nonce()
  1121  	w.log.Trace("Nonce chosen for tx generator =", n)
  1122  
  1123  	// Make a first attempt with our best-known nonce.
  1124  	tx, txType, amt, recipient, err := f(n)
  1125  	if err != nil && strings.Contains(err.Error(), "nonce too low") {
  1126  		w.log.Warnf("Too-low nonce detected. Attempting recovery")
  1127  		confirmedNonceAt, pendingNonceAt, err := w.node.nonce(ctx)
  1128  		if err != nil {
  1129  			return fmt.Errorf("error during too-low nonce recovery: %v", err)
  1130  		}
  1131  		w.confirmedNonceAt = confirmedNonceAt
  1132  		w.pendingNonceAt = pendingNonceAt
  1133  		if newNonce := nonce(); newNonce != n {
  1134  			n = newNonce
  1135  			// Try again.
  1136  			tx, txType, amt, recipient, err = f(n)
  1137  			if err != nil {
  1138  				return err
  1139  			}
  1140  			w.log.Info("Nonce recovered and transaction broadcast")
  1141  		} else {
  1142  			return fmt.Errorf("best RPC nonce %d not better than our best nonce %d", newNonce, n)
  1143  		}
  1144  	}
  1145  
  1146  	if tx != nil {
  1147  		et := w.extendedTx(tx, txType, amt, recipient)
  1148  		w.pendingTxs = append(w.pendingTxs, et)
  1149  		if n.Cmp(w.pendingNonceAt) >= 0 {
  1150  			w.pendingNonceAt.Add(n, big.NewInt(1))
  1151  		}
  1152  		w.emitTransactionNote(et.WalletTransaction, true)
  1153  		w.log.Tracef("Transaction %s generated for nonce %s", et.ID, n)
  1154  	}
  1155  	return err
  1156  }
  1157  
  1158  // nonceIsSane performs sanity checks on pending txs.
  1159  func nonceIsSane(pendingTxs []*extendedWalletTx, pendingNonceAt *big.Int) error {
  1160  	if len(pendingTxs) == 0 && pendingNonceAt == nil {
  1161  		return errors.New("no pending txs and no best nonce")
  1162  	}
  1163  	var lastNonce uint64
  1164  	var numNotIndexed, confirmedTip int
  1165  	for i, pendingTx := range pendingTxs {
  1166  		if !pendingTx.savedToDB {
  1167  			return errors.New("tx database problem detected")
  1168  		}
  1169  		nonce := pendingTx.Nonce.Uint64()
  1170  		if nonce < lastNonce {
  1171  			return fmt.Errorf("pending txs not sorted")
  1172  		}
  1173  		if pendingTx.Confirmed || pendingTx.BlockNumber != 0 {
  1174  			if confirmedTip != i {
  1175  				return fmt.Errorf("confirmed tx sequence error. pending tx %s is confirmed but older txs were not", pendingTx.ID)
  1176  
  1177  			}
  1178  			confirmedTip = i + 1
  1179  			continue
  1180  		}
  1181  		lastNonce = nonce
  1182  		age := pendingTx.age()
  1183  		// Only allow a handful of txs that we haven't been seen on-chain yet.
  1184  		if age > stateUpdateTick*10 {
  1185  			numNotIndexed++
  1186  		}
  1187  		if age >= txAgeOut {
  1188  			// If any tx is unindexed and aged out, wait for user to fix it.
  1189  			return fmt.Errorf("tx %s is aged out. waiting for user to take action", pendingTx.ID)
  1190  		}
  1191  
  1192  	}
  1193  	if numNotIndexed >= maxUnindexedTxs {
  1194  		return fmt.Errorf("%d unindexed txs has reached the limit of %d", numNotIndexed, maxUnindexedTxs)
  1195  	}
  1196  	return nil
  1197  }
  1198  
  1199  // tokenWalletConfig is the configuration options for token wallets.
  1200  type tokenWalletConfig struct {
  1201  	// LimitAllowance disabled for now.
  1202  	// See https://github.com/decred/dcrdex/pull/1394#discussion_r780479402.
  1203  	// LimitAllowance bool `ini:"limitAllowance"`
  1204  }
  1205  
  1206  // parseTokenWalletConfig parses the settings map into a *tokenWalletConfig.
  1207  func parseTokenWalletConfig(settings map[string]string) (cfg *tokenWalletConfig, err error) {
  1208  	cfg = new(tokenWalletConfig)
  1209  	err = config.Unmapify(settings, cfg)
  1210  	if err != nil {
  1211  		return nil, fmt.Errorf("error parsing wallet config: %w", err)
  1212  	}
  1213  	return cfg, nil
  1214  }
  1215  
  1216  // CreateTokenWallet "creates" a wallet for a token. There is really nothing
  1217  // to do, except check that the token exists.
  1218  func (w *baseWallet) CreateTokenWallet(tokenID uint32, _ map[string]string) error {
  1219  	// Just check that the token exists for now.
  1220  	if w.tokens[tokenID] == nil {
  1221  		return fmt.Errorf("token not found for asset ID %d", tokenID)
  1222  	}
  1223  	return nil
  1224  }
  1225  
  1226  // OpenTokenWallet creates a new TokenWallet.
  1227  func (w *ETHWallet) OpenTokenWallet(tokenCfg *asset.TokenConfig) (asset.Wallet, error) {
  1228  	token, found := w.tokens[tokenCfg.AssetID]
  1229  	if !found {
  1230  		return nil, fmt.Errorf("token %d not found", tokenCfg.AssetID)
  1231  	}
  1232  
  1233  	cfg, err := parseTokenWalletConfig(tokenCfg.Settings)
  1234  	if err != nil {
  1235  		return nil, err
  1236  	}
  1237  
  1238  	netToken := token.NetTokens[w.net]
  1239  	if netToken == nil || len(netToken.SwapContracts) == 0 {
  1240  		return nil, fmt.Errorf("could not find token with ID %d on network %s", w.assetID, w.net)
  1241  	}
  1242  
  1243  	var maxSwapGas, maxRedeemGas uint64
  1244  	for _, contract := range netToken.SwapContracts {
  1245  		if contract.Gas.Swap > maxSwapGas {
  1246  			maxSwapGas = contract.Gas.Swap
  1247  		}
  1248  		if contract.Gas.Redeem > maxRedeemGas {
  1249  			maxRedeemGas = contract.Gas.Redeem
  1250  		}
  1251  	}
  1252  
  1253  	txGasLimit := perTxGasLimit(atomic.LoadUint64(&w.gasFeeLimitV))
  1254  
  1255  	if maxSwapGas == 0 || txGasLimit < maxSwapGas {
  1256  		return nil, errors.New("max swaps cannot be zero or undefined")
  1257  	}
  1258  	if maxRedeemGas == 0 || txGasLimit < maxRedeemGas {
  1259  		return nil, errors.New("max redeems cannot be zero or undefined")
  1260  	}
  1261  
  1262  	contracts := make(map[uint32]common.Address)
  1263  	gases := make(map[uint32]*dexeth.Gases)
  1264  	for ver, c := range netToken.SwapContracts {
  1265  		contracts[ver] = c.Address
  1266  		gases[ver] = &c.Gas
  1267  	}
  1268  
  1269  	aw := &assetWallet{
  1270  		baseWallet:         w.baseWallet,
  1271  		log:                w.baseWallet.log.SubLogger(strings.ToUpper(dex.BipIDSymbol(tokenCfg.AssetID))),
  1272  		assetID:            tokenCfg.AssetID,
  1273  		versionedContracts: contracts,
  1274  		versionedGases:     gases,
  1275  		maxSwapGas:         maxSwapGas,
  1276  		maxRedeemGas:       maxRedeemGas,
  1277  		emit:               tokenCfg.Emit,
  1278  		peersChange:        tokenCfg.PeersChange,
  1279  		findRedemptionReqs: make(map[[32]byte]*findRedemptionRequest),
  1280  		pendingApprovals:   make(map[uint32]*pendingApproval),
  1281  		approvalCache:      make(map[uint32]bool),
  1282  		contractors:        make(map[uint32]contractor),
  1283  		evmify:             token.AtomicToEVM,
  1284  		atomize:            token.EVMToAtomic,
  1285  		ui:                 token.UnitInfo,
  1286  		wi: asset.WalletInfo{
  1287  			Name:              token.Name,
  1288  			SupportedVersions: w.wi.SupportedVersions,
  1289  			UnitInfo:          token.UnitInfo,
  1290  		},
  1291  		pendingTxCheckBal: new(big.Int),
  1292  	}
  1293  
  1294  	w.baseWallet.walletsMtx.Lock()
  1295  	w.baseWallet.wallets[tokenCfg.AssetID] = aw
  1296  	w.baseWallet.walletsMtx.Unlock()
  1297  
  1298  	return &TokenWallet{
  1299  		assetWallet: aw,
  1300  		cfg:         cfg,
  1301  		parent:      w.assetWallet,
  1302  		token:       token,
  1303  		netToken:    netToken,
  1304  	}, nil
  1305  }
  1306  
  1307  // OwnsDepositAddress indicates if an address belongs to the wallet. The address
  1308  // need not be a EIP55-compliant formatted address. It may or may not have a 0x
  1309  // prefix, and case is not important.
  1310  //
  1311  // In Ethereum, an address is an account.
  1312  func (eth *baseWallet) OwnsDepositAddress(address string) (bool, error) {
  1313  	if !common.IsHexAddress(address) {
  1314  		return false, errors.New("invalid address")
  1315  	}
  1316  	addr := common.HexToAddress(address)
  1317  	return addr == eth.addr, nil
  1318  }
  1319  
  1320  func (w *assetWallet) amtString(amt uint64) string {
  1321  	return fmt.Sprintf("%s %s", w.ui.ConventionalString(amt), w.ui.Conventional.Unit)
  1322  }
  1323  
  1324  // fundReserveType represents the various uses for which funds need to be locked:
  1325  // initiations, redemptions, and refunds.
  1326  type fundReserveType uint32
  1327  
  1328  const (
  1329  	initiationReserve fundReserveType = iota
  1330  	redemptionReserve
  1331  	refundReserve
  1332  )
  1333  
  1334  func (f fundReserveType) String() string {
  1335  	switch f {
  1336  	case initiationReserve:
  1337  		return "initiation"
  1338  	case redemptionReserve:
  1339  		return "redemption"
  1340  	case refundReserve:
  1341  		return "refund"
  1342  	default:
  1343  		return ""
  1344  	}
  1345  }
  1346  
  1347  // fundReserveOfType returns a pointer to the funds reserved for a particular
  1348  // use case.
  1349  func (w *assetWallet) fundReserveOfType(t fundReserveType) *uint64 {
  1350  	switch t {
  1351  	case initiationReserve:
  1352  		return &w.lockedFunds.initiateReserves
  1353  	case redemptionReserve:
  1354  		return &w.lockedFunds.redemptionReserves
  1355  	case refundReserve:
  1356  		return &w.lockedFunds.refundReserves
  1357  	default:
  1358  		panic(fmt.Sprintf("invalid fund reserve type: %v", t))
  1359  	}
  1360  }
  1361  
  1362  // lockFunds locks funds for a use case.
  1363  func (w *assetWallet) lockFunds(amt uint64, t fundReserveType) error {
  1364  	balance, err := w.balance()
  1365  	if err != nil {
  1366  		return err
  1367  	}
  1368  
  1369  	if balance.Available < amt {
  1370  		return fmt.Errorf("attempting to lock more %s for %s than is currently available. %d > %d %s",
  1371  			dex.BipIDSymbol(w.assetID), t, amt, balance.Available, w.ui.AtomicUnit)
  1372  	}
  1373  
  1374  	w.lockedFunds.mtx.Lock()
  1375  	defer w.lockedFunds.mtx.Unlock()
  1376  
  1377  	*w.fundReserveOfType(t) += amt
  1378  	return nil
  1379  }
  1380  
  1381  // unlockFunds unlocks funds for a use case.
  1382  func (w *assetWallet) unlockFunds(amt uint64, t fundReserveType) {
  1383  	w.lockedFunds.mtx.Lock()
  1384  	defer w.lockedFunds.mtx.Unlock()
  1385  
  1386  	reserve := w.fundReserveOfType(t)
  1387  
  1388  	if *reserve < amt {
  1389  		*reserve = 0
  1390  		w.log.Errorf("attempting to unlock more for %s than is currently locked - %d > %d. "+
  1391  			"clearing all locked funds", t, amt, *reserve)
  1392  		return
  1393  	}
  1394  
  1395  	*reserve -= amt
  1396  }
  1397  
  1398  // amountLocked returns the total amount currently locked.
  1399  func (w *assetWallet) amountLocked() uint64 {
  1400  	w.lockedFunds.mtx.RLock()
  1401  	defer w.lockedFunds.mtx.RUnlock()
  1402  	return w.lockedFunds.initiateReserves + w.lockedFunds.redemptionReserves + w.lockedFunds.refundReserves
  1403  }
  1404  
  1405  // Balance returns the available and locked funds (token or eth).
  1406  func (w *assetWallet) Balance() (*asset.Balance, error) {
  1407  	return w.balance()
  1408  }
  1409  
  1410  // balance returns the total available funds in the account.
  1411  func (w *assetWallet) balance() (*asset.Balance, error) {
  1412  	bal, err := w.balanceWithTxPool()
  1413  	if err != nil {
  1414  		return nil, fmt.Errorf("pending balance error: %w", err)
  1415  	}
  1416  
  1417  	locked := w.amountLocked()
  1418  	var available uint64
  1419  	if w.atomize(bal.Current) > locked+w.atomize(bal.PendingOut) {
  1420  		available = w.atomize(bal.Current) - locked - w.atomize(bal.PendingOut)
  1421  	}
  1422  
  1423  	return &asset.Balance{
  1424  		Available: available,
  1425  		Locked:    locked,
  1426  		Immature:  w.atomize(bal.PendingIn),
  1427  	}, nil
  1428  }
  1429  
  1430  // MaxOrder generates information about the maximum order size and associated
  1431  // fees that the wallet can support for the given DEX configuration. The fees are an
  1432  // estimate based on current network conditions, and will be <= the fees
  1433  // associated with nfo.MaxFeeRate. For quote assets, the caller will have to
  1434  // calculate lotSize based on a rate conversion from the base asset's lot size.
  1435  func (w *ETHWallet) MaxOrder(ord *asset.MaxOrderForm) (*asset.SwapEstimate, error) {
  1436  	return w.maxOrder(ord.LotSize, ord.MaxFeeRate, ord.AssetVersion,
  1437  		ord.RedeemVersion, ord.RedeemAssetID, nil)
  1438  }
  1439  
  1440  // MaxOrder generates information about the maximum order size and associated
  1441  // fees that the wallet can support for the given DEX configuration.
  1442  func (w *TokenWallet) MaxOrder(ord *asset.MaxOrderForm) (*asset.SwapEstimate, error) {
  1443  	return w.maxOrder(ord.LotSize, ord.MaxFeeRate, ord.AssetVersion,
  1444  		ord.RedeemVersion, ord.RedeemAssetID, w.parent)
  1445  }
  1446  
  1447  func (w *assetWallet) maxOrder(lotSize uint64, maxFeeRate uint64, ver uint32,
  1448  	redeemVer, redeemAssetID uint32, feeWallet *assetWallet) (*asset.SwapEstimate, error) {
  1449  	balance, err := w.Balance()
  1450  	if err != nil {
  1451  		return nil, err
  1452  	}
  1453  	// Get the refund gas.
  1454  	if g := w.gases(ver); g == nil {
  1455  		return nil, fmt.Errorf("no gas table")
  1456  	}
  1457  
  1458  	g, err := w.initGasEstimate(1, ver, redeemVer, redeemAssetID)
  1459  	liveEstimateFailed := errors.Is(err, LiveEstimateFailedError)
  1460  	if err != nil && !liveEstimateFailed {
  1461  		return nil, fmt.Errorf("gasEstimate error: %w", err)
  1462  	}
  1463  
  1464  	refundCost := g.Refund * maxFeeRate
  1465  	oneFee := g.oneGas * maxFeeRate
  1466  	feeReservesPerLot := oneFee + refundCost
  1467  	var lots uint64
  1468  	if feeWallet == nil {
  1469  		lots = balance.Available / (lotSize + feeReservesPerLot)
  1470  	} else { // token
  1471  		lots = balance.Available / lotSize
  1472  		parentBal, err := feeWallet.Balance()
  1473  		if err != nil {
  1474  			return nil, fmt.Errorf("error getting base chain balance: %w", err)
  1475  		}
  1476  		feeLots := parentBal.Available / feeReservesPerLot
  1477  		if feeLots < lots {
  1478  			w.log.Infof("MaxOrder reducing lots because of low fee reserves: %d -> %d", lots, feeLots)
  1479  			lots = feeLots
  1480  		}
  1481  	}
  1482  
  1483  	if lots < 1 {
  1484  		return &asset.SwapEstimate{
  1485  			FeeReservesPerLot: feeReservesPerLot,
  1486  		}, nil
  1487  	}
  1488  	return w.estimateSwap(lots, lotSize, maxFeeRate, ver, feeReservesPerLot)
  1489  }
  1490  
  1491  // PreSwap gets order estimates based on the available funds and the wallet
  1492  // configuration.
  1493  func (w *ETHWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) {
  1494  	return w.preSwap(req, nil)
  1495  }
  1496  
  1497  // PreSwap gets order estimates based on the available funds and the wallet
  1498  // configuration.
  1499  func (w *TokenWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) {
  1500  	return w.preSwap(req, w.parent)
  1501  }
  1502  
  1503  func (w *assetWallet) preSwap(req *asset.PreSwapForm, feeWallet *assetWallet) (*asset.PreSwap, error) {
  1504  	maxEst, err := w.maxOrder(req.LotSize, req.MaxFeeRate, req.Version,
  1505  		req.RedeemVersion, req.RedeemAssetID, feeWallet)
  1506  	if err != nil {
  1507  		return nil, err
  1508  	}
  1509  
  1510  	if maxEst.Lots < req.Lots {
  1511  		return nil, fmt.Errorf("%d lots available for %d-lot order", maxEst.Lots, req.Lots)
  1512  	}
  1513  
  1514  	est, err := w.estimateSwap(req.Lots, req.LotSize, req.MaxFeeRate,
  1515  		req.Version, maxEst.FeeReservesPerLot)
  1516  	if err != nil {
  1517  		return nil, err
  1518  	}
  1519  
  1520  	return &asset.PreSwap{
  1521  		Estimate: est,
  1522  	}, nil
  1523  }
  1524  
  1525  // MaxFundingFees returns 0 because ETH does not have funding fees.
  1526  func (w *baseWallet) MaxFundingFees(_ uint32, _ uint64, _ map[string]string) uint64 {
  1527  	return 0
  1528  }
  1529  
  1530  // SingleLotSwapRefundFees returns the fees for a swap transaction for a single lot.
  1531  func (w *assetWallet) SingleLotSwapRefundFees(version uint32, feeSuggestion uint64, _ bool) (swapFees uint64, refundFees uint64, err error) {
  1532  	if version == asset.VersionNewest {
  1533  		version = contractVersionNewest
  1534  	}
  1535  	g := w.gases(version)
  1536  	if g == nil {
  1537  		return 0, 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version)
  1538  	}
  1539  	return g.Swap * feeSuggestion, g.Refund * feeSuggestion, nil
  1540  }
  1541  
  1542  // estimateSwap prepares an *asset.SwapEstimate. The estimate does not include
  1543  // funds that might be locked for refunds.
  1544  func (w *assetWallet) estimateSwap(
  1545  	lots, lotSize uint64, maxFeeRate uint64, ver uint32, feeReservesPerLot uint64,
  1546  ) (*asset.SwapEstimate, error) {
  1547  
  1548  	if lots == 0 {
  1549  		return &asset.SwapEstimate{
  1550  			FeeReservesPerLot: feeReservesPerLot,
  1551  		}, nil
  1552  	}
  1553  
  1554  	feeRate, err := w.currentFeeRate(w.ctx)
  1555  	if err != nil {
  1556  		return nil, err
  1557  	}
  1558  	feeRateGwei := dexeth.WeiToGweiCeil(feeRate)
  1559  	// This is an estimate, so we use the (lower) live gas estimates.
  1560  	oneSwap, err := w.estimateInitGas(w.ctx, 1, ver)
  1561  	if err != nil {
  1562  		return nil, fmt.Errorf("(%d) error estimating swap gas: %v", w.assetID, err)
  1563  	}
  1564  
  1565  	// NOTE: nSwap is neither best nor worst case. A single match can be
  1566  	// multiple lots. See RealisticBestCase descriptions.
  1567  
  1568  	value := lots * lotSize
  1569  	oneGasMax := oneSwap * lots
  1570  	maxFees := oneGasMax * maxFeeRate
  1571  
  1572  	return &asset.SwapEstimate{
  1573  		Lots:               lots,
  1574  		Value:              value,
  1575  		MaxFees:            maxFees,
  1576  		RealisticWorstCase: oneGasMax * feeRateGwei,
  1577  		RealisticBestCase:  oneSwap * feeRateGwei, // not even batch, just perfect match
  1578  		FeeReservesPerLot:  feeReservesPerLot,
  1579  	}, nil
  1580  }
  1581  
  1582  // gases gets the gas table for the specified contract version.
  1583  func (w *assetWallet) gases(contractVer uint32) *dexeth.Gases {
  1584  	return gases(contractVer, w.versionedGases)
  1585  }
  1586  
  1587  // PreRedeem generates an estimate of the range of redemption fees that could
  1588  // be assessed.
  1589  func (w *assetWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, error) {
  1590  	oneRedeem, nRedeem, err := w.redeemGas(int(req.Lots), req.Version)
  1591  	if err != nil {
  1592  		return nil, err
  1593  	}
  1594  
  1595  	return &asset.PreRedeem{
  1596  		Estimate: &asset.RedeemEstimate{
  1597  			RealisticBestCase:  nRedeem * req.FeeSuggestion,
  1598  			RealisticWorstCase: oneRedeem * req.Lots * req.FeeSuggestion,
  1599  		},
  1600  	}, nil
  1601  }
  1602  
  1603  // SingleLotRedeemFees returns the fees for a redeem transaction for a single lot.
  1604  func (w *assetWallet) SingleLotRedeemFees(version uint32, feeSuggestion uint64) (fees uint64, err error) {
  1605  	if version == asset.VersionNewest {
  1606  		version = contractVersionNewest
  1607  	}
  1608  	g := w.gases(version)
  1609  	if g == nil {
  1610  		return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version)
  1611  	}
  1612  
  1613  	return g.Redeem * feeSuggestion, nil
  1614  }
  1615  
  1616  // coin implements the asset.Coin interface for ETH
  1617  type coin struct {
  1618  	id common.Hash
  1619  	// the value can be determined from the coin id, but for some
  1620  	// coin ids a lookup would be required from the blockchain to
  1621  	// determine its value, so this field is used as a cache.
  1622  	value uint64
  1623  }
  1624  
  1625  // ID is the ETH coins ID. For functions related to funding an order,
  1626  // the ID must contain an encoded fundingCoinID, but when returned from
  1627  // Swap, it will contain the transaction hash used to initiate the swap.
  1628  func (c *coin) ID() dex.Bytes {
  1629  	return c.id[:]
  1630  }
  1631  
  1632  func (c *coin) TxID() string {
  1633  	return c.String()
  1634  }
  1635  
  1636  // String is a string representation of the coin.
  1637  func (c *coin) String() string {
  1638  	return c.id.String()
  1639  }
  1640  
  1641  // Value returns the value in gwei of the coin.
  1642  func (c *coin) Value() uint64 {
  1643  	return c.value
  1644  }
  1645  
  1646  var _ asset.Coin = (*coin)(nil)
  1647  
  1648  func (eth *ETHWallet) createFundingCoin(amount uint64) *fundingCoin {
  1649  	return createFundingCoin(eth.addr, amount)
  1650  }
  1651  
  1652  func (eth *TokenWallet) createTokenFundingCoin(amount, fees uint64) *tokenFundingCoin {
  1653  	return createTokenFundingCoin(eth.addr, amount, fees)
  1654  }
  1655  
  1656  // FundOrder locks value for use in an order.
  1657  func (w *ETHWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint64, error) {
  1658  	if ord.MaxFeeRate < dexeth.MinGasTipCap {
  1659  		return nil, nil, 0, fmt.Errorf("%v: server's max fee rate is lower than our min gas tip cap. %d < %d",
  1660  			dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, dexeth.MinGasTipCap)
  1661  	}
  1662  
  1663  	if w.gasFeeLimit() < ord.MaxFeeRate {
  1664  		return nil, nil, 0, fmt.Errorf(
  1665  			"%v: server's max fee rate %v higher than configured fee rate limit %v",
  1666  			dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit())
  1667  	}
  1668  
  1669  	g, err := w.initGasEstimate(int(ord.MaxSwapCount), ord.Version,
  1670  		ord.RedeemVersion, ord.RedeemAssetID)
  1671  	if err != nil {
  1672  		return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err)
  1673  	}
  1674  
  1675  	ethToLock := ord.MaxFeeRate*g.Swap*ord.MaxSwapCount + ord.Value
  1676  	// Note: In a future refactor, we could lock the redemption funds here too
  1677  	// and signal to the user so that they don't call `RedeemN`. This has the
  1678  	// same net effect, but avoids a lockFunds -> unlockFunds for us and likely
  1679  	// some work for the caller as well. We can't just always do it that way and
  1680  	// remove RedeemN, since we can't guarantee that the redemption asset is in
  1681  	// our fee-family. though it could still be an AccountRedeemer.
  1682  	w.log.Debugf("Locking %s to swap %s in up to %d swaps at a fee rate of %d gwei/gas using up to %d gas per swap",
  1683  		w.amtString(ethToLock), w.amtString(ord.Value), ord.MaxSwapCount, ord.MaxFeeRate, g.Swap)
  1684  
  1685  	coin := w.createFundingCoin(ethToLock)
  1686  
  1687  	if err = w.lockFunds(ethToLock, initiationReserve); err != nil {
  1688  		return nil, nil, 0, err
  1689  	}
  1690  
  1691  	return asset.Coins{coin}, []dex.Bytes{nil}, 0, nil
  1692  }
  1693  
  1694  // FundOrder locks value for use in an order.
  1695  func (w *TokenWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint64, error) {
  1696  	if ord.MaxFeeRate < dexeth.MinGasTipCap {
  1697  		return nil, nil, 0, fmt.Errorf("%v: server's max fee rate is lower than our min gas tip cap. %d < %d",
  1698  			dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, dexeth.MinGasTipCap)
  1699  	}
  1700  
  1701  	if w.gasFeeLimit() < ord.MaxFeeRate {
  1702  		return nil, nil, 0, fmt.Errorf(
  1703  			"%v: server's max fee rate %v higher than configured fee rate limit %v",
  1704  			dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit())
  1705  	}
  1706  
  1707  	approvalStatus, err := w.approvalStatus(ord.Version)
  1708  	if err != nil {
  1709  		return nil, nil, 0, fmt.Errorf("error getting approval status: %v", err)
  1710  	}
  1711  	switch approvalStatus {
  1712  	case asset.NotApproved:
  1713  		return nil, nil, 0, asset.ErrUnapprovedToken
  1714  	case asset.Pending:
  1715  		return nil, nil, 0, asset.ErrApprovalPending
  1716  	case asset.Approved:
  1717  	default:
  1718  		return nil, nil, 0, fmt.Errorf("unknown approval status %d", approvalStatus)
  1719  	}
  1720  
  1721  	g, err := w.initGasEstimate(int(ord.MaxSwapCount), ord.Version,
  1722  		ord.RedeemVersion, ord.RedeemAssetID)
  1723  	if err != nil {
  1724  		return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err)
  1725  	}
  1726  
  1727  	ethToLock := ord.MaxFeeRate * g.Swap * ord.MaxSwapCount
  1728  	var success bool
  1729  	if err = w.lockFunds(ord.Value, initiationReserve); err != nil {
  1730  		return nil, nil, 0, fmt.Errorf("error locking token funds: %v", err)
  1731  	}
  1732  	defer func() {
  1733  		if !success {
  1734  			w.unlockFunds(ord.Value, initiationReserve)
  1735  		}
  1736  	}()
  1737  
  1738  	w.log.Debugf("Locking %s to swap %s in up to %d swaps at a fee rate of %d gwei/gas using up to %d gas per swap",
  1739  		w.parent.amtString(ethToLock), w.amtString(ord.Value), ord.MaxSwapCount, ord.MaxFeeRate, g.Swap)
  1740  	if err := w.parent.lockFunds(ethToLock, initiationReserve); err != nil {
  1741  		return nil, nil, 0, err
  1742  	}
  1743  
  1744  	coin := w.createTokenFundingCoin(ord.Value, ethToLock)
  1745  
  1746  	success = true
  1747  	return asset.Coins{coin}, []dex.Bytes{nil}, 0, nil
  1748  }
  1749  
  1750  // FundMultiOrder funds multiple orders in one shot. No special handling is
  1751  // required for ETH as ETH does not over-lock during funding.
  1752  func (w *ETHWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]asset.Coins, [][]dex.Bytes, uint64, error) {
  1753  	if w.gasFeeLimit() < ord.MaxFeeRate {
  1754  		return nil, nil, 0, fmt.Errorf(
  1755  			"%v: server's max fee rate %v higher than configured fee rate limit %v",
  1756  			dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit())
  1757  	}
  1758  
  1759  	g, err := w.initGasEstimate(1, ord.Version, ord.RedeemVersion, ord.RedeemAssetID)
  1760  	if err != nil {
  1761  		return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err)
  1762  	}
  1763  
  1764  	var totalToLock uint64
  1765  	allCoins := make([]asset.Coins, 0, len(ord.Values))
  1766  	for _, value := range ord.Values {
  1767  		toLock := ord.MaxFeeRate*g.Swap*value.MaxSwapCount + value.Value
  1768  		allCoins = append(allCoins, asset.Coins{w.createFundingCoin(toLock)})
  1769  		totalToLock += toLock
  1770  	}
  1771  
  1772  	if maxLock > 0 && maxLock < totalToLock {
  1773  		return nil, nil, 0, fmt.Errorf("insufficient funds to lock %d for %d orders", totalToLock, len(ord.Values))
  1774  	}
  1775  
  1776  	if err = w.lockFunds(totalToLock, initiationReserve); err != nil {
  1777  		return nil, nil, 0, err
  1778  	}
  1779  
  1780  	redeemScripts := make([][]dex.Bytes, len(ord.Values))
  1781  	for i := range redeemScripts {
  1782  		redeemScripts[i] = []dex.Bytes{nil}
  1783  	}
  1784  
  1785  	return allCoins, redeemScripts, 0, nil
  1786  }
  1787  
  1788  // FundMultiOrder funds multiple orders in one shot. No special handling is
  1789  // required for ETH as ETH does not over-lock during funding.
  1790  func (w *TokenWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]asset.Coins, [][]dex.Bytes, uint64, error) {
  1791  	if w.gasFeeLimit() < ord.MaxFeeRate {
  1792  		return nil, nil, 0, fmt.Errorf(
  1793  			"%v: server's max fee rate %v higher than configured fee rate limit %v",
  1794  			dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit())
  1795  	}
  1796  
  1797  	approvalStatus, err := w.approvalStatus(ord.Version)
  1798  	if err != nil {
  1799  		return nil, nil, 0, fmt.Errorf("error getting approval status: %v", err)
  1800  	}
  1801  	switch approvalStatus {
  1802  	case asset.NotApproved:
  1803  		return nil, nil, 0, asset.ErrUnapprovedToken
  1804  	case asset.Pending:
  1805  		return nil, nil, 0, asset.ErrApprovalPending
  1806  	case asset.Approved:
  1807  	default:
  1808  		return nil, nil, 0, fmt.Errorf("unknown approval status %d", approvalStatus)
  1809  	}
  1810  
  1811  	g, err := w.initGasEstimate(1, ord.Version,
  1812  		ord.RedeemVersion, ord.RedeemAssetID)
  1813  	if err != nil {
  1814  		return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err)
  1815  	}
  1816  
  1817  	var totalETHToLock, totalTokenToLock uint64
  1818  	allCoins := make([]asset.Coins, 0, len(ord.Values))
  1819  	for _, value := range ord.Values {
  1820  		ethToLock := ord.MaxFeeRate * g.Swap * value.MaxSwapCount
  1821  		tokenToLock := value.Value
  1822  		allCoins = append(allCoins, asset.Coins{w.createTokenFundingCoin(tokenToLock, ethToLock)})
  1823  		totalETHToLock += ethToLock
  1824  		totalTokenToLock += tokenToLock
  1825  	}
  1826  
  1827  	var success bool
  1828  	if err = w.lockFunds(totalTokenToLock, initiationReserve); err != nil {
  1829  		return nil, nil, 0, fmt.Errorf("error locking token funds: %v", err)
  1830  	}
  1831  	defer func() {
  1832  		if !success {
  1833  			w.unlockFunds(totalTokenToLock, initiationReserve)
  1834  		}
  1835  	}()
  1836  
  1837  	if err := w.parent.lockFunds(totalETHToLock, initiationReserve); err != nil {
  1838  		return nil, nil, 0, err
  1839  	}
  1840  
  1841  	redeemScripts := make([][]dex.Bytes, len(ord.Values))
  1842  	for i := range redeemScripts {
  1843  		redeemScripts[i] = []dex.Bytes{nil}
  1844  	}
  1845  
  1846  	success = true
  1847  	return allCoins, redeemScripts, 0, nil
  1848  }
  1849  
  1850  // gasEstimates are estimates of gas required for operations involving a swap
  1851  // combination of (swap asset, redeem asset, # lots).
  1852  type gasEstimate struct {
  1853  	// The embedded are best calculated values for Swap gases, and redeem costs
  1854  	// IF the redeem asset is a fee-family asset. Otherwise Redeem and RedeemAdd
  1855  	// are zero.
  1856  	dexeth.Gases
  1857  	// Additional fields may be based on live estimates of the swap. Both oneGas
  1858  	// and nGas will include both swap and redeem gas, but note that the redeem
  1859  	// gas may be zero if the redeemed asset is not ETH or an ETH token.
  1860  	oneGas, nGas, nSwap, nRedeem uint64
  1861  }
  1862  
  1863  // initGasEstimate gets the best available gas estimate for n initiations. A
  1864  // live estimate is checked against the server's configured values and our own
  1865  // known values and errors or logs generated in certain cases.
  1866  func (w *assetWallet) initGasEstimate(n int, initVer, redeemVer, redeemAssetID uint32) (est *gasEstimate, err error) {
  1867  	est = new(gasEstimate)
  1868  
  1869  	// Get the refund gas.
  1870  	if g := w.gases(initVer); g == nil {
  1871  		return nil, fmt.Errorf("no gas table")
  1872  	} else { // scoping g
  1873  		est.Refund = g.Refund
  1874  	}
  1875  
  1876  	est.Swap, est.nSwap, err = w.swapGas(n, initVer)
  1877  	if err != nil && !errors.Is(err, LiveEstimateFailedError) {
  1878  		return nil, err
  1879  	}
  1880  	// Could be LiveEstimateFailedError. Still populate static estimates if we
  1881  	// couldn't get live. Error is still propagated.
  1882  	est.oneGas = est.Swap
  1883  	est.nGas = est.nSwap
  1884  
  1885  	if redeemW := w.wallet(redeemAssetID); redeemW != nil {
  1886  		var er error
  1887  		est.Redeem, est.nRedeem, er = redeemW.redeemGas(n, redeemVer)
  1888  		if err != nil {
  1889  			return nil, fmt.Errorf("error calculating fee-family redeem gas: %w", er)
  1890  		}
  1891  		est.oneGas += est.Redeem
  1892  		est.nGas += est.nRedeem
  1893  	}
  1894  
  1895  	return
  1896  }
  1897  
  1898  // swapGas estimates gas for a number of initiations. swapGas will error if we
  1899  // cannot get a live estimate from the contractor, which will happen if the
  1900  // wallet has no balance. A live gas estimate will always be attempted, and used
  1901  // if our expected gas values are lower (anomalous).
  1902  func (w *assetWallet) swapGas(n int, ver uint32) (oneSwap, nSwap uint64, err error) {
  1903  	g := w.gases(ver)
  1904  	if g == nil {
  1905  		return 0, 0, fmt.Errorf("no gases known for %d version %d", w.assetID, ver)
  1906  	}
  1907  	oneSwap = g.Swap
  1908  
  1909  	// We have no way of updating the value of SwapAdd without a version change,
  1910  	// but we're not gonna worry about accuracy for nSwap, since it's only used
  1911  	// for estimates and never for dex-validated values like order funding.
  1912  	nSwap = oneSwap + uint64(n-1)*g.SwapAdd
  1913  
  1914  	// The amount we can estimate and ultimately the amount we can use in a
  1915  	// single transaction is limited by the block gas limit or the tx gas
  1916  	// limit. Core will use the largest gas among all versions when
  1917  	// determining the maximum number of swaps that can be in one
  1918  	// transaction. Limit our gas estimate to the same number of swaps.
  1919  	nMax := n
  1920  	maxSwaps, _ := w.maxSwapsAndRedeems()
  1921  	var nRemain, nFull int
  1922  	if uint64(n) > maxSwaps {
  1923  		nMax = int(maxSwaps)
  1924  		nFull = n / nMax
  1925  		nSwap = (oneSwap + uint64(nMax-1)*g.SwapAdd) * uint64(nFull)
  1926  		nRemain = n % nMax
  1927  		if nRemain != 0 {
  1928  			nSwap += oneSwap + uint64(nRemain-1)*g.SwapAdd
  1929  		}
  1930  	}
  1931  
  1932  	// If a live estimate is greater than our estimate from configured values,
  1933  	// use the live estimate with a warning.
  1934  	gasEst, err := w.estimateInitGas(w.ctx, nMax, ver)
  1935  	if err != nil {
  1936  		err = errors.Join(err, LiveEstimateFailedError)
  1937  		return
  1938  		// Or we could go with what we know? But this estimate error could be a
  1939  		// hint that the transaction would fail, and we don't have a way to
  1940  		// recover from that. Play it safe and allow caller to retry assuming
  1941  		// the error is transient with the provider.
  1942  		// w.log.Errorf("(%d) error estimating swap gas (using expected gas cap instead): %v", w.assetID, err)
  1943  		// return oneSwap, nSwap, true, nil
  1944  	}
  1945  	if nMax != n {
  1946  		// If we needed to adjust the max earlier, and the estimate did
  1947  		// not error, multiply the estimate by the number of full
  1948  		// transactions and add the estimate of the remainder.
  1949  		gasEst *= uint64(nFull)
  1950  		if nRemain > 0 {
  1951  			remainEst, err := w.estimateInitGas(w.ctx, nRemain, ver)
  1952  			if err != nil {
  1953  				w.log.Errorf("(%d) error estimating swap gas for remainder: %v", w.assetID, err)
  1954  				return 0, 0, err
  1955  			}
  1956  			gasEst += remainEst
  1957  		}
  1958  	}
  1959  	if gasEst > nSwap {
  1960  		w.log.Warnf("Swap gas estimate %d is greater than the server's configured value %d. Using live estimate + 10%%.", gasEst, nSwap)
  1961  		nSwap = gasEst * 11 / 10 // 10% buffer
  1962  		if n == 1 && nSwap > oneSwap {
  1963  			oneSwap = nSwap
  1964  		}
  1965  	}
  1966  	return
  1967  }
  1968  
  1969  // redeemGas gets an accurate estimate for redemption gas. We allow a DEX server
  1970  // some latitude in adjusting the redemption gas, up to 2x our local estimate.
  1971  func (w *assetWallet) redeemGas(n int, ver uint32) (oneGas, nGas uint64, err error) {
  1972  	g := w.gases(ver)
  1973  	if g == nil {
  1974  		return 0, 0, fmt.Errorf("no gas table for redemption asset %d", w.assetID)
  1975  	}
  1976  	redeemGas := g.Redeem
  1977  	// Not concerned with the accuracy of nGas. It's never used outside of
  1978  	// best case estimates.
  1979  	return redeemGas, redeemGas + (uint64(n)-1)*g.RedeemAdd, nil
  1980  }
  1981  
  1982  // approvalGas gets the best available estimate for an approval tx, which is
  1983  // the greater of the asset's registered value and a live estimate. It is an
  1984  // error if a live estimate cannot be retrieved, which will be the case if the
  1985  // user's eth balance is insufficient to cover tx fees for the approval.
  1986  func (w *assetWallet) approvalGas(newGas *big.Int, ver uint32) (uint64, error) {
  1987  	ourGas := w.gases(ver)
  1988  	if ourGas == nil {
  1989  		return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, ver)
  1990  	}
  1991  
  1992  	approveGas := ourGas.Approve
  1993  
  1994  	if approveEst, err := w.estimateApproveGas(newGas); err != nil {
  1995  		return 0, fmt.Errorf("error estimating approve gas: %v", err)
  1996  	} else if approveEst > approveGas {
  1997  		w.log.Warnf("Approve gas estimate %d is greater than the expected value %d. Using live estimate + 10%%.", approveEst, approveGas)
  1998  		return approveEst * 11 / 10, nil
  1999  	}
  2000  	return approveGas, nil
  2001  }
  2002  
  2003  // ReturnCoins unlocks coins. This would be necessary in the case of a
  2004  // canceled order.
  2005  func (w *ETHWallet) ReturnCoins(coins asset.Coins) error {
  2006  	var amt uint64
  2007  	for _, ci := range coins {
  2008  		c, is := ci.(*fundingCoin)
  2009  		if !is {
  2010  			return fmt.Errorf("unknown coin type %T", c)
  2011  		}
  2012  		if c.addr != w.addr {
  2013  			return fmt.Errorf("coin is not funded by this wallet. coin address %s != our address %s", c.addr, w.addr)
  2014  		}
  2015  		amt += c.amt
  2016  
  2017  	}
  2018  	w.unlockFunds(amt, initiationReserve)
  2019  	return nil
  2020  }
  2021  
  2022  // ReturnCoins unlocks coins. This would be necessary in the case of a
  2023  // canceled order.
  2024  func (w *TokenWallet) ReturnCoins(coins asset.Coins) error {
  2025  	var amt, fees uint64
  2026  	for _, ci := range coins {
  2027  		c, is := ci.(*tokenFundingCoin)
  2028  		if !is {
  2029  			return fmt.Errorf("unknown coin type %T", c)
  2030  		}
  2031  		if c.addr != w.addr {
  2032  			return fmt.Errorf("coin is not funded by this wallet. coin address %s != our address %s", c.addr, w.addr)
  2033  		}
  2034  		amt += c.amt
  2035  		fees += c.fees
  2036  	}
  2037  	if fees > 0 {
  2038  		w.parent.unlockFunds(fees, initiationReserve)
  2039  	}
  2040  	w.unlockFunds(amt, initiationReserve)
  2041  	return nil
  2042  }
  2043  
  2044  // FundingCoins gets funding coins for the coin IDs. The coins are locked. This
  2045  // method might be called to reinitialize an order from data stored externally.
  2046  func (w *ETHWallet) FundingCoins(ids []dex.Bytes) (asset.Coins, error) {
  2047  	coins := make([]asset.Coin, 0, len(ids))
  2048  	var amt uint64
  2049  	for _, id := range ids {
  2050  		c, err := decodeFundingCoin(id)
  2051  		if err != nil {
  2052  			return nil, fmt.Errorf("error decoding funding coin ID: %w", err)
  2053  		}
  2054  		if c.addr != w.addr {
  2055  			return nil, fmt.Errorf("funding coin has wrong address. %s != %s", c.addr, w.addr)
  2056  		}
  2057  		amt += c.amt
  2058  		coins = append(coins, c)
  2059  	}
  2060  	if err := w.lockFunds(amt, initiationReserve); err != nil {
  2061  		return nil, err
  2062  	}
  2063  
  2064  	return coins, nil
  2065  }
  2066  
  2067  // FundingCoins gets funding coins for the coin IDs. The coins are locked. This
  2068  // method might be called to reinitialize an order from data stored externally.
  2069  func (w *TokenWallet) FundingCoins(ids []dex.Bytes) (asset.Coins, error) {
  2070  	coins := make([]asset.Coin, 0, len(ids))
  2071  	var amt, fees uint64
  2072  	for _, id := range ids {
  2073  		c, err := decodeTokenFundingCoin(id)
  2074  		if err != nil {
  2075  			return nil, fmt.Errorf("error decoding funding coin ID: %w", err)
  2076  		}
  2077  		if c.addr != w.addr {
  2078  			return nil, fmt.Errorf("funding coin has wrong address. %s != %s", c.addr, w.addr)
  2079  		}
  2080  
  2081  		amt += c.amt
  2082  		fees += c.fees
  2083  		coins = append(coins, c)
  2084  	}
  2085  
  2086  	var success bool
  2087  	if fees > 0 {
  2088  		if err := w.parent.lockFunds(fees, initiationReserve); err != nil {
  2089  			return nil, fmt.Errorf("error unlocking parent asset fees: %w", err)
  2090  		}
  2091  		defer func() {
  2092  			if !success {
  2093  				w.parent.unlockFunds(fees, initiationReserve)
  2094  			}
  2095  		}()
  2096  	}
  2097  	if err := w.lockFunds(amt, initiationReserve); err != nil {
  2098  		return nil, err
  2099  	}
  2100  
  2101  	success = true
  2102  	return coins, nil
  2103  }
  2104  
  2105  // swapReceipt implements the asset.Receipt interface for ETH.
  2106  type swapReceipt struct {
  2107  	txHash     common.Hash
  2108  	secretHash [dexeth.SecretHashSize]byte
  2109  	// expiration and value can be determined with a blockchain
  2110  	// lookup, but we cache these values to avoid this.
  2111  	expiration   time.Time
  2112  	value        uint64
  2113  	ver          uint32
  2114  	contractAddr string // specified by ver, here for naive consumers
  2115  }
  2116  
  2117  // Expiration returns the time after which the contract can be
  2118  // refunded.
  2119  func (r *swapReceipt) Expiration() time.Time {
  2120  	return r.expiration
  2121  }
  2122  
  2123  // Coin returns the coin used to fund the swap.
  2124  func (r *swapReceipt) Coin() asset.Coin {
  2125  	return &coin{
  2126  		value: r.value,
  2127  		id:    r.txHash, // server's idea of ETH coin ID encoding
  2128  	}
  2129  }
  2130  
  2131  // Contract returns the swap's identifying data, which the concatenation of the
  2132  // contract version and the secret hash.
  2133  func (r *swapReceipt) Contract() dex.Bytes {
  2134  	return dexeth.EncodeContractData(r.ver, r.secretHash)
  2135  }
  2136  
  2137  // String returns a string representation of the swapReceipt. The secret hash
  2138  // and contract address should be included in this to facilitate a manual refund
  2139  // since the secret hash identifies the swap in the contract (for v0). Although
  2140  // the user can pick this information from the transaction's "to" address and
  2141  // the calldata, this simplifies the process.
  2142  func (r *swapReceipt) String() string {
  2143  	return fmt.Sprintf("{ tx hash: %s, contract address: %s, secret hash: %x }",
  2144  		r.txHash, r.contractAddr, r.secretHash)
  2145  }
  2146  
  2147  // SignedRefund returns an empty byte array. ETH does not support a pre-signed
  2148  // redeem script because the nonce needed in the transaction cannot be previously
  2149  // determined.
  2150  func (*swapReceipt) SignedRefund() dex.Bytes {
  2151  	return dex.Bytes{}
  2152  }
  2153  
  2154  var _ asset.Receipt = (*swapReceipt)(nil)
  2155  
  2156  // Swap sends the swaps in a single transaction. The fees used returned are the
  2157  // max fees that will possibly be used, since in ethereum with EIP-1559 we cannot
  2158  // know exactly how much fees will be used.
  2159  func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint64, error) {
  2160  	if swaps.FeeRate == 0 {
  2161  		return nil, nil, 0, fmt.Errorf("cannot send swap with with zero fee rate")
  2162  	}
  2163  
  2164  	fail := func(s string, a ...any) ([]asset.Receipt, asset.Coin, uint64, error) {
  2165  		return nil, nil, 0, fmt.Errorf(s, a...)
  2166  	}
  2167  
  2168  	var reservedVal uint64
  2169  	for _, input := range swaps.Inputs { // Should only ever be 1 input, I think.
  2170  		c, is := input.(*fundingCoin)
  2171  		if !is {
  2172  			return fail("wrong coin type: %T", input)
  2173  		}
  2174  		reservedVal += c.amt
  2175  	}
  2176  
  2177  	var swapVal uint64
  2178  	for _, contract := range swaps.Contracts {
  2179  		swapVal += contract.Value
  2180  	}
  2181  
  2182  	// Set the gas limit as high as reserves will allow.
  2183  	n := len(swaps.Contracts)
  2184  	oneSwap, nSwap, err := w.swapGas(n, swaps.Version)
  2185  	if err != nil {
  2186  		return fail("error getting gas fees: %v", err)
  2187  	}
  2188  	gasLimit := oneSwap * uint64(n) // naive unbatched, higher but not realistic
  2189  	fees := gasLimit * swaps.FeeRate
  2190  	if swapVal+fees > reservedVal {
  2191  		if n == 1 {
  2192  			return fail("unfunded swap: %d < %d", reservedVal, swapVal+fees)
  2193  		}
  2194  		w.log.Warnf("Unexpectedly low reserves for %d swaps: %d < %d", n, reservedVal, swapVal+fees)
  2195  		// Since this is a batch swap, attempt to use the realistic limits.
  2196  		gasLimit = nSwap
  2197  		fees = gasLimit * swaps.FeeRate
  2198  		if swapVal+fees > reservedVal {
  2199  			// If the live gas estimate is giving us an unrealistically high
  2200  			// value, we're in trouble, so we might consider a third fallback
  2201  			// that only uses our known gases:
  2202  			//   g := w.gases(swaps.Version)
  2203  			//   nSwap = g.Swap + uint64(n-1)*g.SwapAdd
  2204  			// But we've not swapped yet and we don't want a failed transaction,
  2205  			// so we will do nothing.
  2206  			return fail("unfunded swap: %d < %d", reservedVal, swapVal+fees)
  2207  		}
  2208  	}
  2209  
  2210  	maxFeeRate := dexeth.GweiToWei(swaps.FeeRate)
  2211  	_, tipRate, err := w.currentNetworkFees(w.ctx)
  2212  	if err != nil {
  2213  		return fail("Swap: failed to get network tip cap: %w", err)
  2214  	}
  2215  
  2216  	tx, err := w.initiate(w.ctx, w.assetID, swaps.Contracts, gasLimit, maxFeeRate, tipRate, swaps.Version)
  2217  	if err != nil {
  2218  		return fail("Swap: initiate error: %w", err)
  2219  	}
  2220  
  2221  	txHash := tx.Hash()
  2222  	receipts := make([]asset.Receipt, 0, n)
  2223  	for _, swap := range swaps.Contracts {
  2224  		var secretHash [dexeth.SecretHashSize]byte
  2225  		copy(secretHash[:], swap.SecretHash)
  2226  		receipts = append(receipts, &swapReceipt{
  2227  			expiration:   time.Unix(int64(swap.LockTime), 0),
  2228  			value:        swap.Value,
  2229  			txHash:       txHash,
  2230  			secretHash:   secretHash,
  2231  			ver:          swaps.Version,
  2232  			contractAddr: w.versionedContracts[swaps.Version].String(),
  2233  		})
  2234  	}
  2235  
  2236  	var change asset.Coin
  2237  	if swaps.LockChange {
  2238  		w.unlockFunds(swapVal+fees, initiationReserve)
  2239  		change = w.createFundingCoin(reservedVal - swapVal - fees)
  2240  	} else {
  2241  		w.unlockFunds(reservedVal, initiationReserve)
  2242  	}
  2243  
  2244  	return receipts, change, fees, nil
  2245  }
  2246  
  2247  // Swap sends the swaps in a single transaction. The fees used returned are the
  2248  // max fees that will possibly be used, since in ethereum with EIP-1559 we cannot
  2249  // know exactly how much fees will be used.
  2250  func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint64, error) {
  2251  	if swaps.FeeRate == 0 {
  2252  		return nil, nil, 0, fmt.Errorf("cannot send swap with with zero fee rate")
  2253  	}
  2254  
  2255  	fail := func(s string, a ...any) ([]asset.Receipt, asset.Coin, uint64, error) {
  2256  		return nil, nil, 0, fmt.Errorf(s, a...)
  2257  	}
  2258  
  2259  	var reservedVal, reservedParent uint64
  2260  	for _, input := range swaps.Inputs { // Should only ever be 1 input, I think.
  2261  		c, is := input.(*tokenFundingCoin)
  2262  		if !is {
  2263  			return fail("wrong coin type: %T", input)
  2264  		}
  2265  		reservedVal += c.amt
  2266  		reservedParent += c.fees
  2267  	}
  2268  
  2269  	var swapVal uint64
  2270  	for _, contract := range swaps.Contracts {
  2271  		swapVal += contract.Value
  2272  	}
  2273  
  2274  	if swapVal > reservedVal {
  2275  		return fail("unfunded token swap: %d < %d", reservedVal, swapVal)
  2276  	}
  2277  
  2278  	n := len(swaps.Contracts)
  2279  	oneSwap, nSwap, err := w.swapGas(n, swaps.Version)
  2280  	if err != nil {
  2281  		return fail("error getting gas fees: %v", err)
  2282  	}
  2283  
  2284  	gasLimit := oneSwap * uint64(n)
  2285  	fees := gasLimit * swaps.FeeRate
  2286  	if fees > reservedParent {
  2287  		if n == 1 {
  2288  			return fail("unfunded token swap fees: %d < %d", reservedParent, fees)
  2289  		}
  2290  		// Since this is a batch swap, attempt to use the realistic limits.
  2291  		w.log.Warnf("Unexpectedly low reserves for %d swaps: %d < %d", n, reservedVal, swapVal+fees)
  2292  		gasLimit = nSwap
  2293  		fees = gasLimit * swaps.FeeRate
  2294  		if fees > reservedParent {
  2295  			return fail("unfunded token swap fees: %d < %d", reservedParent, fees)
  2296  		} // See (*ETHWallet).Swap comments for a third option.
  2297  	}
  2298  
  2299  	maxFeeRate := dexeth.GweiToWei(swaps.FeeRate)
  2300  	_, tipRate, err := w.currentNetworkFees(w.ctx)
  2301  	if err != nil {
  2302  		return fail("Swap: failed to get network tip cap: %w", err)
  2303  	}
  2304  
  2305  	tx, err := w.initiate(w.ctx, w.assetID, swaps.Contracts, gasLimit, maxFeeRate, tipRate, swaps.Version)
  2306  	if err != nil {
  2307  		return fail("Swap: initiate error: %w", err)
  2308  	}
  2309  
  2310  	if w.netToken.SwapContracts[swaps.Version] == nil {
  2311  		return fail("unable to find contract address for asset %d contract version %d", w.assetID, swaps.Version)
  2312  	}
  2313  
  2314  	contractAddr := w.netToken.SwapContracts[swaps.Version].Address.String()
  2315  
  2316  	txHash := tx.Hash()
  2317  	receipts := make([]asset.Receipt, 0, n)
  2318  	for _, swap := range swaps.Contracts {
  2319  		var secretHash [dexeth.SecretHashSize]byte
  2320  		copy(secretHash[:], swap.SecretHash)
  2321  		receipts = append(receipts, &swapReceipt{
  2322  			expiration:   time.Unix(int64(swap.LockTime), 0),
  2323  			value:        swap.Value,
  2324  			txHash:       txHash,
  2325  			secretHash:   secretHash,
  2326  			ver:          swaps.Version,
  2327  			contractAddr: contractAddr,
  2328  		})
  2329  	}
  2330  
  2331  	var change asset.Coin
  2332  	if swaps.LockChange {
  2333  		w.unlockFunds(swapVal, initiationReserve)
  2334  		w.parent.unlockFunds(fees, initiationReserve)
  2335  		change = w.createTokenFundingCoin(reservedVal-swapVal, reservedParent-fees)
  2336  	} else {
  2337  		w.unlockFunds(reservedVal, initiationReserve)
  2338  		w.parent.unlockFunds(reservedParent, initiationReserve)
  2339  	}
  2340  
  2341  	return receipts, change, fees, nil
  2342  }
  2343  
  2344  // Redeem sends the redemption transaction, which may contain more than one
  2345  // redemption. All redemptions must be for the same contract version because the
  2346  // current API requires a single transaction reported (asset.Coin output), but
  2347  // conceptually a batch of redeems could be processed for any number of
  2348  // different contract addresses with multiple transactions. (buck: what would
  2349  // the difference from calling Redeem repeatedly?)
  2350  func (w *ETHWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) {
  2351  	return w.assetWallet.Redeem(form, nil, nil)
  2352  }
  2353  
  2354  // Redeem sends the redemption transaction, which may contain more than one
  2355  // redemption.
  2356  func (w *TokenWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) {
  2357  	return w.assetWallet.Redeem(form, w.parent, nil)
  2358  }
  2359  
  2360  // Redeem sends the redemption transaction, which may contain more than one
  2361  // redemption. The nonceOverride parameter is used to specify a specific nonce
  2362  // to be used for the redemption transaction. It is needed when resubmitting a
  2363  // redemption with a fee too low to be mined.
  2364  func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, nonceOverride *uint64) ([]dex.Bytes, asset.Coin, uint64, error) {
  2365  	fail := func(err error) ([]dex.Bytes, asset.Coin, uint64, error) {
  2366  		return nil, nil, 0, err
  2367  	}
  2368  
  2369  	n := uint64(len(form.Redemptions))
  2370  
  2371  	if n == 0 {
  2372  		return fail(errors.New("Redeem: must be called with at least 1 redemption"))
  2373  	}
  2374  
  2375  	var contractVer uint32 // require a consistent version since this is a single transaction
  2376  	secrets := make([][32]byte, 0, n)
  2377  	var redeemedValue uint64
  2378  	for i, redemption := range form.Redemptions {
  2379  		// NOTE: redemption.Spends.SecretHash is a dup of the hash extracted
  2380  		// from redemption.Spends.Contract. Even for scriptable UTXO assets, the
  2381  		// redeem script in this Contract field is redundant with the SecretHash
  2382  		// field as ExtractSwapDetails can be applied to extract the hash.
  2383  		ver, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract)
  2384  		if err != nil {
  2385  			return fail(fmt.Errorf("Redeem: invalid versioned swap contract data: %w", err))
  2386  		}
  2387  		if i == 0 {
  2388  			contractVer = ver
  2389  		} else if contractVer != ver {
  2390  			return fail(fmt.Errorf("Redeem: inconsistent contract versions in RedeemForm.Redemptions: "+
  2391  				"%d != %d", contractVer, ver))
  2392  		}
  2393  
  2394  		// Use the contract's free public view function to validate the secret
  2395  		// against the secret hash, and ensure the swap is otherwise redeemable
  2396  		// before broadcasting our secrets, which is especially important if we
  2397  		// are maker (the swap initiator).
  2398  		var secret [32]byte
  2399  		copy(secret[:], redemption.Secret)
  2400  		secrets = append(secrets, secret)
  2401  		redeemable, err := w.isRedeemable(secretHash, secret, ver)
  2402  		if err != nil {
  2403  			return fail(fmt.Errorf("Redeem: failed to check if swap is redeemable: %w", err))
  2404  		}
  2405  		if !redeemable {
  2406  			return fail(fmt.Errorf("Redeem: secretHash %x not redeemable with secret %x",
  2407  				secretHash, secret))
  2408  		}
  2409  
  2410  		swapData, err := w.swap(w.ctx, secretHash, ver)
  2411  		if err != nil {
  2412  			return nil, nil, 0, fmt.Errorf("error finding swap state: %w", err)
  2413  		}
  2414  		if swapData.State != dexeth.SSInitiated {
  2415  			return nil, nil, 0, asset.ErrSwapNotInitiated
  2416  		}
  2417  		redeemedValue += w.atomize(swapData.Value)
  2418  	}
  2419  
  2420  	g := w.gases(contractVer)
  2421  	if g == nil {
  2422  		return fail(fmt.Errorf("no gas table"))
  2423  	}
  2424  
  2425  	if feeWallet == nil {
  2426  		feeWallet = w
  2427  	}
  2428  	bal, err := feeWallet.Balance()
  2429  	if err != nil {
  2430  		return nil, nil, 0, fmt.Errorf("error getting balance in excessive gas fee recovery: %v", err)
  2431  	}
  2432  
  2433  	gasLimit, gasFeeCap := g.Redeem*n, form.FeeSuggestion
  2434  	originalFundsReserved := gasLimit * gasFeeCap
  2435  
  2436  	/* We could get a gas estimate via RPC, but this will reveal the secret key
  2437  	   before submitting the redeem transaction. This is not OK for maker.
  2438  	   Disable for now.
  2439  
  2440  	if gasEst, err := w.estimateRedeemGas(w.ctx, secrets, contractVer); err != nil {
  2441  		return fail(fmt.Errorf("error getting redemption estimate: %w", err))
  2442  	} else if gasEst > gasLimit {
  2443  		// This is sticky. We only reserved so much for redemption, so accepting
  2444  		// a gas limit higher than anticipated could potentially mess us up. On
  2445  		// the other hand, we don't want to simply reject the redemption.
  2446  		// Let's see if it looks like we can cover the fee. If so go ahead, up
  2447  		// to a limit.
  2448  		candidateLimit := gasEst * 11 / 10 // Add 10% for good measure.
  2449  		// Cannot be more than double.
  2450  		if candidateLimit > gasLimit*2 {
  2451  			return fail(fmt.Errorf("cannot recover from excessive gas estimate %d > 2 * %d", candidateLimit, gasLimit))
  2452  		}
  2453  		additionalFundsNeeded := (candidateLimit - gasLimit) * form.FeeSuggestion
  2454  		if bal.Available < additionalFundsNeeded {
  2455  			return fail(fmt.Errorf("no balance available for gas overshoot recovery. %d < %d", bal.Available, additionalFundsNeeded))
  2456  		}
  2457  		w.log.Warnf("live gas estimate %d exceeded expected max value %d. using higher limit %d for redemption", gasEst, gasLimit, candidateLimit)
  2458  		gasLimit = candidateLimit
  2459  	}
  2460  	*/
  2461  
  2462  	// If the base fee is higher than the FeeSuggestion we attempt to increase
  2463  	// the gasFeeCap to 2*baseFee. If we don't have enough funds, we use the
  2464  	// funds we have available.
  2465  	baseFee, tipRate, err := w.currentNetworkFees(w.ctx)
  2466  	if err != nil {
  2467  		return fail(fmt.Errorf("Error getting net fee state: %w", err))
  2468  	}
  2469  	baseFeeGwei := dexeth.WeiToGweiCeil(baseFee)
  2470  	if baseFeeGwei > form.FeeSuggestion {
  2471  		additionalFundsNeeded := (2 * baseFeeGwei * gasLimit) - originalFundsReserved
  2472  		if bal.Available > additionalFundsNeeded {
  2473  			gasFeeCap = 2 * baseFeeGwei
  2474  		} else {
  2475  			gasFeeCap = (bal.Available + originalFundsReserved) / gasLimit
  2476  		}
  2477  		w.log.Warnf("base fee %d > server max fee rate %d. using %d as gas fee cap for redemption", baseFeeGwei, form.FeeSuggestion, gasFeeCap)
  2478  	}
  2479  
  2480  	tx, err := w.redeem(w.ctx, form.Redemptions, gasFeeCap, tipRate, gasLimit, contractVer)
  2481  	if err != nil {
  2482  		return fail(fmt.Errorf("Redeem: redeem error: %w", err))
  2483  	}
  2484  
  2485  	txHash := tx.Hash()
  2486  
  2487  	txs := make([]dex.Bytes, len(form.Redemptions))
  2488  	for i := range txs {
  2489  		txs[i] = txHash[:]
  2490  	}
  2491  
  2492  	outputCoin := &coin{
  2493  		id:    txHash,
  2494  		value: redeemedValue,
  2495  	}
  2496  
  2497  	// This is still a fee estimate. If we add a redemption confirmation method
  2498  	// as has been discussed, then maybe the fees can be updated there.
  2499  	fees := g.RedeemN(len(form.Redemptions)) * form.FeeSuggestion
  2500  
  2501  	return txs, outputCoin, fees, nil
  2502  }
  2503  
  2504  // recoverPubkey recovers the uncompressed public key from the signature and the
  2505  // message hash that was signed. The signature should be a compact signature
  2506  // generated by a geth wallet, with the format [R || S || V], with the recover
  2507  // bit V at the end. See go-ethereum/crypto.Sign.
  2508  func recoverPubkey(msgHash, sig []byte) ([]byte, error) {
  2509  	// Using Decred's ecdsa.RecoverCompact requires moving the recovery byte to
  2510  	// the beginning of the serialized compact signature and adding back in the
  2511  	// compactSigMagicOffset scalar.
  2512  	sigBTC := make([]byte, 65)
  2513  	sigBTC[0] = sig[64] + 27 // compactSigMagicOffset
  2514  	copy(sigBTC[1:], sig)
  2515  	pubKey, _, err := ecdsa.RecoverCompact(sigBTC, msgHash)
  2516  	if err != nil {
  2517  		return nil, err
  2518  	}
  2519  	return pubKey.SerializeUncompressed(), nil
  2520  }
  2521  
  2522  // tokenBalance checks the token balance of the account handled by the wallet.
  2523  func (w *assetWallet) tokenBalance() (bal *big.Int, err error) {
  2524  	// We don't care about the version.
  2525  	return bal, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error {
  2526  		bal, err = c.balance(w.ctx)
  2527  		return err
  2528  	})
  2529  }
  2530  
  2531  // tokenAllowance checks the amount of tokens that the swap contract is approved
  2532  // to spend on behalf of the account handled by the wallet.
  2533  func (w *assetWallet) tokenAllowance(version uint32) (allowance *big.Int, err error) {
  2534  	return allowance, w.withTokenContractor(w.assetID, version, func(c tokenContractor) error {
  2535  		allowance, err = c.allowance(w.ctx)
  2536  		return err
  2537  	})
  2538  }
  2539  
  2540  // approveToken approves the token swap contract to spend tokens on behalf of
  2541  // account handled by the wallet.
  2542  func (w *assetWallet) approveToken(ctx context.Context, amount *big.Int, gasLimit uint64, maxFeeRate, tipRate *big.Int, contractVer uint32) (tx *types.Transaction, err error) {
  2543  	return tx, w.withNonce(ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) {
  2544  		txOpts, err := w.node.txOpts(w.ctx, 0, gasLimit, maxFeeRate, tipRate, nonce)
  2545  		if err != nil {
  2546  			return nil, 0, 0, nil, fmt.Errorf("addSignerToOpts error: %w", err)
  2547  		}
  2548  
  2549  		return tx, asset.ApproveToken, w.atomize(amount), nil, w.withTokenContractor(w.assetID, contractVer, func(c tokenContractor) error {
  2550  			tx, err = c.approve(txOpts, amount)
  2551  			if err != nil {
  2552  				return err
  2553  			}
  2554  			w.log.Infof("Approval sent for %s at token address %s, nonce = %s, txID = %s",
  2555  				dex.BipIDSymbol(w.assetID), c.tokenAddress(), txOpts.Nonce, tx.Hash().Hex())
  2556  			return nil
  2557  		})
  2558  	})
  2559  }
  2560  
  2561  func (w *assetWallet) approvalStatus(version uint32) (asset.ApprovalStatus, error) {
  2562  	if w.assetID == w.baseChainID {
  2563  		return asset.Approved, nil
  2564  	}
  2565  
  2566  	// If the result has been cached, return what is in the cache.
  2567  	// The cache is cleared if an approval/unapproval tx is done.
  2568  	w.approvalsMtx.RLock()
  2569  	if approved, cached := w.approvalCache[version]; cached {
  2570  		w.approvalsMtx.RUnlock()
  2571  		if approved {
  2572  			return asset.Approved, nil
  2573  		} else {
  2574  			return asset.NotApproved, nil
  2575  		}
  2576  	}
  2577  
  2578  	if _, pending := w.pendingApprovals[version]; pending {
  2579  		w.approvalsMtx.RUnlock()
  2580  		return asset.Pending, nil
  2581  	}
  2582  	w.approvalsMtx.RUnlock()
  2583  
  2584  	w.approvalsMtx.Lock()
  2585  	defer w.approvalsMtx.Unlock()
  2586  
  2587  	currentAllowance, err := w.tokenAllowance(version)
  2588  	if err != nil {
  2589  		return asset.NotApproved, fmt.Errorf("error retrieving current allowance: %w", err)
  2590  	}
  2591  	if currentAllowance.Cmp(unlimitedAllowanceReplenishThreshold) >= 0 {
  2592  		w.approvalCache[version] = true
  2593  		return asset.Approved, nil
  2594  	}
  2595  	w.approvalCache[version] = false
  2596  	return asset.NotApproved, nil
  2597  }
  2598  
  2599  // ApproveToken sends an approval transaction for a specific version of
  2600  // the token's swap contract. An error is returned if an approval has
  2601  // already been done or is pending. The onConfirm callback is called
  2602  // when the approval transaction is confirmed.
  2603  func (w *TokenWallet) ApproveToken(assetVer uint32, onConfirm func()) (string, error) {
  2604  	approvalStatus, err := w.approvalStatus(assetVer)
  2605  	if err != nil {
  2606  		return "", fmt.Errorf("error checking approval status: %w", err)
  2607  	}
  2608  	if approvalStatus == asset.Approved {
  2609  		return "", fmt.Errorf("token is already approved")
  2610  	}
  2611  	if approvalStatus == asset.Pending {
  2612  		return "", asset.ErrApprovalPending
  2613  	}
  2614  
  2615  	maxFeeRate, tipRate, err := w.recommendedMaxFeeRate(w.ctx)
  2616  	if err != nil {
  2617  		return "", fmt.Errorf("error calculating approval fee rate: %w", err)
  2618  	}
  2619  	feeRateGwei := dexeth.WeiToGweiCeil(maxFeeRate)
  2620  	approvalGas, err := w.approvalGas(unlimitedAllowance, assetVer)
  2621  	if err != nil {
  2622  		return "", fmt.Errorf("error calculating approval gas: %w", err)
  2623  	}
  2624  
  2625  	ethBal, err := w.parent.balance()
  2626  	if err != nil {
  2627  		return "", fmt.Errorf("error getting eth balance: %w", err)
  2628  	}
  2629  	if ethBal.Available < approvalGas*feeRateGwei {
  2630  		return "", fmt.Errorf("insufficient fee balance for approval. required: %d, available: %d",
  2631  			approvalGas*feeRateGwei, ethBal.Available)
  2632  	}
  2633  
  2634  	tx, err := w.approveToken(w.ctx, unlimitedAllowance, approvalGas, maxFeeRate, tipRate, assetVer)
  2635  	if err != nil {
  2636  		return "", fmt.Errorf("error approving token: %w", err)
  2637  	}
  2638  
  2639  	w.approvalsMtx.Lock()
  2640  	defer w.approvalsMtx.Unlock()
  2641  
  2642  	delete(w.approvalCache, assetVer)
  2643  	w.pendingApprovals[assetVer] = &pendingApproval{
  2644  		txHash:    tx.Hash(),
  2645  		onConfirm: onConfirm,
  2646  	}
  2647  
  2648  	return tx.Hash().Hex(), nil
  2649  }
  2650  
  2651  // UnapproveToken removes the approval for a specific version of the token's
  2652  // swap contract.
  2653  func (w *TokenWallet) UnapproveToken(assetVer uint32, onConfirm func()) (string, error) {
  2654  	approvalStatus, err := w.approvalStatus(assetVer)
  2655  	if err != nil {
  2656  		return "", fmt.Errorf("error checking approval status: %w", err)
  2657  	}
  2658  	if approvalStatus == asset.NotApproved {
  2659  		return "", fmt.Errorf("token is not approved")
  2660  	}
  2661  	if approvalStatus == asset.Pending {
  2662  		return "", asset.ErrApprovalPending
  2663  	}
  2664  
  2665  	maxFeeRate, tipRate, err := w.recommendedMaxFeeRate(w.ctx)
  2666  	if err != nil {
  2667  		return "", fmt.Errorf("error calculating approval fee rate: %w", err)
  2668  	}
  2669  	feeRateGwei := dexeth.WeiToGweiCeil(maxFeeRate)
  2670  	approvalGas, err := w.approvalGas(big.NewInt(0), assetVer)
  2671  	if err != nil {
  2672  		return "", fmt.Errorf("error calculating approval gas: %w", err)
  2673  	}
  2674  
  2675  	ethBal, err := w.parent.balance()
  2676  	if err != nil {
  2677  		return "", fmt.Errorf("error getting eth balance: %w", err)
  2678  	}
  2679  	if ethBal.Available < approvalGas*feeRateGwei {
  2680  		return "", fmt.Errorf("insufficient eth balance for unapproval. required: %d, available: %d",
  2681  			approvalGas*feeRateGwei, ethBal.Available)
  2682  	}
  2683  
  2684  	tx, err := w.approveToken(w.ctx, big.NewInt(0), approvalGas, maxFeeRate, tipRate, assetVer)
  2685  	if err != nil {
  2686  		return "", fmt.Errorf("error unapproving token: %w", err)
  2687  	}
  2688  
  2689  	w.approvalsMtx.Lock()
  2690  	defer w.approvalsMtx.Unlock()
  2691  
  2692  	delete(w.approvalCache, assetVer)
  2693  	w.pendingApprovals[assetVer] = &pendingApproval{
  2694  		txHash:    tx.Hash(),
  2695  		onConfirm: onConfirm,
  2696  	}
  2697  
  2698  	return tx.Hash().Hex(), nil
  2699  }
  2700  
  2701  // ApprovalFee returns the estimated fee for an approval transaction.
  2702  func (w *TokenWallet) ApprovalFee(assetVer uint32, approve bool) (uint64, error) {
  2703  	var allowance *big.Int
  2704  	if approve {
  2705  		allowance = unlimitedAllowance
  2706  	} else {
  2707  		allowance = big.NewInt(0)
  2708  	}
  2709  	approvalGas, err := w.approvalGas(allowance, assetVer)
  2710  	if err != nil {
  2711  		return 0, fmt.Errorf("error calculating approval gas: %w", err)
  2712  	}
  2713  
  2714  	feeRateGwei, err := w.recommendedMaxFeeRateGwei(w.ctx)
  2715  	if err != nil {
  2716  		return 0, fmt.Errorf("error calculating approval fee rate: %w", err)
  2717  	}
  2718  
  2719  	return approvalGas * feeRateGwei, nil
  2720  }
  2721  
  2722  // ApprovalStatus returns the approval status for each version of the
  2723  // token's swap contract.
  2724  func (w *TokenWallet) ApprovalStatus() map[uint32]asset.ApprovalStatus {
  2725  	versions := w.Info().SupportedVersions
  2726  
  2727  	statuses := map[uint32]asset.ApprovalStatus{}
  2728  	for _, version := range versions {
  2729  		status, err := w.approvalStatus(version)
  2730  		if err != nil {
  2731  			w.log.Errorf("error checking approval status for version %d: %w", version, err)
  2732  			continue
  2733  		}
  2734  		statuses[version] = status
  2735  	}
  2736  
  2737  	return statuses
  2738  }
  2739  
  2740  // ReserveNRedemptions locks funds for redemption. It is an error if there
  2741  // is insufficient spendable balance. Part of the AccountLocker interface.
  2742  func (w *ETHWallet) ReserveNRedemptions(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) {
  2743  	g := w.gases(ver)
  2744  	if g == nil {
  2745  		return 0, fmt.Errorf("no gas table")
  2746  	}
  2747  	redeemCost := g.Redeem * maxFeeRate
  2748  	reserve := redeemCost * n
  2749  
  2750  	if err := w.lockFunds(reserve, redemptionReserve); err != nil {
  2751  		return 0, err
  2752  	}
  2753  
  2754  	return reserve, nil
  2755  }
  2756  
  2757  // ReserveNRedemptions locks funds for redemption. It is an error if there
  2758  // is insufficient spendable balance.
  2759  // Part of the AccountLocker interface.
  2760  func (w *TokenWallet) ReserveNRedemptions(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) {
  2761  	g := w.gases(ver)
  2762  	if g == nil {
  2763  		return 0, fmt.Errorf("no gas table")
  2764  	}
  2765  	reserve := g.Redeem * maxFeeRate * n
  2766  
  2767  	if err := w.parent.lockFunds(reserve, redemptionReserve); err != nil {
  2768  		return 0, err
  2769  	}
  2770  
  2771  	return reserve, nil
  2772  }
  2773  
  2774  // UnlockRedemptionReserves unlocks the specified amount from redemption
  2775  // reserves. Part of the AccountLocker interface.
  2776  func (w *ETHWallet) UnlockRedemptionReserves(reserves uint64) {
  2777  	unlockRedemptionReserves(w.assetWallet, reserves)
  2778  }
  2779  
  2780  // UnlockRedemptionReserves unlocks the specified amount from redemption
  2781  // reserves. Part of the AccountLocker interface.
  2782  func (w *TokenWallet) UnlockRedemptionReserves(reserves uint64) {
  2783  	unlockRedemptionReserves(w.parent, reserves)
  2784  }
  2785  
  2786  func unlockRedemptionReserves(w *assetWallet, reserves uint64) {
  2787  	w.unlockFunds(reserves, redemptionReserve)
  2788  }
  2789  
  2790  // ReReserveRedemption checks out an amount for redemptions. Use
  2791  // ReReserveRedemption after initializing a new asset.Wallet.
  2792  // Part of the AccountLocker interface.
  2793  func (w *ETHWallet) ReReserveRedemption(req uint64) error {
  2794  	return w.lockFunds(req, redemptionReserve)
  2795  }
  2796  
  2797  // ReReserveRedemption checks out an amount for redemptions. Use
  2798  // ReReserveRedemption after initializing a new asset.Wallet.
  2799  // Part of the AccountLocker interface.
  2800  func (w *TokenWallet) ReReserveRedemption(req uint64) error {
  2801  	return w.parent.lockFunds(req, redemptionReserve)
  2802  }
  2803  
  2804  // ReserveNRefunds locks funds for doing refunds. It is an error if there
  2805  // is insufficient spendable balance. Part of the AccountLocker interface.
  2806  func (w *ETHWallet) ReserveNRefunds(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) {
  2807  	g := w.gases(ver)
  2808  	if g == nil {
  2809  		return 0, errors.New("no gas table")
  2810  	}
  2811  	return reserveNRefunds(w.assetWallet, n, maxFeeRate, g)
  2812  }
  2813  
  2814  // ReserveNRefunds locks funds for doing refunds. It is an error if there
  2815  // is insufficient spendable balance. Part of the AccountLocker interface.
  2816  func (w *TokenWallet) ReserveNRefunds(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) {
  2817  	g := w.gases(ver)
  2818  	if g == nil {
  2819  		return 0, errors.New("no gas table")
  2820  	}
  2821  	return reserveNRefunds(w.parent, n, maxFeeRate, g)
  2822  }
  2823  
  2824  func reserveNRefunds(w *assetWallet, n, maxFeeRate uint64, g *dexeth.Gases) (uint64, error) {
  2825  	refundCost := g.Refund * maxFeeRate
  2826  	reserve := refundCost * n
  2827  
  2828  	if err := w.lockFunds(reserve, refundReserve); err != nil {
  2829  		return 0, err
  2830  	}
  2831  	return reserve, nil
  2832  }
  2833  
  2834  // UnlockRefundReserves unlocks the specified amount from refund
  2835  // reserves. Part of the AccountLocker interface.
  2836  func (w *ETHWallet) UnlockRefundReserves(reserves uint64) {
  2837  	unlockRefundReserves(w.assetWallet, reserves)
  2838  }
  2839  
  2840  // UnlockRefundReserves unlocks the specified amount from refund
  2841  // reserves. Part of the AccountLocker interface.
  2842  func (w *TokenWallet) UnlockRefundReserves(reserves uint64) {
  2843  	unlockRefundReserves(w.parent, reserves)
  2844  }
  2845  
  2846  func unlockRefundReserves(w *assetWallet, reserves uint64) {
  2847  	w.unlockFunds(reserves, refundReserve)
  2848  }
  2849  
  2850  // ReReserveRefund checks out an amount for doing refunds. Use ReReserveRefund
  2851  // after initializing a new assetWallet. Part of the AccountLocker
  2852  // interface.
  2853  func (w *ETHWallet) ReReserveRefund(req uint64) error {
  2854  	return w.lockFunds(req, refundReserve)
  2855  }
  2856  
  2857  // ReReserveRefund checks out an amount for doing refunds. Use ReReserveRefund
  2858  // after initializing a new assetWallet. Part of the AccountLocker
  2859  // interface.
  2860  func (w *TokenWallet) ReReserveRefund(req uint64) error {
  2861  	return w.parent.lockFunds(req, refundReserve)
  2862  }
  2863  
  2864  // SignMessage signs the message with the private key associated with the
  2865  // specified funding Coin. Only a coin that came from the address this wallet
  2866  // is initialized with can be used to sign.
  2867  func (eth *baseWallet) SignMessage(_ asset.Coin, msg dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) {
  2868  	sig, pubKey, err := eth.node.signData(msg)
  2869  	if err != nil {
  2870  		return nil, nil, fmt.Errorf("SignMessage: error signing data: %w", err)
  2871  	}
  2872  
  2873  	return []dex.Bytes{pubKey}, []dex.Bytes{sig}, nil
  2874  }
  2875  
  2876  // AuditContract retrieves information about a swap contract on the
  2877  // blockchain. This would be used to verify the counter-party's contract
  2878  // during a swap. coinID is expected to be the transaction id, and must
  2879  // be the same as the hash of serializedTx. contract is expected to be
  2880  // (contractVersion|secretHash) where the secretHash uniquely keys the swap.
  2881  func (w *assetWallet) AuditContract(coinID, contract, serializedTx dex.Bytes, rebroadcast bool) (*asset.AuditInfo, error) {
  2882  	tx := new(types.Transaction)
  2883  	err := tx.UnmarshalBinary(serializedTx)
  2884  	if err != nil {
  2885  		return nil, fmt.Errorf("AuditContract: failed to unmarshal transaction: %w", err)
  2886  	}
  2887  
  2888  	txHash := tx.Hash()
  2889  	if !bytes.Equal(coinID, txHash[:]) {
  2890  		return nil, fmt.Errorf("AuditContract: coin id != txHash - coin id: %x, txHash: %s", coinID, tx.Hash())
  2891  	}
  2892  
  2893  	version, secretHash, err := dexeth.DecodeContractData(contract)
  2894  	if err != nil {
  2895  		return nil, fmt.Errorf("AuditContract: failed to decode contract data: %w", err)
  2896  	}
  2897  
  2898  	initiations, err := dexeth.ParseInitiateData(tx.Data(), version)
  2899  	if err != nil {
  2900  		return nil, fmt.Errorf("AuditContract: failed to parse initiate data: %w", err)
  2901  	}
  2902  
  2903  	initiation, ok := initiations[secretHash]
  2904  	if !ok {
  2905  		return nil, errors.New("AuditContract: tx does not initiate secret hash")
  2906  	}
  2907  
  2908  	coin := &coin{
  2909  		id:    txHash,
  2910  		value: w.atomize(initiation.Value),
  2911  	}
  2912  
  2913  	return &asset.AuditInfo{
  2914  		Recipient:  initiation.Participant.Hex(),
  2915  		Expiration: initiation.LockTime,
  2916  		Coin:       coin,
  2917  		Contract:   contract,
  2918  		SecretHash: secretHash[:],
  2919  	}, nil
  2920  }
  2921  
  2922  // LockTimeExpired returns true if the specified locktime has expired, making it
  2923  // possible to redeem the locked coins.
  2924  func (w *assetWallet) LockTimeExpired(ctx context.Context, lockTime time.Time) (bool, error) {
  2925  	header, err := w.node.bestHeader(ctx)
  2926  	if err != nil {
  2927  		return false, fmt.Errorf("unable to retrieve block header: %w", err)
  2928  	}
  2929  	blockTime := time.Unix(int64(header.Time), 0)
  2930  	return lockTime.Before(blockTime), nil
  2931  }
  2932  
  2933  // ContractLockTimeExpired returns true if the specified contract's locktime has
  2934  // expired, making it possible to issue a Refund.
  2935  func (w *assetWallet) ContractLockTimeExpired(ctx context.Context, contract dex.Bytes) (bool, time.Time, error) {
  2936  	contractVer, secretHash, err := dexeth.DecodeContractData(contract)
  2937  	if err != nil {
  2938  		return false, time.Time{}, err
  2939  	}
  2940  
  2941  	swap, err := w.swap(ctx, secretHash, contractVer)
  2942  	if err != nil {
  2943  		return false, time.Time{}, err
  2944  	}
  2945  
  2946  	// Time is not yet set for uninitiated swaps.
  2947  	if swap.State == dexeth.SSNone {
  2948  		return false, time.Time{}, asset.ErrSwapNotInitiated
  2949  	}
  2950  
  2951  	expired, err := w.LockTimeExpired(ctx, swap.LockTime)
  2952  	if err != nil {
  2953  		return false, time.Time{}, err
  2954  	}
  2955  	return expired, swap.LockTime, nil
  2956  }
  2957  
  2958  // findRedemptionResult is used internally for queued findRedemptionRequests.
  2959  type findRedemptionResult struct {
  2960  	err       error
  2961  	secret    []byte
  2962  	makerAddr string
  2963  }
  2964  
  2965  // findRedemptionRequest is a request that is waiting on a redemption result.
  2966  type findRedemptionRequest struct {
  2967  	contractVer uint32
  2968  	res         chan *findRedemptionResult
  2969  }
  2970  
  2971  // sendFindRedemptionResult sends the result or logs a message if it cannot be
  2972  // sent.
  2973  func (eth *baseWallet) sendFindRedemptionResult(req *findRedemptionRequest, secretHash [32]byte,
  2974  	secret []byte, makerAddr string, err error) {
  2975  	select {
  2976  	case req.res <- &findRedemptionResult{secret: secret, makerAddr: makerAddr, err: err}:
  2977  	default:
  2978  		eth.log.Info("findRedemptionResult channel blocking for request %s", secretHash)
  2979  	}
  2980  }
  2981  
  2982  // findRedemptionRequests creates a copy of the findRedemptionReqs map.
  2983  func (w *assetWallet) findRedemptionRequests() map[[32]byte]*findRedemptionRequest {
  2984  	w.findRedemptionMtx.RLock()
  2985  	defer w.findRedemptionMtx.RUnlock()
  2986  	reqs := make(map[[32]byte]*findRedemptionRequest, len(w.findRedemptionReqs))
  2987  	for secretHash, req := range w.findRedemptionReqs {
  2988  		reqs[secretHash] = req
  2989  	}
  2990  	return reqs
  2991  }
  2992  
  2993  // FindRedemption checks the contract for a redemption. If the swap is initiated
  2994  // but un-redeemed and un-refunded, FindRedemption will block until a redemption
  2995  // is seen.
  2996  func (w *assetWallet) FindRedemption(ctx context.Context, _, contract dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) {
  2997  	// coinIDTmpl is a template for constructing Coin ID when Taker
  2998  	// (aka participant) finds redemption himself. %s represents Maker Ethereum
  2999  	// account address so that user, as Taker, could manually look it up in case
  3000  	// he needs it. Ideally we'd want to have transaction ID there instead of
  3001  	// account address, but that's currently impossible to get in Ethereum smart
  3002  	// contract, so we are basically doing the next best thing here.
  3003  	const coinIDTmpl = coinIDTakerFoundMakerRedemption + "%s"
  3004  
  3005  	contractVer, secretHash, err := dexeth.DecodeContractData(contract)
  3006  	if err != nil {
  3007  		return nil, nil, err
  3008  	}
  3009  
  3010  	// See if it's ready right away.
  3011  	secret, makerAddr, err := w.findSecret(secretHash, contractVer)
  3012  	if err != nil {
  3013  		return nil, nil, err
  3014  	}
  3015  
  3016  	if len(secret) > 0 {
  3017  		return dex.Bytes(fmt.Sprintf(coinIDTmpl, makerAddr)), secret, nil
  3018  	}
  3019  
  3020  	// Not ready. Queue the request.
  3021  	req := &findRedemptionRequest{
  3022  		contractVer: contractVer,
  3023  		res:         make(chan *findRedemptionResult, 1),
  3024  	}
  3025  
  3026  	w.findRedemptionMtx.Lock()
  3027  
  3028  	if w.findRedemptionReqs[secretHash] != nil {
  3029  		w.findRedemptionMtx.Unlock()
  3030  		return nil, nil, fmt.Errorf("duplicate find redemption request for %x", secretHash)
  3031  	}
  3032  
  3033  	w.findRedemptionReqs[secretHash] = req
  3034  
  3035  	w.findRedemptionMtx.Unlock()
  3036  
  3037  	var res *findRedemptionResult
  3038  	select {
  3039  	case res = <-req.res:
  3040  	case <-ctx.Done():
  3041  	}
  3042  
  3043  	w.findRedemptionMtx.Lock()
  3044  	delete(w.findRedemptionReqs, secretHash)
  3045  	w.findRedemptionMtx.Unlock()
  3046  
  3047  	if res == nil {
  3048  		return nil, nil, fmt.Errorf("context cancelled for find redemption request %x", secretHash)
  3049  	}
  3050  
  3051  	if res.err != nil {
  3052  		return nil, nil, res.err
  3053  	}
  3054  
  3055  	return dex.Bytes(fmt.Sprintf(coinIDTmpl, res.makerAddr)), res.secret[:], nil
  3056  }
  3057  
  3058  // findSecret returns redemption secret from smart contract that Maker put there
  3059  // redeeming Taker swap along with Maker Ethereum account address. Returns empty
  3060  // values if Maker hasn't redeemed yet.
  3061  func (w *assetWallet) findSecret(secretHash [32]byte, contractVer uint32) ([]byte, string, error) {
  3062  	ctx, cancel := context.WithTimeout(w.ctx, 10*time.Second)
  3063  	defer cancel()
  3064  	swap, err := w.swap(ctx, secretHash, contractVer)
  3065  	if err != nil {
  3066  		return nil, "", err
  3067  	}
  3068  
  3069  	switch swap.State {
  3070  	case dexeth.SSInitiated:
  3071  		return nil, "", nil // no Maker redeem yet, but keep checking
  3072  	case dexeth.SSRedeemed:
  3073  		return swap.Secret[:], swap.Initiator.String(), nil
  3074  	case dexeth.SSNone:
  3075  		return nil, "", fmt.Errorf("swap %x does not exist", secretHash)
  3076  	case dexeth.SSRefunded:
  3077  		return nil, "", fmt.Errorf("swap %x is already refunded", secretHash)
  3078  	}
  3079  	return nil, "", fmt.Errorf("unrecognized swap state %v", swap.State)
  3080  }
  3081  
  3082  // Refund refunds a contract. This can only be used after the time lock has
  3083  // expired.
  3084  func (w *assetWallet) Refund(_, contract dex.Bytes, feeRate uint64) (dex.Bytes, error) {
  3085  	version, secretHash, err := dexeth.DecodeContractData(contract)
  3086  	if err != nil {
  3087  		return nil, fmt.Errorf("Refund: failed to decode contract: %w", err)
  3088  	}
  3089  
  3090  	swap, err := w.swap(w.ctx, secretHash, version)
  3091  	if err != nil {
  3092  		return nil, err
  3093  	}
  3094  	// It's possible the swap was refunded by someone else. In that case we
  3095  	// cannot know the refunding tx hash.
  3096  	switch swap.State {
  3097  	case dexeth.SSInitiated: // good, check refundability
  3098  	case dexeth.SSNone:
  3099  		return nil, asset.ErrSwapNotInitiated
  3100  	case dexeth.SSRefunded:
  3101  		w.log.Infof("Swap with secret hash %x already refunded.", secretHash)
  3102  		zeroHash := common.Hash{}
  3103  		return zeroHash[:], nil
  3104  	case dexeth.SSRedeemed:
  3105  		w.log.Infof("Swap with secret hash %x already redeemed with secret key %x.",
  3106  			secretHash, swap.Secret)
  3107  		return nil, asset.CoinNotFoundError // so caller knows to FindRedemption
  3108  	}
  3109  
  3110  	refundable, err := w.isRefundable(secretHash, version)
  3111  	if err != nil {
  3112  		return nil, fmt.Errorf("Refund: failed to check isRefundable: %w", err)
  3113  	}
  3114  	if !refundable {
  3115  		return nil, fmt.Errorf("Refund: swap with secret hash %x is not refundable", secretHash)
  3116  	}
  3117  
  3118  	maxFeeRate := dexeth.GweiToWei(feeRate)
  3119  	_, tipRate, err := w.currentNetworkFees(w.ctx)
  3120  	if err != nil {
  3121  		return nil, fmt.Errorf("Refund: failed to get network tip cap: %w", err)
  3122  	}
  3123  
  3124  	tx, err := w.refund(secretHash, w.atomize(swap.Value), maxFeeRate, tipRate, version)
  3125  	if err != nil {
  3126  		return nil, fmt.Errorf("Refund: failed to call refund: %w", err)
  3127  	}
  3128  
  3129  	txHash := tx.Hash()
  3130  	return txHash[:], nil
  3131  }
  3132  
  3133  // DepositAddress returns an address for the exchange wallet. This implementation
  3134  // is idempotent, always returning the same address for a given assetWallet.
  3135  func (eth *baseWallet) DepositAddress() (string, error) {
  3136  	return eth.addr.String(), nil
  3137  }
  3138  
  3139  // RedemptionAddress gets an address for use in redeeming the counterparty's
  3140  // swap. This would be included in their swap initialization.
  3141  func (eth *baseWallet) RedemptionAddress() (string, error) {
  3142  	return eth.addr.String(), nil
  3143  }
  3144  
  3145  // Unlock unlocks the exchange wallet.
  3146  func (eth *ETHWallet) Unlock(pw []byte) error {
  3147  	return eth.node.unlock(string(pw))
  3148  }
  3149  
  3150  // Lock locks the exchange wallet.
  3151  func (eth *ETHWallet) Lock() error {
  3152  	return eth.node.lock()
  3153  }
  3154  
  3155  // Locked will be true if the wallet is currently locked.
  3156  func (eth *ETHWallet) Locked() bool {
  3157  	return eth.node.locked()
  3158  }
  3159  
  3160  // SendTransaction broadcasts a valid fully-signed transaction.
  3161  func (eth *baseWallet) SendTransaction(rawTx []byte) ([]byte, error) {
  3162  	tx := new(types.Transaction)
  3163  	err := tx.UnmarshalBinary(rawTx)
  3164  	if err != nil {
  3165  		return nil, fmt.Errorf("failed to unmarshal transaction: %w", err)
  3166  	}
  3167  	if err := eth.node.sendSignedTransaction(eth.ctx, tx); err != nil {
  3168  		return nil, err
  3169  	}
  3170  	return tx.Hash().Bytes(), nil
  3171  }
  3172  
  3173  // ValidateAddress checks whether the provided address is a valid hex-encoded
  3174  // Ethereum address.
  3175  func (w *ETHWallet) ValidateAddress(address string) bool {
  3176  	return common.IsHexAddress(address)
  3177  }
  3178  
  3179  // ValidateAddress checks whether the provided address is a valid hex-encoded
  3180  // Ethereum address.
  3181  func (w *TokenWallet) ValidateAddress(address string) bool {
  3182  	return common.IsHexAddress(address)
  3183  }
  3184  
  3185  // isValidSend is a helper function for both token and ETH wallet. It returns an
  3186  // error if subtract is true, addr is invalid or value is zero.
  3187  func isValidSend(addr string, value uint64, subtract bool) error {
  3188  	if value == 0 {
  3189  		return fmt.Errorf("cannot send zero amount")
  3190  	}
  3191  	if subtract {
  3192  		return fmt.Errorf("wallet does not support subtracting network fee from send amount")
  3193  	}
  3194  	if !common.IsHexAddress(addr) {
  3195  		return fmt.Errorf("invalid hex address %q", addr)
  3196  	}
  3197  	return nil
  3198  }
  3199  
  3200  // canSend ensures that the wallet has enough to cover send value and returns
  3201  // the fee rate and max fee required for the send tx. If isPreEstimate is false,
  3202  // wallet balance must be enough to cover total spend.
  3203  func (w *ETHWallet) canSend(value uint64, verifyBalance, isPreEstimate bool) (maxFee uint64, maxFeeRate, tipRate *big.Int, err error) {
  3204  	maxFeeRate, tipRate, err = w.recommendedMaxFeeRate(w.ctx)
  3205  	if err != nil {
  3206  		return 0, nil, nil, fmt.Errorf("error getting max fee rate: %w", err)
  3207  	}
  3208  	maxFeeRateGwei := dexeth.WeiToGweiCeil(maxFeeRate)
  3209  
  3210  	maxFee = defaultSendGasLimit * maxFeeRateGwei
  3211  
  3212  	if isPreEstimate {
  3213  		maxFee = maxFee * 12 / 10 // 20% buffer
  3214  	}
  3215  
  3216  	if verifyBalance {
  3217  		bal, err := w.Balance()
  3218  		if err != nil {
  3219  			return 0, nil, nil, err
  3220  		}
  3221  		avail := bal.Available
  3222  		if avail < value {
  3223  			return 0, nil, nil, fmt.Errorf("not enough funds to send: have %d gwei need %d gwei", avail, value)
  3224  		}
  3225  
  3226  		if avail < value+maxFee {
  3227  			return 0, nil, nil, fmt.Errorf("available funds %d gwei cannot cover value being sent: need %d gwei + %d gwei max fee", avail, value, maxFee)
  3228  		}
  3229  	}
  3230  	return
  3231  }
  3232  
  3233  // canSend ensures that the wallet has enough to cover send value and returns
  3234  // the fee rate and max fee required for the send tx.
  3235  func (w *TokenWallet) canSend(value uint64, verifyBalance, isPreEstimate bool) (maxFee uint64, maxFeeRate, tipRate *big.Int, err error) {
  3236  	maxFeeRate, tipRate, err = w.recommendedMaxFeeRate(w.ctx)
  3237  	if err != nil {
  3238  		return 0, nil, nil, fmt.Errorf("error getting max fee rate: %w", err)
  3239  	}
  3240  	maxFeeRateGwei := dexeth.WeiToGweiCeil(maxFeeRate)
  3241  
  3242  	g := w.gases(contractVersionNewest)
  3243  	if g == nil {
  3244  		return 0, nil, nil, fmt.Errorf("gas table not found")
  3245  	}
  3246  
  3247  	maxFee = maxFeeRateGwei * g.Transfer
  3248  
  3249  	if isPreEstimate {
  3250  		maxFee = maxFee * 12 / 10 // 20% buffer
  3251  	}
  3252  
  3253  	if verifyBalance {
  3254  		bal, err := w.Balance()
  3255  		if err != nil {
  3256  			return 0, nil, nil, err
  3257  		}
  3258  		avail := bal.Available
  3259  		if avail < value {
  3260  			return 0, nil, nil, fmt.Errorf("not enough tokens: have %[1]d %[3]s need %[2]d %[3]s", avail, value, w.ui.AtomicUnit)
  3261  		}
  3262  
  3263  		ethBal, err := w.parent.Balance()
  3264  		if err != nil {
  3265  			return 0, nil, nil, fmt.Errorf("error getting base chain balance: %w", err)
  3266  		}
  3267  
  3268  		if ethBal.Available < maxFee {
  3269  			return 0, nil, nil, fmt.Errorf("insufficient balance to cover token transfer fees. %d < %d",
  3270  				ethBal.Available, maxFee)
  3271  		}
  3272  	}
  3273  	return
  3274  }
  3275  
  3276  // EstimateSendTxFee returns a tx fee estimate for a send tx. The provided fee
  3277  // rate is ignored since all sends will use an internally derived fee rate. If
  3278  // an address is provided, it will ensure wallet has enough to cover total
  3279  // spend.
  3280  func (w *ETHWallet) EstimateSendTxFee(addr string, value, _ uint64, _, maxWithdraw bool) (uint64, bool, error) {
  3281  	if err := isValidSend(addr, value, maxWithdraw); err != nil && addr != "" { // fee estimate for a send tx.
  3282  		return 0, false, err
  3283  	}
  3284  	maxFee, _, _, err := w.canSend(value, addr != "", true)
  3285  	if err != nil {
  3286  		return 0, false, err
  3287  	}
  3288  	return maxFee, w.ValidateAddress(addr), nil
  3289  }
  3290  
  3291  // StandardSendFees returns the fees for a simple send tx.
  3292  func (w *ETHWallet) StandardSendFee(feeRate uint64) uint64 {
  3293  	return defaultSendGasLimit * feeRate
  3294  }
  3295  
  3296  // EstimateSendTxFee returns a tx fee estimate for a send tx. The provided fee
  3297  // rate is ignored since all sends will use an internally derived fee rate. If
  3298  // an address is provided, it will ensure wallet has enough to cover total
  3299  // spend.
  3300  func (w *TokenWallet) EstimateSendTxFee(addr string, value, _ uint64, _, maxWithdraw bool) (fee uint64, isValidAddress bool, err error) {
  3301  	if err := isValidSend(addr, value, maxWithdraw); err != nil && addr != "" { // fee estimate for a send tx.
  3302  		return 0, false, err
  3303  	}
  3304  	maxFee, _, _, err := w.canSend(value, addr != "", true)
  3305  	if err != nil {
  3306  		return 0, false, err
  3307  	}
  3308  	return maxFee, w.ValidateAddress(addr), nil
  3309  }
  3310  
  3311  // StandardSendFees returns the fees for a simple send tx.
  3312  func (w *TokenWallet) StandardSendFee(feeRate uint64) uint64 {
  3313  	g := w.gases(contractVersionNewest)
  3314  	if g == nil {
  3315  		w.log.Errorf("error getting gases for token %s", w.token.Name)
  3316  		return 0
  3317  	}
  3318  	return g.Transfer * feeRate
  3319  }
  3320  
  3321  // RestorationInfo returns information about how to restore the wallet in
  3322  // various external wallets.
  3323  func (w *ETHWallet) RestorationInfo(seed []byte) ([]*asset.WalletRestoration, error) {
  3324  	privateKey, zero, err := privKeyFromSeed(seed)
  3325  	if err != nil {
  3326  		return nil, err
  3327  	}
  3328  	defer zero()
  3329  
  3330  	return []*asset.WalletRestoration{
  3331  		{
  3332  			Target:   "MetaMask",
  3333  			Seed:     hex.EncodeToString(privateKey),
  3334  			SeedName: "Private Key",
  3335  			Instructions: "Accounts can be imported by private key only if MetaMask has already be initialized. " +
  3336  				"If this is your first time installing MetaMask, create a new wallet and secret recovery phrase. " +
  3337  				"Then, to import your DEX account into MetaMask, follow the steps below:\n" +
  3338  				`1. Open the settings menu
  3339  				 2. Select "Import Account"
  3340  				 3. Make sure "Private Key" is selected, and enter the private key above into the box`,
  3341  		},
  3342  	}, nil
  3343  }
  3344  
  3345  // SwapConfirmations gets the number of confirmations and the spend status
  3346  // for the specified swap.
  3347  func (w *assetWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, contract dex.Bytes, _ time.Time) (confs uint32, spent bool, err error) {
  3348  	contractVer, secretHash, err := dexeth.DecodeContractData(contract)
  3349  	if err != nil {
  3350  		return 0, false, err
  3351  	}
  3352  
  3353  	ctx, cancel := context.WithTimeout(ctx, onChainDataFetchTimeout)
  3354  	defer cancel()
  3355  
  3356  	swapData, err := w.swap(ctx, secretHash, contractVer)
  3357  	if err != nil {
  3358  		return 0, false, fmt.Errorf("error finding swap state: %w", err)
  3359  	}
  3360  
  3361  	if swapData.State == dexeth.SSNone {
  3362  		// Check if we know about the tx ourselves. If it's not in pendingTxs
  3363  		// or the database, assume it's lost.
  3364  		return 0, false, asset.ErrSwapNotInitiated
  3365  	}
  3366  
  3367  	spent = swapData.State >= dexeth.SSRedeemed
  3368  	tip := w.tipHeight()
  3369  	// TODO: If tip < swapData.BlockHeight (which has been observed), what does
  3370  	// that mean? Are we using the wrong provider in a multi-provider setup? How
  3371  	// do we resolve provider relevance?
  3372  	if tip >= swapData.BlockHeight {
  3373  		confs = uint32(tip - swapData.BlockHeight + 1)
  3374  	}
  3375  	return
  3376  }
  3377  
  3378  // Send sends the exact value to the specified address. The provided fee rate is
  3379  // ignored since all sends will use an internally derived fee rate.
  3380  func (w *ETHWallet) Send(addr string, value, _ uint64) (asset.Coin, error) {
  3381  	if err := isValidSend(addr, value, false); err != nil {
  3382  		return nil, err
  3383  	}
  3384  
  3385  	_ /* maxFee */, maxFeeRate, tipRate, err := w.canSend(value, true, false)
  3386  	if err != nil {
  3387  		return nil, err
  3388  	}
  3389  	// TODO: Subtract option.
  3390  	// if avail < value+maxFee {
  3391  	// 	value -= maxFee
  3392  	// }
  3393  
  3394  	tx, err := w.sendToAddr(common.HexToAddress(addr), value, maxFeeRate, tipRate)
  3395  	if err != nil {
  3396  		return nil, err
  3397  	}
  3398  
  3399  	txHash := tx.Hash()
  3400  
  3401  	return &coin{id: txHash, value: value}, nil
  3402  }
  3403  
  3404  // Send sends the exact value to the specified address. Fees are taken from the
  3405  // parent wallet. The provided fee rate is ignored since all sends will use an
  3406  // internally derived fee rate.
  3407  func (w *TokenWallet) Send(addr string, value, _ uint64) (asset.Coin, error) {
  3408  	if err := isValidSend(addr, value, false); err != nil {
  3409  		return nil, err
  3410  	}
  3411  
  3412  	_ /* maxFee */, maxFeeRate, tipRate, err := w.canSend(value, true, false)
  3413  	if err != nil {
  3414  		return nil, err
  3415  	}
  3416  
  3417  	tx, err := w.sendToAddr(common.HexToAddress(addr), value, maxFeeRate, tipRate)
  3418  	if err != nil {
  3419  		return nil, err
  3420  	}
  3421  
  3422  	return &coin{id: tx.Hash(), value: value}, nil
  3423  }
  3424  
  3425  // ValidateSecret checks that the secret satisfies the contract.
  3426  func (*baseWallet) ValidateSecret(secret, secretHash []byte) bool {
  3427  	h := sha256.Sum256(secret)
  3428  	return bytes.Equal(h[:], secretHash)
  3429  }
  3430  
  3431  // SyncStatus is information about the blockchain sync status.
  3432  //
  3433  // TODO: Since the merge, the sync status from a geth full node, namely the
  3434  // prog.CurrentBlock prog.HighestBlock, always seem to be the same number.
  3435  // Initial sync will always be zero. Later when restarting the node they move
  3436  // together but never indicate the highest known block on the chain. Further
  3437  // more, requesting the best block header starts to fail after a few tries
  3438  // during initial sync. Investigate how to get correct sync progress.
  3439  func (eth *baseWallet) SyncStatus() (*asset.SyncStatus, error) {
  3440  	prog, tipTime, err := eth.node.syncProgress(eth.ctx)
  3441  	if err != nil {
  3442  		return nil, err
  3443  	}
  3444  	checkHeaderTime := func() bool {
  3445  		// Time in the header is in seconds.
  3446  		timeDiff := time.Now().Unix() - int64(tipTime)
  3447  		if timeDiff > dexeth.MaxBlockInterval && eth.net != dex.Simnet {
  3448  			eth.log.Infof("Time since block (%d sec) exceeds %d sec. "+
  3449  				"Assuming not in sync. Ensure your computer's system clock "+
  3450  				"is correct.", timeDiff, dexeth.MaxBlockInterval)
  3451  			return false
  3452  		}
  3453  		return true
  3454  	}
  3455  	return &asset.SyncStatus{
  3456  		Synced:         checkHeaderTime() && eth.node.peerCount() > 0,
  3457  		StartingBlocks: eth.startingBlocks.Load(),
  3458  		TargetHeight:   prog.HighestBlock,
  3459  		Blocks:         prog.CurrentBlock,
  3460  	}, nil
  3461  }
  3462  
  3463  // DynamicSwapFeesPaid returns fees for initiation transactions. Part of the
  3464  // asset.DynamicSwapper interface.
  3465  func (eth *assetWallet) DynamicSwapFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (fee uint64, secretHashes [][]byte, err error) {
  3466  	return eth.swapOrRedemptionFeesPaid(ctx, coinID, contractData, true)
  3467  }
  3468  
  3469  // DynamicRedemptionFeesPaid returns fees for redemption transactions. Part of
  3470  // the asset.DynamicSwapper interface.
  3471  func (eth *assetWallet) DynamicRedemptionFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (fee uint64, secretHashes [][]byte, err error) {
  3472  	return eth.swapOrRedemptionFeesPaid(ctx, coinID, contractData, false)
  3473  }
  3474  
  3475  // extractSecretHashes extracts the secret hashes from the reedeem or swap tx
  3476  // data. The returned hashes are sorted lexicographically.
  3477  func extractSecretHashes(isInit bool, txData []byte, contractVer uint32) (secretHashes [][]byte, _ error) {
  3478  	defer func() {
  3479  		sort.Slice(secretHashes, func(i, j int) bool { return bytes.Compare(secretHashes[i], secretHashes[j]) < 0 })
  3480  	}()
  3481  	if isInit {
  3482  		inits, err := dexeth.ParseInitiateData(txData, contractVer)
  3483  		if err != nil {
  3484  			return nil, fmt.Errorf("invalid initiate data: %v", err)
  3485  		}
  3486  		secretHashes = make([][]byte, 0, len(inits))
  3487  		for k := range inits {
  3488  			copyK := k
  3489  			secretHashes = append(secretHashes, copyK[:])
  3490  		}
  3491  		return secretHashes, nil
  3492  	}
  3493  	// redeem
  3494  	redeems, err := dexeth.ParseRedeemData(txData, contractVer)
  3495  	if err != nil {
  3496  		return nil, fmt.Errorf("invalid redeem data: %v", err)
  3497  	}
  3498  	secretHashes = make([][]byte, 0, len(redeems))
  3499  	for k := range redeems {
  3500  		copyK := k
  3501  		secretHashes = append(secretHashes, copyK[:])
  3502  	}
  3503  	return secretHashes, nil
  3504  }
  3505  
  3506  // swapOrRedemptionFeesPaid returns exactly how much gwei was used to send an
  3507  // initiation or redemption transaction. It also returns the secret hashes
  3508  // included with this init or redeem. Secret hashes are sorted so returns are
  3509  // always the same, but the order may not be the same as they exist in the
  3510  // transaction on chain. The transaction must be already mined for this
  3511  // function to work. Returns asset.CoinNotFoundError for unmined txn. Returns
  3512  // asset.ErrNotEnoughConfirms for txn with too few confirmations. Will also
  3513  // error if the secret hash in the contractData is not found in the transaction
  3514  // secret hashes.
  3515  func (w *baseWallet) swapOrRedemptionFeesPaid(
  3516  	ctx context.Context,
  3517  	coinID dex.Bytes,
  3518  	contractData dex.Bytes,
  3519  	isInit bool,
  3520  ) (fee uint64, secretHashes [][]byte, err error) {
  3521  
  3522  	var txHash common.Hash
  3523  	copy(txHash[:], coinID)
  3524  
  3525  	contractVer, secretHash, err := dexeth.DecodeContractData(contractData)
  3526  	if err != nil {
  3527  		return 0, nil, err
  3528  	}
  3529  
  3530  	tip := w.tipHeight()
  3531  
  3532  	var blockNum uint64
  3533  	var tx *types.Transaction
  3534  	if w.withLocalTxRead(txHash, func(wt *extendedWalletTx) {
  3535  		blockNum = wt.BlockNumber
  3536  		fee = wt.Fees
  3537  		tx, err = wt.tx()
  3538  		if err != nil {
  3539  			w.log.Errorf("Error decoding wallet transaction %s: %v", txHash, err)
  3540  		}
  3541  	}) && err == nil {
  3542  		if confs := safeConfs(tip, blockNum); confs < w.finalizeConfs {
  3543  			return 0, nil, asset.ErrNotEnoughConfirms
  3544  		}
  3545  		secretHashes, err = extractSecretHashes(isInit, tx.Data(), contractVer)
  3546  		return
  3547  	}
  3548  
  3549  	// We don't have information locally. This really shouldn't happen anymore,
  3550  	// but let's look on-chain anyway.
  3551  
  3552  	receipt, tx, err := w.node.transactionAndReceipt(ctx, txHash)
  3553  	if err != nil {
  3554  		return 0, nil, err
  3555  	}
  3556  
  3557  	if confs := safeConfsBig(tip, receipt.BlockNumber); confs < w.finalizeConfs {
  3558  		return 0, nil, asset.ErrNotEnoughConfirms
  3559  	}
  3560  
  3561  	bigFees := new(big.Int).Mul(receipt.EffectiveGasPrice, big.NewInt(int64(receipt.GasUsed)))
  3562  	fee = dexeth.WeiToGweiCeil(bigFees)
  3563  	secretHashes, err = extractSecretHashes(isInit, tx.Data(), contractVer)
  3564  	if err != nil {
  3565  		return 0, nil, err
  3566  	}
  3567  	var found bool
  3568  	for i := range secretHashes {
  3569  		if bytes.Equal(secretHash[:], secretHashes[i]) {
  3570  			found = true
  3571  			break
  3572  		}
  3573  	}
  3574  	if !found {
  3575  		return 0, nil, fmt.Errorf("secret hash %x not found in transaction", secretHash)
  3576  	}
  3577  	return dexeth.WeiToGweiCeil(bigFees), secretHashes, nil
  3578  }
  3579  
  3580  // RegFeeConfirmations gets the number of confirmations for the specified
  3581  // transaction.
  3582  func (w *baseWallet) RegFeeConfirmations(ctx context.Context, coinID dex.Bytes) (confs uint32, err error) {
  3583  	var txHash common.Hash
  3584  	copy(txHash[:], coinID)
  3585  	if found, txData := w.localTxStatus(txHash); found {
  3586  		if tip := w.tipHeight(); txData.blockNum != 0 && txData.blockNum < tip {
  3587  			return uint32(tip - txData.blockNum + 1), nil
  3588  		}
  3589  		return 0, nil
  3590  	}
  3591  
  3592  	return w.node.transactionConfirmations(ctx, txHash)
  3593  }
  3594  
  3595  // currentNetworkFees give the current base fee rate (from the best header),
  3596  // and recommended tip cap.
  3597  func (w *baseWallet) currentNetworkFees(ctx context.Context) (baseRate, tipRate *big.Int, err error) {
  3598  	tip := w.tipHeight()
  3599  	c := &w.currentFees
  3600  	c.Lock()
  3601  	defer c.Unlock()
  3602  	if tip > 0 && c.blockNum == tip {
  3603  		return c.baseRate, c.tipRate, nil
  3604  	}
  3605  	c.baseRate, c.tipRate, err = w.node.currentFees(ctx)
  3606  	if err != nil {
  3607  		return nil, nil, fmt.Errorf("Error getting net fee state: %v", err)
  3608  	}
  3609  	c.blockNum = tip
  3610  	return c.baseRate, c.tipRate, nil
  3611  }
  3612  
  3613  // currentFeeRate gives the current rate of transactions being mined. Only
  3614  // use this to provide informative realistic estimates of actual fee *use*. For
  3615  // transaction planning, use recommendedMaxFeeRateGwei.
  3616  func (w *baseWallet) currentFeeRate(ctx context.Context) (_ *big.Int, err error) {
  3617  	b, t, err := w.currentNetworkFees(ctx)
  3618  	if err != nil {
  3619  		return nil, err
  3620  	}
  3621  	return new(big.Int).Add(b, t), nil
  3622  }
  3623  
  3624  // recommendedMaxFeeRate finds a recommended max fee rate using the somewhat
  3625  // standard baseRate * 2 + tip formula.
  3626  func (eth *baseWallet) recommendedMaxFeeRate(ctx context.Context) (maxFeeRate, tipRate *big.Int, err error) {
  3627  	base, tip, err := eth.currentNetworkFees(ctx)
  3628  	if err != nil {
  3629  		return nil, nil, fmt.Errorf("Error getting net fee state: %v", err)
  3630  	}
  3631  
  3632  	return new(big.Int).Add(
  3633  		tip,
  3634  		new(big.Int).Mul(base, big.NewInt(2)),
  3635  	), tip, nil
  3636  }
  3637  
  3638  // recommendedMaxFeeRateGwei gets the recommended max fee rate and converts it
  3639  // to gwei.
  3640  func (w *baseWallet) recommendedMaxFeeRateGwei(ctx context.Context) (uint64, error) {
  3641  	feeRate, _, err := w.recommendedMaxFeeRate(ctx)
  3642  	if err != nil {
  3643  		return 0, err
  3644  	}
  3645  	return dexeth.WeiToGweiSafe(feeRate)
  3646  }
  3647  
  3648  // FeeRate satisfies asset.FeeRater.
  3649  func (eth *baseWallet) FeeRate() uint64 {
  3650  	r, err := eth.recommendedMaxFeeRateGwei(eth.ctx)
  3651  	if err != nil {
  3652  		eth.log.Errorf("Error getting max fee recommendation: %v", err)
  3653  		return 0
  3654  	}
  3655  	return r
  3656  }
  3657  
  3658  func (eth *ETHWallet) checkPeers() {
  3659  	numPeers := eth.node.peerCount()
  3660  
  3661  	for _, w := range eth.connectedWallets() {
  3662  		prevPeer := atomic.SwapUint32(&w.lastPeerCount, numPeers)
  3663  		if prevPeer != numPeers {
  3664  			w.peersChange(numPeers, nil)
  3665  		}
  3666  	}
  3667  }
  3668  
  3669  func (eth *ETHWallet) monitorPeers(ctx context.Context) {
  3670  	ticker := time.NewTicker(peerCountTicker)
  3671  	defer ticker.Stop()
  3672  	for {
  3673  		eth.checkPeers()
  3674  
  3675  		select {
  3676  		case <-ticker.C:
  3677  		case <-ctx.Done():
  3678  			return
  3679  		}
  3680  	}
  3681  }
  3682  
  3683  // monitorBlocks pings for new blocks and runs the tipChange callback function
  3684  // when the block changes. New blocks are also scanned for potential contract
  3685  // redeems.
  3686  func (eth *ETHWallet) monitorBlocks(ctx context.Context) {
  3687  	ticker := time.NewTicker(stateUpdateTick)
  3688  	defer ticker.Stop()
  3689  	for {
  3690  		select {
  3691  		case <-ticker.C:
  3692  			eth.checkForNewBlocks(ctx)
  3693  		case <-ctx.Done():
  3694  			return
  3695  		}
  3696  		if ctx.Err() != nil { // shutdown during last check, disallow chance of another tick
  3697  			return
  3698  		}
  3699  	}
  3700  }
  3701  
  3702  // checkForNewBlocks checks for new blocks. When a tip change is detected, the
  3703  // tipChange callback function is invoked and a goroutine is started to check
  3704  // if any contracts in the findRedemptionQueue are redeemed in the new blocks.
  3705  func (eth *ETHWallet) checkForNewBlocks(ctx context.Context) {
  3706  	ctx, cancel := context.WithTimeout(ctx, onChainDataFetchTimeout)
  3707  	defer cancel()
  3708  	bestHdr, err := eth.node.bestHeader(ctx)
  3709  	if err != nil {
  3710  		eth.log.Errorf("failed to get best hash: %v", err)
  3711  		return
  3712  	}
  3713  	bestHash := bestHdr.Hash()
  3714  	// This method is called frequently. Don't hold write lock
  3715  	// unless tip has changed.
  3716  	eth.tipMtx.RLock()
  3717  	currentTipHash := eth.currentTip.Hash()
  3718  	eth.tipMtx.RUnlock()
  3719  	if currentTipHash == bestHash {
  3720  		return
  3721  	}
  3722  
  3723  	eth.tipMtx.Lock()
  3724  	prevTip := eth.currentTip
  3725  	eth.currentTip = bestHdr
  3726  	eth.tipMtx.Unlock()
  3727  
  3728  	eth.log.Tracef("tip change: %s (%s) => %s (%s)", prevTip.Number,
  3729  		currentTipHash, bestHdr.Number, bestHash)
  3730  
  3731  	eth.checkPendingTxs()
  3732  	for _, w := range eth.connectedWallets() {
  3733  		w.checkFindRedemptions()
  3734  		w.checkPendingApprovals()
  3735  		w.emit.TipChange(bestHdr.Number.Uint64())
  3736  	}
  3737  }
  3738  
  3739  // ConfirmRedemption checks the status of a redemption. If a transaction has
  3740  // been fee-replaced, the caller is notified of this by having a different
  3741  // coinID in the returned asset.ConfirmRedemptionStatus as was used to call the
  3742  // function. Fee argument is ignored since it is calculated from the best
  3743  // header.
  3744  func (w *ETHWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, _ uint64) (*asset.ConfirmRedemptionStatus, error) {
  3745  	return w.confirmRedemption(coinID, redemption)
  3746  }
  3747  
  3748  // ConfirmRedemption checks the status of a redemption. If a transaction has
  3749  // been fee-replaced, the caller is notified of this by having a different
  3750  // coinID in the returned asset.ConfirmRedemptionStatus as was used to call the
  3751  // function. Fee argument is ignored since it is calculated from the best
  3752  // header.
  3753  func (w *TokenWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, _ uint64) (*asset.ConfirmRedemptionStatus, error) {
  3754  	return w.confirmRedemption(coinID, redemption)
  3755  }
  3756  
  3757  func confStatus(confs, req uint64, txHash common.Hash) *asset.ConfirmRedemptionStatus {
  3758  	return &asset.ConfirmRedemptionStatus{
  3759  		Confs:  confs,
  3760  		Req:    req,
  3761  		CoinID: txHash[:],
  3762  	}
  3763  }
  3764  
  3765  // confirmRedemption checks the confirmation status of a redemption transaction.
  3766  func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Redemption) (*asset.ConfirmRedemptionStatus, error) {
  3767  	if len(coinID) != common.HashLength {
  3768  		return nil, fmt.Errorf("expected coin ID to be a transaction hash, but it has a length of %d",
  3769  			len(coinID))
  3770  	}
  3771  	var txHash common.Hash
  3772  	copy(txHash[:], coinID)
  3773  
  3774  	contractVer, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract)
  3775  	if err != nil {
  3776  		return nil, fmt.Errorf("failed to decode contract data: %w", err)
  3777  	}
  3778  
  3779  	tip := w.tipHeight()
  3780  
  3781  	// If we have local information, use that.
  3782  	if found, s := w.localTxStatus(txHash); found {
  3783  		if s.assumedLost || len(s.nonceReplacement) > 0 {
  3784  			if !s.feeReplacement {
  3785  				// Tell core to update it's coin ID.
  3786  				txHash = common.HexToHash(s.nonceReplacement)
  3787  			} else {
  3788  				return nil, asset.ErrTxLost
  3789  			}
  3790  		}
  3791  
  3792  		var confirmStatus *asset.ConfirmRedemptionStatus
  3793  		if s.blockNum != 0 && s.blockNum <= tip {
  3794  			confirmStatus = confStatus(tip-s.blockNum+1, w.finalizeConfs, txHash)
  3795  		} else {
  3796  			// Apparently not mined yet.
  3797  			confirmStatus = confStatus(0, w.finalizeConfs, txHash)
  3798  		}
  3799  		if s.receipt != nil && s.receipt.Status != types.ReceiptStatusSuccessful && confirmStatus.Confs >= w.finalizeConfs {
  3800  			return nil, asset.ErrTxRejected
  3801  		}
  3802  		return confirmStatus, nil
  3803  	}
  3804  
  3805  	// We know nothing of the tx locally. This shouldn't really happen, but
  3806  	// we'll look for it on-chain anyway.
  3807  	r, err := w.node.transactionReceipt(w.ctx, txHash)
  3808  	if err != nil {
  3809  		if errors.Is(err, asset.CoinNotFoundError) {
  3810  			// We don't know it ourselves and we can't see it on-chain. This
  3811  			// used to be a CoinNotFoundError, but since we have local tx
  3812  			// storage, we'll assume it's lost to space and time now.
  3813  			return nil, asset.ErrTxLost
  3814  		}
  3815  		return nil, err
  3816  	}
  3817  
  3818  	// We could potentially grab the tx, check the from address, and store it
  3819  	// to our db right here, but I suspect that this case would be exceedingly
  3820  	// rare anyway.
  3821  
  3822  	confs := safeConfsBig(tip, r.BlockNumber)
  3823  	if confs >= w.finalizeConfs {
  3824  		if r.Status == types.ReceiptStatusSuccessful {
  3825  			return confStatus(w.finalizeConfs, w.finalizeConfs, txHash), nil
  3826  		}
  3827  		// We weren't able to redeem. Perhaps fees were too low, but we'll
  3828  		// check the status in the contract for a couple of other conditions.
  3829  		swap, err := w.swap(w.ctx, secretHash, contractVer)
  3830  		if err != nil {
  3831  			return nil, fmt.Errorf("error pulling swap data from contract: %v", err)
  3832  		}
  3833  		switch swap.State {
  3834  		case dexeth.SSRedeemed:
  3835  			w.log.Infof("Redemption in tx %s was apparently redeemed by another tx. OK.", txHash)
  3836  			return confStatus(w.finalizeConfs, w.finalizeConfs, txHash), nil
  3837  		case dexeth.SSRefunded:
  3838  			return nil, asset.ErrSwapRefunded
  3839  		}
  3840  
  3841  		err = fmt.Errorf("tx %s failed to redeem %s funds", txHash, dex.BipIDSymbol(w.assetID))
  3842  		return nil, errors.Join(err, asset.ErrTxRejected)
  3843  	}
  3844  	return confStatus(confs, w.finalizeConfs, txHash), nil
  3845  }
  3846  
  3847  // withLocalTxRead runs a function that reads a pending or DB tx under
  3848  // read-lock. Certain DB transactions in undeterminable states will not be
  3849  // used.
  3850  func (w *baseWallet) withLocalTxRead(txHash common.Hash, f func(*extendedWalletTx)) bool {
  3851  	withPendingTxRead := func(txHash common.Hash, f func(*extendedWalletTx)) bool {
  3852  		w.nonceMtx.RLock()
  3853  		defer w.nonceMtx.RUnlock()
  3854  		for _, pendingTx := range w.pendingTxs {
  3855  			if pendingTx.txHash == txHash {
  3856  				f(pendingTx)
  3857  				return true
  3858  			}
  3859  		}
  3860  		return false
  3861  	}
  3862  	if withPendingTxRead(txHash, f) {
  3863  		return true
  3864  	}
  3865  	// Could be finalized and in the database.
  3866  	if confirmedTx, err := w.txDB.getTx(txHash); err != nil {
  3867  		w.log.Errorf("Error getting DB transaction: %v", err)
  3868  	} else if confirmedTx != nil {
  3869  		if !confirmedTx.Confirmed && confirmedTx.Receipt == nil && !confirmedTx.AssumedLost && confirmedTx.NonceReplacement == "" {
  3870  			// If it's in the db but not in pendingTxs, and we know nothing
  3871  			// about the tx, don't use it.
  3872  			return false
  3873  		}
  3874  		f(confirmedTx)
  3875  		return true
  3876  	}
  3877  	return false
  3878  }
  3879  
  3880  // walletTxStatus is data copied from an extendedWalletTx.
  3881  type walletTxStatus struct {
  3882  	confirmed        bool
  3883  	blockNum         uint64
  3884  	nonceReplacement string
  3885  	feeReplacement   bool
  3886  	receipt          *types.Receipt
  3887  	assumedLost      bool
  3888  }
  3889  
  3890  // localTxStatus looks for an extendedWalletTx and copies critical values to
  3891  // a walletTxStatus for use without mutex protection.
  3892  func (w *baseWallet) localTxStatus(txHash common.Hash) (_ bool, s *walletTxStatus) {
  3893  	return w.withLocalTxRead(txHash, func(wt *extendedWalletTx) {
  3894  		s = &walletTxStatus{
  3895  			confirmed:        wt.Confirmed,
  3896  			blockNum:         wt.BlockNumber,
  3897  			nonceReplacement: wt.NonceReplacement,
  3898  			feeReplacement:   wt.FeeReplacement,
  3899  			receipt:          wt.Receipt,
  3900  			assumedLost:      wt.AssumedLost,
  3901  		}
  3902  	}), s
  3903  }
  3904  
  3905  // checkFindRedemptions checks queued findRedemptionRequests.
  3906  func (w *assetWallet) checkFindRedemptions() {
  3907  	for secretHash, req := range w.findRedemptionRequests() {
  3908  		if w.ctx.Err() != nil {
  3909  			return
  3910  		}
  3911  		secret, makerAddr, err := w.findSecret(secretHash, req.contractVer)
  3912  		if err != nil {
  3913  			w.sendFindRedemptionResult(req, secretHash, nil, "", err)
  3914  		} else if len(secret) > 0 {
  3915  			w.sendFindRedemptionResult(req, secretHash, secret, makerAddr, nil)
  3916  		}
  3917  	}
  3918  }
  3919  
  3920  func (w *assetWallet) checkPendingApprovals() {
  3921  	w.approvalsMtx.Lock()
  3922  	defer w.approvalsMtx.Unlock()
  3923  
  3924  	for version, pendingApproval := range w.pendingApprovals {
  3925  		if w.ctx.Err() != nil {
  3926  			return
  3927  		}
  3928  		var confirmed bool
  3929  		if found, txData := w.localTxStatus(pendingApproval.txHash); found {
  3930  			confirmed = txData.blockNum > 0 && txData.blockNum <= w.tipHeight()
  3931  		} else {
  3932  			confs, err := w.node.transactionConfirmations(w.ctx, pendingApproval.txHash)
  3933  			if err != nil && !errors.Is(err, asset.CoinNotFoundError) {
  3934  				w.log.Errorf("error getting confirmations for tx %s: %v", pendingApproval.txHash, err)
  3935  				continue
  3936  			}
  3937  			confirmed = confs > 0
  3938  		}
  3939  		if confirmed {
  3940  			go pendingApproval.onConfirm()
  3941  			delete(w.pendingApprovals, version)
  3942  		}
  3943  	}
  3944  }
  3945  
  3946  // sumPendingTxs sums the expected incoming and outgoing values in pending
  3947  // transactions stored in pendingTxs. Not used if the node is a
  3948  // txPoolFetcher.
  3949  func (w *assetWallet) sumPendingTxs() (out, in uint64) {
  3950  	isToken := w.assetID != w.baseChainID
  3951  
  3952  	sumPendingTx := func(pendingTx *extendedWalletTx) {
  3953  		// Already confirmed, but still in the unconfirmed txs map waiting for
  3954  		// txConfsNeededToConfirm confirmations.
  3955  		if pendingTx.BlockNumber != 0 {
  3956  			return
  3957  		}
  3958  
  3959  		txAssetID := w.baseChainID
  3960  		if pendingTx.TokenID != nil {
  3961  			txAssetID = *pendingTx.TokenID
  3962  		}
  3963  
  3964  		if txAssetID == w.assetID {
  3965  			if asset.IncomingTxType(pendingTx.Type) {
  3966  				in += pendingTx.Amount
  3967  			} else {
  3968  				out += pendingTx.Amount
  3969  			}
  3970  		}
  3971  		if !isToken {
  3972  			out += pendingTx.Fees
  3973  		}
  3974  	}
  3975  
  3976  	w.nonceMtx.RLock()
  3977  	defer w.nonceMtx.RUnlock()
  3978  
  3979  	for _, pendingTx := range w.pendingTxs {
  3980  		sumPendingTx(pendingTx)
  3981  	}
  3982  
  3983  	return
  3984  }
  3985  
  3986  func (w *assetWallet) getConfirmedBalance() (*big.Int, error) {
  3987  	now := time.Now()
  3988  	tip := w.tipHeight()
  3989  
  3990  	w.balances.Lock()
  3991  	defer w.balances.Unlock()
  3992  
  3993  	if w.balances.m == nil {
  3994  		w.balances.m = make(map[uint32]*cachedBalance)
  3995  	}
  3996  	// Check to see if we already have one up-to-date
  3997  	cached := w.balances.m[w.assetID]
  3998  	if cached != nil && cached.height == tip && time.Since(cached.stamp) < time.Minute {
  3999  		return cached.bal, nil
  4000  	}
  4001  
  4002  	if w.multiBalanceContract == nil {
  4003  		var bal *big.Int
  4004  		var err error
  4005  		if w.assetID == w.baseChainID {
  4006  			bal, err = w.node.addressBalance(w.ctx, w.addr)
  4007  		} else {
  4008  			bal, err = w.tokenBalance()
  4009  		}
  4010  		if err != nil {
  4011  			return nil, err
  4012  		}
  4013  		w.balances.m[w.assetID] = &cachedBalance{
  4014  			stamp:  now,
  4015  			height: tip,
  4016  			bal:    bal,
  4017  		}
  4018  		return bal, nil
  4019  	}
  4020  
  4021  	// Either not cached, or outdated. Fetch anew.
  4022  	var tokenAddrs []common.Address
  4023  	idIndexes := map[int]uint32{
  4024  		0: w.baseChainID,
  4025  	}
  4026  	i := 1
  4027  	for assetID, tkn := range w.tokens {
  4028  		netToken := tkn.NetTokens[w.net]
  4029  		if netToken == nil || netToken.Address == (common.Address{}) {
  4030  			continue
  4031  		}
  4032  		tokenAddrs = append(tokenAddrs, netToken.Address)
  4033  		idIndexes[i] = assetID
  4034  		i++
  4035  	}
  4036  	callOpts := &bind.CallOpts{
  4037  		From:    w.addr,
  4038  		Context: w.ctx,
  4039  	}
  4040  
  4041  	bals, err := w.multiBalanceContract.Balances(callOpts, w.addr, tokenAddrs)
  4042  	if err != nil {
  4043  		return nil, fmt.Errorf("error fetching multi-balance: %w", err)
  4044  	}
  4045  	if len(bals) != len(idIndexes) {
  4046  		return nil, fmt.Errorf("wrong number of balances in multi-balance result. wanted %d, got %d", len(idIndexes), len(bals))
  4047  	}
  4048  	var reqBal *big.Int
  4049  	for i, bal := range bals {
  4050  		assetID := idIndexes[i]
  4051  		if assetID == w.assetID {
  4052  			reqBal = bal
  4053  		}
  4054  		w.balances.m[assetID] = &cachedBalance{
  4055  			stamp:  now,
  4056  			height: tip,
  4057  			bal:    bal,
  4058  		}
  4059  	}
  4060  	if reqBal == nil {
  4061  		return nil, fmt.Errorf("requested asset not in multi-balance result: %v", err)
  4062  	}
  4063  	return reqBal, nil
  4064  }
  4065  
  4066  func (w *assetWallet) balanceWithTxPool() (*Balance, error) {
  4067  	isToken := w.assetID != w.baseChainID
  4068  	confirmed, err := w.getConfirmedBalance()
  4069  	if err != nil {
  4070  		return nil, fmt.Errorf("balance error: %v", err)
  4071  	}
  4072  
  4073  	txPoolNode, is := w.node.(txPoolFetcher)
  4074  	if !is {
  4075  		for {
  4076  			out, in := w.sumPendingTxs()
  4077  			checkBal, err := w.getConfirmedBalance()
  4078  			if err != nil {
  4079  				return nil, fmt.Errorf("balance consistency check error: %v", err)
  4080  			}
  4081  			outEVM := w.evmify(out)
  4082  			// If our balance has gone down in the interim, we'll use the lower
  4083  			// balance, but ensure that we're not setting up an underflow for
  4084  			// available balance.
  4085  			if checkBal.Cmp(confirmed) != 0 {
  4086  				w.log.Debugf("balance changed while checking pending txs. Trying again.")
  4087  				confirmed = checkBal
  4088  				continue
  4089  			}
  4090  			if outEVM.Cmp(confirmed) > 0 {
  4091  				return nil, fmt.Errorf("balance underflow detected. pending out (%s) > balance (%s)", outEVM, confirmed)
  4092  			}
  4093  
  4094  			return &Balance{
  4095  				Current:    confirmed,
  4096  				PendingOut: outEVM,
  4097  				PendingIn:  w.evmify(in),
  4098  			}, nil
  4099  		}
  4100  	}
  4101  
  4102  	pendingTxs, err := txPoolNode.pendingTransactions()
  4103  	if err != nil {
  4104  		return nil, fmt.Errorf("error getting pending txs: %w", err)
  4105  	}
  4106  
  4107  	outgoing := new(big.Int)
  4108  	incoming := new(big.Int)
  4109  	zero := new(big.Int)
  4110  
  4111  	addFees := func(tx *types.Transaction) {
  4112  		gas := new(big.Int).SetUint64(tx.Gas())
  4113  		// For legacy transactions, GasFeeCap returns gas price
  4114  		if gasFeeCap := tx.GasFeeCap(); gasFeeCap != nil {
  4115  			outgoing.Add(outgoing, new(big.Int).Mul(gas, gasFeeCap))
  4116  		} else {
  4117  			w.log.Errorf("unable to calculate fees for tx %s", tx.Hash())
  4118  		}
  4119  	}
  4120  
  4121  	ethSigner := types.LatestSigner(w.node.chainConfig()) // "latest" good for pending
  4122  
  4123  	for _, tx := range pendingTxs {
  4124  		from, _ := ethSigner.Sender(tx)
  4125  		if from != w.addr {
  4126  			continue
  4127  		}
  4128  
  4129  		if !isToken {
  4130  			// Add tx fees
  4131  			addFees(tx)
  4132  		}
  4133  
  4134  		var contractOut uint64
  4135  		for ver, c := range w.contractors {
  4136  			in, out, err := c.value(w.ctx, tx)
  4137  			if err != nil {
  4138  				w.log.Errorf("version %d contractor incomingValue error: %v", ver, err)
  4139  				continue
  4140  			}
  4141  			contractOut += out
  4142  			if in > 0 {
  4143  				incoming.Add(incoming, w.evmify(in))
  4144  			}
  4145  		}
  4146  		if contractOut > 0 {
  4147  			outgoing.Add(outgoing, w.evmify(contractOut))
  4148  		} else if !isToken {
  4149  			// Count withdraws and sends for ETH.
  4150  			v := tx.Value()
  4151  			if v.Cmp(zero) > 0 {
  4152  				outgoing.Add(outgoing, v)
  4153  			}
  4154  		}
  4155  	}
  4156  
  4157  	return &Balance{
  4158  		Current:    confirmed,
  4159  		PendingOut: outgoing,
  4160  		PendingIn:  incoming,
  4161  	}, nil
  4162  }
  4163  
  4164  // Uncomment here and in sendToAddr to test actionTypeLostNonce.
  4165  // var nonceBorked atomic.Bool
  4166  // func (w *ETHWallet) borkNonce(tx *types.Transaction) {
  4167  // 	fmt.Printf("\n##### losing tx for lost nonce testing\n\n")
  4168  // 	txHash := tx.Hash()
  4169  // 	v := big.NewInt(dexeth.GweiFactor)
  4170  // 	spoofTx := types.NewTransaction(tx.Nonce(), w.addr, v, defaultSendGasLimit, v, nil)
  4171  // 	spoofHash := tx.Hash()
  4172  // 	pendingSpoofTx := w.extendedTx(spoofTx, asset.SelfSend, dexeth.GweiFactor)
  4173  // 	w.nonceMtx.Lock()
  4174  // 	for i, pendingTx := range w.pendingTxs {
  4175  // 		if pendingTx.txHash == txHash {
  4176  // 			w.pendingTxs[i] = pendingSpoofTx
  4177  // 			w.tryStoreDBTx(pendingSpoofTx)
  4178  // 			fmt.Printf("\n##### Replaced pending tx %s with spoof tx %s, nonce %d \n\n", txHash, spoofHash, tx.Nonce())
  4179  // 			break
  4180  // 		}
  4181  // 	}
  4182  // 	w.nonceMtx.Unlock()
  4183  // }
  4184  
  4185  // Uncomment here and in sendToAddr to test actionTypeMissingNonces.
  4186  // var nonceFuturized atomic.Bool
  4187  
  4188  // Uncomment here and in sendToAddr to test actionTypeTooCheap
  4189  // var nonceSkimped atomic.Bool
  4190  
  4191  // sendToAddr sends funds to the address.
  4192  func (w *ETHWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate, tipRate *big.Int) (tx *types.Transaction, err error) {
  4193  
  4194  	// Uncomment here and above to test actionTypeLostNonce.
  4195  	// Also change txAgeOut to like 1 minute.
  4196  	// if nonceBorked.CompareAndSwap(false, true) {
  4197  	// 	defer w.borkNonce(tx)
  4198  	// }
  4199  
  4200  	return tx, w.withNonce(w.ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) {
  4201  
  4202  		// Uncomment here and above to test actionTypeMissingNonces.
  4203  		// if nonceFuturized.CompareAndSwap(false, true) {
  4204  		// 	fmt.Printf("\n##### advancing nonce for missing nonce testing\n\n")
  4205  		// 	nonce.Add(nonce, big.NewInt(3))
  4206  		// }
  4207  
  4208  		// Uncomment here and above to test actionTypeTooCheap.
  4209  		// if nonceSkimped.CompareAndSwap(false, true) {
  4210  		// 	fmt.Printf("\n##### lower max fee rate to test cheap tx handling\n\n")
  4211  		// 	maxFeeRate.SetUint64(1)
  4212  		// 	tipRate.SetUint64(0)
  4213  		// }
  4214  
  4215  		txOpts, err := w.node.txOpts(w.ctx, amt, defaultSendGasLimit, maxFeeRate, tipRate, nonce)
  4216  		if err != nil {
  4217  			return nil, 0, 0, nil, err
  4218  		}
  4219  		tx, err = w.node.sendTransaction(w.ctx, txOpts, addr, nil)
  4220  		if err != nil {
  4221  			return nil, 0, 0, nil, err
  4222  		}
  4223  		txType := asset.Send
  4224  		if addr == w.addr {
  4225  			txType = asset.SelfSend
  4226  		}
  4227  		recipient := addr.Hex()
  4228  		return tx, txType, amt, &recipient, nil
  4229  	})
  4230  }
  4231  
  4232  // sendToAddr sends funds to the address.
  4233  func (w *TokenWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate, tipRate *big.Int) (tx *types.Transaction, err error) {
  4234  	g := w.gases(contractVersionNewest)
  4235  	if g == nil {
  4236  		return nil, fmt.Errorf("no gas table")
  4237  	}
  4238  	return tx, w.withNonce(w.ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) {
  4239  		txOpts, err := w.node.txOpts(w.ctx, 0, g.Transfer, maxFeeRate, tipRate, nonce)
  4240  		if err != nil {
  4241  			return nil, 0, 0, nil, err
  4242  		}
  4243  		txType := asset.Send
  4244  		if addr == w.addr {
  4245  			txType = asset.SelfSend
  4246  		}
  4247  		recipient := addr.Hex()
  4248  		return tx, txType, amt, &recipient, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error {
  4249  			tx, err = c.transfer(txOpts, addr, w.evmify(amt))
  4250  			if err != nil {
  4251  				return err
  4252  			}
  4253  			return nil
  4254  		})
  4255  	})
  4256  
  4257  }
  4258  
  4259  // swap gets a swap keyed by secretHash in the contract.
  4260  func (w *assetWallet) swap(ctx context.Context, secretHash [32]byte, contractVer uint32) (swap *dexeth.SwapState, err error) {
  4261  	return swap, w.withContractor(contractVer, func(c contractor) error {
  4262  		swap, err = c.swap(ctx, secretHash)
  4263  		return err
  4264  	})
  4265  }
  4266  
  4267  // initiate initiates multiple swaps in the same transaction.
  4268  func (w *assetWallet) initiate(
  4269  	ctx context.Context, assetID uint32, contracts []*asset.Contract, gasLimit uint64, maxFeeRate, tipRate *big.Int, contractVer uint32,
  4270  ) (tx *types.Transaction, err error) {
  4271  
  4272  	var val, amt uint64
  4273  	for _, c := range contracts {
  4274  		amt += c.Value
  4275  		if assetID == w.baseChainID {
  4276  			val += c.Value
  4277  		}
  4278  	}
  4279  	return tx, w.withNonce(ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) {
  4280  		txOpts, err := w.node.txOpts(ctx, val, gasLimit, maxFeeRate, tipRate, nonce)
  4281  		if err != nil {
  4282  			return nil, 0, 0, nil, err
  4283  		}
  4284  
  4285  		return tx, asset.Swap, amt, nil, w.withContractor(contractVer, func(c contractor) error {
  4286  			tx, err = c.initiate(txOpts, contracts)
  4287  			return err
  4288  		})
  4289  	})
  4290  }
  4291  
  4292  // estimateInitGas checks the amount of gas that is used for the
  4293  // initialization.
  4294  func (w *assetWallet) estimateInitGas(ctx context.Context, numSwaps int, contractVer uint32) (gas uint64, err error) {
  4295  	return gas, w.withContractor(contractVer, func(c contractor) error {
  4296  		gas, err = c.estimateInitGas(ctx, numSwaps)
  4297  		return err
  4298  	})
  4299  }
  4300  
  4301  // estimateRedeemGas checks the amount of gas that is used for the redemption.
  4302  // Only used with testing and development tools like the
  4303  // nodeclient_harness_test.go suite (GetGasEstimates, testRedeemGas, etc.).
  4304  // Never use this with a public RPC provider, especially as maker, since it
  4305  // reveals the secret keys.
  4306  func (w *assetWallet) estimateRedeemGas(ctx context.Context, secrets [][32]byte, contractVer uint32) (gas uint64, err error) {
  4307  	return gas, w.withContractor(contractVer, func(c contractor) error {
  4308  		gas, err = c.estimateRedeemGas(ctx, secrets)
  4309  		return err
  4310  	})
  4311  }
  4312  
  4313  // estimateRefundGas checks the amount of gas that is used for a refund.
  4314  func (w *assetWallet) estimateRefundGas(ctx context.Context, secretHash [32]byte, contractVer uint32) (gas uint64, err error) {
  4315  	return gas, w.withContractor(contractVer, func(c contractor) error {
  4316  		gas, err = c.estimateRefundGas(ctx, secretHash)
  4317  		return err
  4318  	})
  4319  }
  4320  
  4321  // loadContractors prepares the token contractors and add them to the map.
  4322  func (w *assetWallet) loadContractors() error {
  4323  	token, found := w.tokens[w.assetID]
  4324  	if !found {
  4325  		return fmt.Errorf("token %d not found", w.assetID)
  4326  	}
  4327  	netToken, found := token.NetTokens[w.net]
  4328  	if !found {
  4329  		return fmt.Errorf("token %d not found", w.assetID)
  4330  	}
  4331  
  4332  	for ver := range netToken.SwapContracts {
  4333  		constructor, found := tokenContractorConstructors[ver]
  4334  		if !found {
  4335  			w.log.Errorf("contractor constructor not found for token %s, version %d", token.Name, ver)
  4336  			continue
  4337  		}
  4338  		c, err := constructor(w.net, token, w.addr, w.node.contractBackend())
  4339  		if err != nil {
  4340  			return fmt.Errorf("error constructing token %s contractor version %d: %w", token.Name, ver, err)
  4341  		}
  4342  
  4343  		if netToken.Address != c.tokenAddress() {
  4344  			return fmt.Errorf("wrong %s token address. expected %s, got %s", token.Name, netToken.Address, c.tokenAddress())
  4345  		}
  4346  
  4347  		w.contractors[ver] = c
  4348  	}
  4349  	return nil
  4350  }
  4351  
  4352  // withContractor runs the provided function with the versioned contractor.
  4353  func (w *assetWallet) withContractor(contractVer uint32, f func(contractor) error) error {
  4354  	if contractVer == contractVersionNewest {
  4355  		var bestVer uint32
  4356  		var bestContractor contractor
  4357  		for ver, c := range w.contractors {
  4358  			if ver >= bestVer {
  4359  				bestContractor = c
  4360  				bestVer = ver
  4361  			}
  4362  		}
  4363  		return f(bestContractor)
  4364  	}
  4365  	contractor, found := w.contractors[contractVer]
  4366  	if !found {
  4367  		return fmt.Errorf("no version %d contractor for asset %d", contractVer, w.assetID)
  4368  	}
  4369  	return f(contractor)
  4370  }
  4371  
  4372  // withTokenContractor runs the provided function with the tokenContractor.
  4373  func (w *assetWallet) withTokenContractor(assetID, ver uint32, f func(tokenContractor) error) error {
  4374  	return w.withContractor(ver, func(c contractor) error {
  4375  		tc, is := c.(tokenContractor)
  4376  		if !is {
  4377  			return fmt.Errorf("contractor for %d %T is not a tokenContractor", assetID, c)
  4378  		}
  4379  		return f(tc)
  4380  	})
  4381  }
  4382  
  4383  // estimateApproveGas estimates the gas required for a transaction approving a
  4384  // spender for an ERC20 contract.
  4385  func (w *assetWallet) estimateApproveGas(newGas *big.Int) (gas uint64, err error) {
  4386  	return gas, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error {
  4387  		gas, err = c.estimateApproveGas(w.ctx, newGas)
  4388  		return err
  4389  	})
  4390  }
  4391  
  4392  // estimateTransferGas estimates the gas needed for a token transfer call to an
  4393  // ERC20 contract.
  4394  func (w *assetWallet) estimateTransferGas(val uint64) (gas uint64, err error) {
  4395  	return gas, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error {
  4396  		gas, err = c.estimateTransferGas(w.ctx, w.evmify(val))
  4397  		return err
  4398  	})
  4399  }
  4400  
  4401  // Can uncomment here and in redeem to test rejected redemption reauthorization.
  4402  // var firstRedemptionBorked atomic.Bool
  4403  
  4404  // Uncomment here and below to test core's handling of lost redemption txs.
  4405  // var firstRedemptionLost atomic.Bool
  4406  
  4407  // redeem redeems a swap contract. Any on-chain failure, such as this secret not
  4408  // matching the hash, will not cause this to error.
  4409  func (w *assetWallet) redeem(
  4410  	ctx context.Context,
  4411  	redemptions []*asset.Redemption,
  4412  	maxFeeRate uint64,
  4413  	tipRate *big.Int,
  4414  	gasLimit uint64,
  4415  	contractVer uint32,
  4416  ) (tx *types.Transaction, err error) {
  4417  
  4418  	// // Uncomment here and above to test core's handling of ErrTxLost from
  4419  	// if firstRedemptionLost.CompareAndSwap(false, true) {
  4420  	// 	fmt.Printf("\n##### Spoofing tx for lost tx testing\n\n")
  4421  	// 	return types.NewTransaction(10, w.addr, big.NewInt(dexeth.GweiFactor), gasLimit, dexeth.GweiToWei(maxFeeRate), nil), nil
  4422  	// }
  4423  
  4424  	return tx, w.withNonce(ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) {
  4425  		var amt uint64
  4426  		for _, r := range redemptions {
  4427  			amt += r.Spends.Coin.Value()
  4428  		}
  4429  		// Uncomment here and above to test rejected redemption handling.
  4430  		// if firstRedemptionBorked.CompareAndSwap(false, true) {
  4431  		// 	fmt.Printf("\n##### Borking gas limit for rejected tx testing\n\n")
  4432  		// 	gasLimit /= 4
  4433  		// }
  4434  
  4435  		txOpts, err := w.node.txOpts(ctx, 0, gasLimit, dexeth.GweiToWei(maxFeeRate), tipRate, nonce)
  4436  		if err != nil {
  4437  			return nil, 0, 0, nil, err
  4438  		}
  4439  		return tx, asset.Redeem, amt, nil, w.withContractor(contractVer, func(c contractor) error {
  4440  			tx, err = c.redeem(txOpts, redemptions)
  4441  			return err
  4442  		})
  4443  	})
  4444  }
  4445  
  4446  // refund refunds a swap contract using the account controlled by the wallet.
  4447  // Any on-chain failure, such as the locktime not being past, will not cause
  4448  // this to error.
  4449  func (w *assetWallet) refund(secretHash [32]byte, amt uint64, maxFeeRate, tipRate *big.Int, contractVer uint32) (tx *types.Transaction, err error) {
  4450  	gas := w.gases(contractVer)
  4451  	if gas == nil {
  4452  		return nil, fmt.Errorf("no gas table for asset %d, version %d", w.assetID, contractVer)
  4453  	}
  4454  	return tx, w.withNonce(w.ctx, func(nonce *big.Int) (*types.Transaction, asset.TransactionType, uint64, *string, error) {
  4455  		txOpts, err := w.node.txOpts(w.ctx, 0, gas.Refund, maxFeeRate, tipRate, nonce)
  4456  		if err != nil {
  4457  			return nil, 0, 0, nil, err
  4458  		}
  4459  		return tx, asset.Refund, amt, nil, w.withContractor(contractVer, func(c contractor) error {
  4460  			tx, err = c.refund(txOpts, secretHash)
  4461  			return err
  4462  		})
  4463  	})
  4464  }
  4465  
  4466  // isRedeemable checks if the swap identified by secretHash is redeemable using
  4467  // secret. This must NOT be a contractor call.
  4468  func (w *assetWallet) isRedeemable(secretHash [32]byte, secret [32]byte, contractVer uint32) (redeemable bool, err error) {
  4469  	swap, err := w.swap(w.ctx, secretHash, contractVer)
  4470  	if err != nil {
  4471  		return false, err
  4472  	}
  4473  
  4474  	if swap.State != dexeth.SSInitiated {
  4475  		return false, nil
  4476  	}
  4477  
  4478  	return w.ValidateSecret(secret[:], secretHash[:]), nil
  4479  }
  4480  
  4481  func (w *assetWallet) isRefundable(secretHash [32]byte, contractVer uint32) (refundable bool, err error) {
  4482  	return refundable, w.withContractor(contractVer, func(c contractor) error {
  4483  		refundable, err = c.isRefundable(secretHash)
  4484  		return err
  4485  	})
  4486  }
  4487  
  4488  func checkTxStatus(receipt *types.Receipt, gasLimit uint64) error {
  4489  	if receipt.Status != types.ReceiptStatusSuccessful {
  4490  		return fmt.Errorf("transaction status failed")
  4491  
  4492  	}
  4493  
  4494  	if receipt.GasUsed > gasLimit {
  4495  		return fmt.Errorf("gas used, %d, appears to have exceeded limit of %d", receipt.GasUsed, gasLimit)
  4496  	}
  4497  
  4498  	return nil
  4499  }
  4500  
  4501  // emitTransactionNote sends a TransactionNote to the base asset wallet and
  4502  // also the wallet, if applicable.
  4503  func (w *baseWallet) emitTransactionNote(tx *asset.WalletTransaction, new bool) {
  4504  	w.walletsMtx.RLock()
  4505  	baseWallet, found := w.wallets[w.baseChainID]
  4506  	var tokenWallet *assetWallet
  4507  	if tx.TokenID != nil {
  4508  		tokenWallet = w.wallets[*tx.TokenID]
  4509  	}
  4510  	w.walletsMtx.RUnlock()
  4511  
  4512  	if found {
  4513  		baseWallet.emit.TransactionNote(tx, new)
  4514  	} else {
  4515  		w.log.Error("emitTransactionNote: base asset wallet not found")
  4516  	}
  4517  	if tokenWallet != nil {
  4518  		tokenWallet.emit.TransactionNote(tx, new)
  4519  	}
  4520  }
  4521  
  4522  func findMissingNonces(confirmedAt, pendingAt *big.Int, pendingTxs []*extendedWalletTx) (ns []uint64) {
  4523  	pendingTxMap := make(map[uint64]struct{})
  4524  	// It's not clear whether all providers will update PendingNonceAt if
  4525  	// there are gaps. geth doesn't do it on simnet, apparently. We'll use
  4526  	// our own pendingTxs max nonce as a backup.
  4527  	nonceHigh := big.NewInt(-1)
  4528  	for _, pendingTx := range pendingTxs {
  4529  		if pendingTx.indexed && pendingTx.Nonce.Cmp(nonceHigh) > 0 {
  4530  			nonceHigh.Set(pendingTx.Nonce)
  4531  		}
  4532  		pendingTxMap[pendingTx.Nonce.Uint64()] = struct{}{}
  4533  	}
  4534  	nonceHigh.Add(nonceHigh, big.NewInt(1))
  4535  	if pendingAt.Cmp(nonceHigh) > 0 {
  4536  		nonceHigh.Set(pendingAt)
  4537  	}
  4538  	for n := confirmedAt.Uint64(); n < nonceHigh.Uint64(); n++ {
  4539  		if _, found := pendingTxMap[n]; !found {
  4540  			ns = append(ns, n)
  4541  		}
  4542  	}
  4543  	return
  4544  }
  4545  
  4546  func (w *baseWallet) missingNoncesActionID() string {
  4547  	return fmt.Sprintf("missingNonces_%d", w.baseChainID)
  4548  }
  4549  
  4550  // updatePendingTx checks the confirmation status of a transaction. The
  4551  // BlockNumber, Fees, and Timestamp fields of the extendedWalletTx are updated
  4552  // if the transaction is confirmed, and if the transaction has reached the
  4553  // required number of confirmations, it is removed from w.pendingTxs.
  4554  //
  4555  // w.nonceMtx must be held.
  4556  func (w *baseWallet) updatePendingTx(tip uint64, pendingTx *extendedWalletTx) {
  4557  	if pendingTx.Confirmed && pendingTx.savedToDB {
  4558  		return
  4559  	}
  4560  	waitingOnConfs := pendingTx.BlockNumber > 0 && safeConfs(tip, pendingTx.BlockNumber) < w.finalizeConfs
  4561  	if waitingOnConfs {
  4562  		// We're just waiting on confs. Don't check again until we expect to be
  4563  		// finalized.
  4564  		return
  4565  	}
  4566  	// Only check when the tip has changed.
  4567  	if pendingTx.lastCheck == tip {
  4568  		return
  4569  	}
  4570  	pendingTx.lastCheck = tip
  4571  
  4572  	var updated bool
  4573  	defer func() {
  4574  		if updated || !pendingTx.savedToDB {
  4575  			w.tryStoreDBTx(pendingTx)
  4576  			w.emitTransactionNote(pendingTx.WalletTransaction, false)
  4577  		}
  4578  	}()
  4579  
  4580  	receipt, tx, err := w.node.transactionAndReceipt(w.ctx, pendingTx.txHash)
  4581  	if w.log.Level() == dex.LevelTrace {
  4582  		w.log.Tracef("Attempted to fetch tx and receipt for %s. receipt = %+v, tx = %+v, err = %+v", pendingTx.txHash, receipt, tx, err)
  4583  	}
  4584  	if err != nil {
  4585  		if errors.Is(err, asset.CoinNotFoundError) {
  4586  			pendingTx.indexed = tx != nil
  4587  			// transactionAndReceipt will return a CoinNotFound for either no
  4588  			// reciept or no tx. If they report the tx, we'll consider the tx
  4589  			// to be "indexed", and won't send lost tx action-required requests.
  4590  			if pendingTx.BlockNumber > 0 {
  4591  				w.log.Warnf("Transaction %s was previously mined but now cannot be confirmed. Might be normal.", pendingTx.txHash)
  4592  				pendingTx.BlockNumber = 0
  4593  				pendingTx.Timestamp = 0
  4594  				updated = true
  4595  			}
  4596  		} else {
  4597  			w.log.Errorf("Error getting confirmations for pending tx %s: %v", pendingTx.txHash, err)
  4598  		}
  4599  		return
  4600  	}
  4601  
  4602  	pendingTx.Receipt = receipt
  4603  	pendingTx.indexed = true
  4604  	pendingTx.Rejected = receipt.Status != types.ReceiptStatusSuccessful
  4605  	updated = true
  4606  
  4607  	if receipt.BlockNumber == nil || receipt.BlockNumber.Cmp(new(big.Int)) == 0 {
  4608  		if pendingTx.BlockNumber > 0 {
  4609  			w.log.Warnf("Transaction %s was previously mined but is now unconfirmed", pendingTx.txHash)
  4610  			pendingTx.Timestamp = 0
  4611  			pendingTx.BlockNumber = 0
  4612  		}
  4613  		return
  4614  	}
  4615  	effectiveGasPrice := receipt.EffectiveGasPrice
  4616  	// NOTE: Would be nice if the receipt contained the block time so we could
  4617  	// set the timestamp without having to fetch the header. We could use
  4618  	// SubmissionTime, I guess. Less accurate, but probably not by much.
  4619  	// NOTE: I don't really know why effectiveGasPrice would be nil, but the
  4620  	// code I'm replacing got it from the header, so I'm gonna add this check
  4621  	// just in case.
  4622  	if pendingTx.Timestamp == 0 || effectiveGasPrice == nil {
  4623  		hdr, err := w.node.headerByHash(w.ctx, receipt.BlockHash)
  4624  		if err != nil {
  4625  			w.log.Errorf("Error getting header for hash %v: %v", receipt.BlockHash, err)
  4626  			return
  4627  		}
  4628  		if hdr == nil {
  4629  			w.log.Errorf("Header for hash %v is nil", receipt.BlockHash)
  4630  			return
  4631  		}
  4632  		pendingTx.Timestamp = hdr.Time
  4633  		if effectiveGasPrice == nil {
  4634  			effectiveGasPrice = new(big.Int).Add(hdr.BaseFee, tx.EffectiveGasTipValue(hdr.BaseFee))
  4635  		}
  4636  	}
  4637  
  4638  	bigFees := new(big.Int).Mul(effectiveGasPrice, big.NewInt(int64(receipt.GasUsed)))
  4639  	pendingTx.Fees = dexeth.WeiToGweiCeil(bigFees)
  4640  	pendingTx.BlockNumber = receipt.BlockNumber.Uint64()
  4641  	pendingTx.Confirmed = safeConfs(tip, pendingTx.BlockNumber) >= w.finalizeConfs
  4642  }
  4643  
  4644  // checkPendingTxs checks the confirmation status of all pending transactions.
  4645  func (w *baseWallet) checkPendingTxs() {
  4646  	tip := w.tipHeight()
  4647  
  4648  	w.nonceMtx.Lock()
  4649  	defer w.nonceMtx.Unlock()
  4650  
  4651  	// If we have pending txs, trace log the before and after.
  4652  	if w.log.Level() == dex.LevelTrace {
  4653  		if nPending := len(w.pendingTxs); nPending > 0 {
  4654  			defer func() {
  4655  				w.log.Tracef("Checked %d pending txs. Finalized %d", nPending, nPending-len(w.pendingTxs))
  4656  			}()
  4657  		}
  4658  
  4659  	}
  4660  
  4661  	// keepFromIndex will be the index of the first un-finalized tx.
  4662  	// lastConfirmed, will be the index of the last confirmed tx. All txs with
  4663  	// nonces lower that lastConfirmed should also be confirmed, or else
  4664  	// something isn't right and we may need to request user input.
  4665  	var keepFromIndex int
  4666  	var lastConfirmed int = -1
  4667  	for i, pendingTx := range w.pendingTxs {
  4668  		if w.ctx.Err() != nil {
  4669  			return
  4670  		}
  4671  		w.updatePendingTx(tip, pendingTx)
  4672  		if pendingTx.Confirmed {
  4673  			lastConfirmed = i
  4674  			if pendingTx.Nonce.Cmp(w.confirmedNonceAt) == 0 {
  4675  				w.confirmedNonceAt.Add(w.confirmedNonceAt, big.NewInt(1))
  4676  			}
  4677  			if pendingTx.savedToDB {
  4678  				if i == keepFromIndex {
  4679  					// This is what we expect. No tx should be confirmed before a
  4680  					// tx with a lower nonce. We'll delete at least up to this
  4681  					// one.
  4682  					keepFromIndex = i + 1
  4683  				}
  4684  			}
  4685  			// This transaction is finalized. If we had previously sought action
  4686  			// on it, cancel that request.
  4687  			if pendingTx.actionRequested {
  4688  				pendingTx.actionRequested = false
  4689  				w.resolveAction(pendingTx.ID, pendingTx.TokenID)
  4690  			}
  4691  		}
  4692  	}
  4693  
  4694  	// If we have missing nonces, send an alert.
  4695  	if !w.recoveryRequestSent && len(findMissingNonces(w.confirmedNonceAt, w.pendingNonceAt, w.pendingTxs)) != 0 {
  4696  		w.recoveryRequestSent = true
  4697  		w.requestAction(actionTypeMissingNonces, w.missingNoncesActionID(), nil, nil)
  4698  	}
  4699  
  4700  	// Loop again, classifying problems and sending action requests.
  4701  	for i, pendingTx := range w.pendingTxs {
  4702  		if pendingTx.Confirmed || pendingTx.BlockNumber > 0 {
  4703  			continue
  4704  		}
  4705  		if time.Since(pendingTx.actionIgnored) < txAgeOut {
  4706  			// They asked us to keep waiting.
  4707  			continue
  4708  		}
  4709  		age := pendingTx.age()
  4710  		// i < lastConfirmed means unconfirmed nonce < a confirmed nonce.
  4711  		if (i < lastConfirmed) ||
  4712  			w.confirmedNonceAt.Cmp(pendingTx.Nonce) > 0 ||
  4713  			(age >= txAgeOut && pendingTx.Receipt == nil && !pendingTx.indexed) {
  4714  
  4715  			// The tx in our records wasn't accepted. Where's the right one?
  4716  			req := newLostNonceNote(*pendingTx.WalletTransaction, pendingTx.Nonce.Uint64())
  4717  			pendingTx.actionRequested = true
  4718  			w.requestAction(actionTypeLostNonce, pendingTx.ID, req, pendingTx.TokenID)
  4719  			continue
  4720  		}
  4721  		// Recheck fees periodically.
  4722  		const feeCheckInterval = time.Minute * 5
  4723  		if time.Since(pendingTx.lastFeeCheck) < feeCheckInterval {
  4724  			continue
  4725  		}
  4726  		pendingTx.lastFeeCheck = time.Now()
  4727  		tx, err := pendingTx.tx()
  4728  		if err != nil {
  4729  			w.log.Errorf("Error decoding raw tx %s for fee check: %v", pendingTx.ID, err)
  4730  			continue
  4731  		}
  4732  		txCap := tx.GasFeeCap()
  4733  		baseRate, tipRate, err := w.currentNetworkFees(w.ctx)
  4734  		if err != nil {
  4735  			w.log.Errorf("Error getting base fee: %w", err)
  4736  			continue
  4737  		}
  4738  		if txCap.Cmp(baseRate) < 0 {
  4739  			maxFees := new(big.Int).Add(tipRate, new(big.Int).Mul(baseRate, big.NewInt(2)))
  4740  			maxFees.Mul(maxFees, new(big.Int).SetUint64(tx.Gas()))
  4741  			req := newLowFeeNote(*pendingTx.WalletTransaction, dexeth.WeiToGweiCeil(maxFees))
  4742  			pendingTx.actionRequested = true
  4743  			w.requestAction(actionTypeTooCheap, pendingTx.ID, req, pendingTx.TokenID)
  4744  			continue
  4745  		}
  4746  		// Fees look good and there's no reason to believe this tx will
  4747  		// not be mined. Do we do anything?
  4748  		// actionRequired(actionTypeStuckTx, pendingTx)
  4749  	}
  4750  
  4751  	// Delete finalized txs from local tracking.
  4752  	w.pendingTxs = w.pendingTxs[keepFromIndex:]
  4753  
  4754  	// Re-broadcast any txs that are not indexed and haven't been re-broadcast
  4755  	// in a while, logging any errors as warnings.
  4756  	const rebroadcastPeriod = time.Minute * 5
  4757  	for _, pendingTx := range w.pendingTxs {
  4758  		if pendingTx.Confirmed || pendingTx.BlockNumber > 0 ||
  4759  			pendingTx.actionRequested || // Waiting on action
  4760  			pendingTx.indexed || // Provider knows about it
  4761  			time.Since(pendingTx.lastBroadcast) < rebroadcastPeriod {
  4762  
  4763  			continue
  4764  		}
  4765  		pendingTx.lastBroadcast = time.Now()
  4766  		tx, err := pendingTx.tx()
  4767  		if err != nil {
  4768  			w.log.Errorf("Error decoding raw tx %s for rebroadcast: %v", pendingTx.ID, err)
  4769  			continue
  4770  		}
  4771  		if err := w.node.sendSignedTransaction(w.ctx, tx, allowAlreadyKnownFilter); err != nil {
  4772  			w.log.Warnf("Error rebroadcasting tx %s: %v", pendingTx.ID, err)
  4773  		} else {
  4774  			w.log.Infof("Rebroadcasted un-indexed transaction %s", pendingTx.ID)
  4775  		}
  4776  	}
  4777  }
  4778  
  4779  // Required Actions: Extraordinary conditions that might require user input.
  4780  
  4781  var _ asset.ActionTaker = (*assetWallet)(nil)
  4782  
  4783  const (
  4784  	actionTypeMissingNonces = "missingNonces"
  4785  	actionTypeLostNonce     = "lostNonce"
  4786  	actionTypeTooCheap      = "tooCheap"
  4787  )
  4788  
  4789  // TransactionActionNote is used to request user action on transactions in
  4790  // abnormal states.
  4791  type TransactionActionNote struct {
  4792  	Tx      *asset.WalletTransaction `json:"tx"`
  4793  	Nonce   uint64                   `json:"nonce,omitempty"`
  4794  	NewFees uint64                   `json:"newFees,omitempty"`
  4795  }
  4796  
  4797  // newLostNonceNote is information regarding a tx that appears to be lost.
  4798  func newLostNonceNote(tx asset.WalletTransaction, nonce uint64) *TransactionActionNote {
  4799  	return &TransactionActionNote{
  4800  		Tx:    &tx,
  4801  		Nonce: nonce,
  4802  	}
  4803  }
  4804  
  4805  // newLowFeeNote is data about a tx that is stuck in mempool with too-low fees.
  4806  func newLowFeeNote(tx asset.WalletTransaction, newFees uint64) *TransactionActionNote {
  4807  	return &TransactionActionNote{
  4808  		Tx:      &tx,
  4809  		NewFees: newFees,
  4810  	}
  4811  }
  4812  
  4813  // parse the pending tx and index from the slice.
  4814  func pendingTxWithID(txID string, pendingTxs []*extendedWalletTx) (int, *extendedWalletTx) {
  4815  	for i, pendingTx := range pendingTxs {
  4816  		if pendingTx.ID == txID {
  4817  			return i, pendingTx
  4818  		}
  4819  	}
  4820  	return 0, nil
  4821  }
  4822  
  4823  // amendPendingTx is called with a function that intends to modify a pendingTx
  4824  // under mutex lock.
  4825  func (w *assetWallet) amendPendingTx(txID string, f func(common.Hash, *types.Transaction, *extendedWalletTx, int) error) error {
  4826  	txHash := common.HexToHash(txID)
  4827  	if txHash == (common.Hash{}) {
  4828  		return fmt.Errorf("invalid tx ID %q", txID)
  4829  	}
  4830  	w.nonceMtx.Lock()
  4831  	defer w.nonceMtx.Unlock()
  4832  	idx, pendingTx := pendingTxWithID(txID, w.pendingTxs)
  4833  	if pendingTx == nil {
  4834  		// Nothing to do anymore.
  4835  		return nil
  4836  	}
  4837  	tx, err := pendingTx.tx()
  4838  	if err != nil {
  4839  		return fmt.Errorf("error decoding transaction: %w", err)
  4840  	}
  4841  	if err := f(txHash, tx, pendingTx, idx); err != nil {
  4842  		return err
  4843  	}
  4844  	w.emit.ActionResolved(txID)
  4845  	pendingTx.actionRequested = false
  4846  	return nil
  4847  }
  4848  
  4849  // userActionBumpFees is a request by a user to resolve a actionTypeTooCheap
  4850  // condition.
  4851  func (w *assetWallet) userActionBumpFees(actionB []byte) error {
  4852  	var action struct {
  4853  		TxID string `json:"txID"`
  4854  		Bump *bool  `json:"bump"`
  4855  	}
  4856  	if err := json.Unmarshal(actionB, &action); err != nil {
  4857  		return fmt.Errorf("error unmarshaling bump action: %v", err)
  4858  	}
  4859  	if action.Bump == nil {
  4860  		return errors.New("no bump value specified")
  4861  	}
  4862  	return w.amendPendingTx(action.TxID, func(txHash common.Hash, tx *types.Transaction, pendingTx *extendedWalletTx, idx int) error {
  4863  		if !*action.Bump {
  4864  			pendingTx.actionIgnored = time.Now()
  4865  			return nil
  4866  		}
  4867  
  4868  		nonce := new(big.Int).SetUint64(tx.Nonce())
  4869  		maxFeeRate, tipCap, err := w.recommendedMaxFeeRate(w.ctx)
  4870  		if err != nil {
  4871  			return fmt.Errorf("error getting new fee rate: %w", err)
  4872  		}
  4873  		txOpts, err := w.node.txOpts(w.ctx, 0 /* set below */, tx.Gas(), maxFeeRate, tipCap, nonce)
  4874  		if err != nil {
  4875  			return fmt.Errorf("error preparing tx opts: %w", err)
  4876  		}
  4877  		txOpts.Value = tx.Value()
  4878  		addr := tx.To()
  4879  		if addr == nil {
  4880  			return errors.New("pending tx has no recipient?")
  4881  		}
  4882  
  4883  		newTx, err := w.node.sendTransaction(w.ctx, txOpts, *addr, tx.Data())
  4884  		if err != nil {
  4885  			return fmt.Errorf("error sending bumped-fee transaction: %w", err)
  4886  		}
  4887  
  4888  		newPendingTx := w.extendedTx(newTx, pendingTx.Type, pendingTx.Amount, pendingTx.Recipient)
  4889  
  4890  		pendingTx.NonceReplacement = newPendingTx.ID
  4891  		pendingTx.FeeReplacement = true
  4892  
  4893  		w.tryStoreDBTx(pendingTx)
  4894  		w.tryStoreDBTx(newPendingTx)
  4895  
  4896  		w.pendingTxs[idx] = newPendingTx
  4897  		return nil
  4898  	})
  4899  }
  4900  
  4901  // tryStoreDBTx attempts to store the DB tx and logs errors internally. This
  4902  // method sets the savedToDB flag, so if this tx is in pendingTxs, the nonceMtx
  4903  // must be held for reading.
  4904  func (w *baseWallet) tryStoreDBTx(wt *extendedWalletTx) {
  4905  	err := w.txDB.storeTx(wt)
  4906  	if err != nil {
  4907  		w.log.Errorf("Error storing tx %s to DB: %v", wt.txHash, err)
  4908  	}
  4909  	wt.savedToDB = err == nil
  4910  }
  4911  
  4912  // userActionNonceReplacement is a request by a user to resolve a
  4913  // actionTypeLostNonce condition.
  4914  func (w *assetWallet) userActionNonceReplacement(actionB []byte) error {
  4915  	var action struct {
  4916  		TxID          string `json:"txID"`
  4917  		Abandon       *bool  `json:"abandon"`
  4918  		ReplacementID string `json:"replacementID"`
  4919  	}
  4920  	if err := json.Unmarshal(actionB, &action); err != nil {
  4921  		return fmt.Errorf("error unmarshaling user action: %v", err)
  4922  	}
  4923  	if action.Abandon == nil {
  4924  		return fmt.Errorf("no abandon value provided for user action for tx %s", action.TxID)
  4925  	}
  4926  	abandon := *action.Abandon
  4927  	if !abandon && action.ReplacementID == "" { // keep waiting
  4928  		return w.amendPendingTx(action.TxID, func(_ common.Hash, _ *types.Transaction, pendingTx *extendedWalletTx, idx int) error {
  4929  			pendingTx.actionIgnored = time.Now()
  4930  			return nil
  4931  		})
  4932  	}
  4933  	if abandon { // abandon
  4934  		return w.amendPendingTx(action.TxID, func(txHash common.Hash, _ *types.Transaction, wt *extendedWalletTx, idx int) error {
  4935  			w.log.Infof("Abandoning transaction %s via user action", txHash)
  4936  			wt.AssumedLost = true
  4937  			w.tryStoreDBTx(wt)
  4938  			copy(w.pendingTxs[idx:], w.pendingTxs[idx+1:])
  4939  			w.pendingTxs = w.pendingTxs[:len(w.pendingTxs)-1]
  4940  			return nil
  4941  		})
  4942  	}
  4943  
  4944  	replacementHash := common.HexToHash(action.ReplacementID)
  4945  	replacementTx, _, err := w.node.getTransaction(w.ctx, replacementHash)
  4946  	if err != nil {
  4947  		return fmt.Errorf("error fetching nonce replacement tx: %v", err)
  4948  	}
  4949  
  4950  	from, err := types.LatestSigner(w.node.chainConfig()).Sender(replacementTx)
  4951  	if err != nil {
  4952  		return fmt.Errorf("error parsing originator address from specified replacement tx %s: %w", from, err)
  4953  	}
  4954  	if from != w.addr {
  4955  		return fmt.Errorf("specified replacement tx originator %s is not you", from)
  4956  	}
  4957  	return w.amendPendingTx(action.TxID, func(txHash common.Hash, oldTx *types.Transaction, pendingTx *extendedWalletTx, idx int) error {
  4958  		if replacementTx.Nonce() != pendingTx.Nonce.Uint64() {
  4959  			return fmt.Errorf("nonce replacement doesn't have the right nonce. %d != %s", replacementTx.Nonce(), pendingTx.Nonce)
  4960  		}
  4961  		recipient := w.addr.Hex()
  4962  		newPendingTx := w.extendedTx(replacementTx, asset.Unknown, 0, &recipient)
  4963  		pendingTx.NonceReplacement = newPendingTx.ID
  4964  		var oldTo, newTo common.Address
  4965  		if oldAddr := oldTx.To(); oldAddr != nil {
  4966  			oldTo = *oldAddr
  4967  		}
  4968  		if newAddr := replacementTx.To(); newAddr != nil {
  4969  			newTo = *newAddr
  4970  		}
  4971  		if bytes.Equal(oldTx.Data(), replacementTx.Data()) && oldTo == newTo {
  4972  			pendingTx.FeeReplacement = true
  4973  		}
  4974  		w.tryStoreDBTx(pendingTx)
  4975  		w.tryStoreDBTx(newPendingTx)
  4976  		w.pendingTxs[idx] = newPendingTx
  4977  		return nil
  4978  	})
  4979  }
  4980  
  4981  // userActionRecoverNonces, if recover is true, examines our confirmed and
  4982  // pending nonces and our pendingTx set and sends zero-value txs to ourselves
  4983  // to fill any gaps or replace any rogue transactions. This should never happen
  4984  // if we're only running one instance of this wallet.
  4985  func (w *assetWallet) userActionRecoverNonces(actionB []byte) error {
  4986  	var action struct {
  4987  		Recover *bool `json:"recover"`
  4988  	}
  4989  	if err := json.Unmarshal(actionB, &action); err != nil {
  4990  		return fmt.Errorf("error unmarshaling user action: %v", err)
  4991  	}
  4992  	if action.Recover == nil {
  4993  		return errors.New("no recover value specified")
  4994  	}
  4995  	if !*action.Recover {
  4996  		// Don't reset recoveryRequestSent. They won't see this message again until
  4997  		// they reboot.
  4998  		return nil
  4999  	}
  5000  	maxFeeRate, tipRate, err := w.recommendedMaxFeeRate(w.ctx)
  5001  	if err != nil {
  5002  		return fmt.Errorf("error getting max fee rate for nonce resolution: %v", err)
  5003  	}
  5004  	w.nonceMtx.Lock()
  5005  	defer w.nonceMtx.Unlock()
  5006  	missingNonces := findMissingNonces(w.confirmedNonceAt, w.pendingNonceAt, w.pendingTxs)
  5007  	if len(missingNonces) == 0 {
  5008  		return nil
  5009  	}
  5010  	for i, n := range missingNonces {
  5011  		nonce := new(big.Int).SetUint64(n)
  5012  		txOpts, err := w.node.txOpts(w.ctx, 0, defaultSendGasLimit, maxFeeRate, tipRate, nonce)
  5013  		if err != nil {
  5014  			return fmt.Errorf("error getting tx opts for nonce resolution: %v", err)
  5015  		}
  5016  		var skip bool
  5017  		tx, err := w.node.sendTransaction(w.ctx, txOpts, w.addr, nil, func(err error) (discard, propagate, fail bool) {
  5018  			if errorFilter(err, "replacement transaction underpriced") {
  5019  				skip = true
  5020  				return true, false, false
  5021  			}
  5022  			return false, false, true
  5023  		})
  5024  		if err != nil {
  5025  			return fmt.Errorf("error sending tx %d for nonce resolution: %v", nonce, err)
  5026  		}
  5027  		if skip {
  5028  			w.log.Warnf("skipping storing underpriced replacement tx for nonce %d", nonce)
  5029  		} else {
  5030  			recipient := w.addr.Hex()
  5031  			pendingTx := w.extendAndStoreTx(tx, asset.SelfSend, 0, nil, &recipient)
  5032  			w.emitTransactionNote(pendingTx.WalletTransaction, true)
  5033  			w.pendingTxs = append(w.pendingTxs, pendingTx)
  5034  			sort.Slice(w.pendingTxs, func(i, j int) bool {
  5035  				return w.pendingTxs[i].Nonce.Cmp(w.pendingTxs[j].Nonce) < 0
  5036  			})
  5037  		}
  5038  		if i < len(missingNonces)-1 {
  5039  			select {
  5040  			case <-time.After(time.Second * 1):
  5041  			case <-w.ctx.Done():
  5042  				return nil
  5043  			}
  5044  		}
  5045  	}
  5046  	w.emit.ActionResolved(w.missingNoncesActionID())
  5047  	return nil
  5048  }
  5049  
  5050  // requestAction sends a ActionRequired notification up the chain of command.
  5051  // nonceMtx must be locked.
  5052  func (w *baseWallet) requestAction(actionID, uniqueID string, req *TransactionActionNote, tokenID *uint32) {
  5053  	assetID := w.baseChainID
  5054  	if tokenID != nil {
  5055  		assetID = *tokenID
  5056  	}
  5057  	aw := w.wallet(assetID)
  5058  	if aw == nil { // sanity
  5059  		return
  5060  	}
  5061  	aw.emit.ActionRequired(uniqueID, actionID, req)
  5062  }
  5063  
  5064  // resolveAction sends a ActionResolved notification up the chain of command.
  5065  // nonceMtx must be locked.
  5066  func (w *baseWallet) resolveAction(uniqueID string, tokenID *uint32) {
  5067  	assetID := w.baseChainID
  5068  	if tokenID != nil {
  5069  		assetID = *tokenID
  5070  	}
  5071  	aw := w.wallet(assetID)
  5072  	if aw == nil { // sanity
  5073  		return
  5074  	}
  5075  	aw.emit.ActionResolved(uniqueID)
  5076  }
  5077  
  5078  // TakeAction satisfies asset.ActionTaker. This handles responses from the
  5079  // user for an ActionRequired request, usually for a stuck tx or otherwise
  5080  // abnormal condition.
  5081  func (w *assetWallet) TakeAction(actionID string, actionB []byte) error {
  5082  	switch actionID {
  5083  	case actionTypeTooCheap:
  5084  		return w.userActionBumpFees(actionB)
  5085  	case actionTypeMissingNonces:
  5086  		return w.userActionRecoverNonces(actionB)
  5087  	case actionTypeLostNonce:
  5088  		return w.userActionNonceReplacement(actionB)
  5089  	default:
  5090  		return fmt.Errorf("unknown action %q", actionID)
  5091  	}
  5092  }
  5093  
  5094  const txHistoryNonceKey = "Nonce"
  5095  
  5096  // transactionFeeLimit calculates the maximum tx fees that are allowed by a tx.
  5097  func transactionFeeLimit(tx *types.Transaction) *big.Int {
  5098  	fees := new(big.Int)
  5099  	feeCap, tipCap := tx.GasFeeCap(), tx.GasTipCap()
  5100  	if feeCap != nil && tipCap != nil {
  5101  		fees.Add(fees, feeCap)
  5102  		fees.Add(fees, tipCap)
  5103  		fees.Mul(fees, new(big.Int).SetUint64(tx.Gas()))
  5104  	}
  5105  	return fees
  5106  }
  5107  
  5108  // extendedTx generates an *extendedWalletTx for a newly-broadcasted tx and
  5109  // stores it in the DB.
  5110  func (w *assetWallet) extendedTx(tx *types.Transaction, txType asset.TransactionType, amt uint64, recipient *string) *extendedWalletTx {
  5111  	var tokenAssetID *uint32
  5112  	if w.assetID != w.baseChainID {
  5113  		tokenAssetID = &w.assetID
  5114  	}
  5115  	return w.baseWallet.extendAndStoreTx(tx, txType, amt, tokenAssetID, recipient)
  5116  }
  5117  
  5118  func (w *baseWallet) extendAndStoreTx(tx *types.Transaction, txType asset.TransactionType, amt uint64, tokenAssetID *uint32, recipient *string) *extendedWalletTx {
  5119  	nonce := tx.Nonce()
  5120  	rawTx, err := tx.MarshalBinary()
  5121  	if err != nil {
  5122  		w.log.Errorf("Error marshaling tx %q: %v", tx.Hash(), err)
  5123  	}
  5124  
  5125  	if recipient == nil {
  5126  		if to := tx.To(); to != nil {
  5127  			s := to.String()
  5128  			recipient = &s
  5129  		}
  5130  	}
  5131  
  5132  	now := time.Now()
  5133  
  5134  	wt := &extendedWalletTx{
  5135  		WalletTransaction: &asset.WalletTransaction{
  5136  			Type:      txType,
  5137  			ID:        tx.Hash().String(),
  5138  			Amount:    amt,
  5139  			Fees:      dexeth.WeiToGweiCeil(transactionFeeLimit(tx)), // updated later
  5140  			TokenID:   tokenAssetID,
  5141  			Recipient: recipient,
  5142  			AdditionalData: map[string]string{
  5143  				txHistoryNonceKey: strconv.FormatUint(nonce, 10),
  5144  			},
  5145  		},
  5146  		SubmissionTime: uint64(now.Unix()),
  5147  		RawTx:          rawTx,
  5148  		Nonce:          big.NewInt(int64(nonce)),
  5149  		txHash:         tx.Hash(),
  5150  		savedToDB:      true,
  5151  		lastBroadcast:  now,
  5152  		lastFeeCheck:   now,
  5153  	}
  5154  
  5155  	w.tryStoreDBTx(wt)
  5156  
  5157  	return wt
  5158  }
  5159  
  5160  // TxHistory returns all the transactions the wallet has made. This
  5161  // includes the ETH wallet and all token wallets. If refID is nil, then
  5162  // transactions starting from the most recent are returned (past is ignored).
  5163  // If past is true, the transactions prior to the refID are returned, otherwise
  5164  // the transactions after the refID are returned. n is the number of
  5165  // transactions to return. If n is <= 0, all the transactions will be returned.
  5166  func (w *ETHWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) {
  5167  	return w.txHistory(n, refID, past, nil)
  5168  }
  5169  
  5170  // TxHistory returns all the transactions the token wallet has made. If refID
  5171  // is nil, then transactions starting from the most recent are returned (past
  5172  // is ignored). If past is true, the transactions prior to the refID are
  5173  // returned, otherwise the transactions after the refID are returned. n is the
  5174  // number of transactions to return. If n is <= 0, all the transactions will be
  5175  // returned.
  5176  func (w *TokenWallet) TxHistory(n int, refID *string, past bool) ([]*asset.WalletTransaction, error) {
  5177  	return w.txHistory(n, refID, past, &w.assetID)
  5178  }
  5179  
  5180  func (w *baseWallet) txHistory(n int, refID *string, past bool, assetID *uint32) ([]*asset.WalletTransaction, error) {
  5181  	var hashID *common.Hash
  5182  	if refID != nil {
  5183  		h := common.HexToHash(*refID)
  5184  		if h == (common.Hash{}) {
  5185  			return nil, fmt.Errorf("invalid reference ID %q provided", *refID)
  5186  		}
  5187  		hashID = &h
  5188  	}
  5189  	return w.txDB.getTxs(n, hashID, past, assetID)
  5190  }
  5191  
  5192  func (w *ETHWallet) getReceivingTransaction(ctx context.Context, txHash common.Hash) (*asset.WalletTransaction, error) {
  5193  	tx, blockHeight, err := w.node.getTransaction(ctx, txHash)
  5194  	if err != nil {
  5195  		return nil, err
  5196  	}
  5197  
  5198  	if *tx.To() != w.addr {
  5199  		return nil, asset.CoinNotFoundError
  5200  	}
  5201  
  5202  	addr := w.addr.String()
  5203  	return &asset.WalletTransaction{
  5204  		Type:      asset.Receive,
  5205  		ID:        tx.Hash().String(),
  5206  		Amount:    w.atomize(tx.Value()),
  5207  		Recipient: &addr,
  5208  		AdditionalData: map[string]string{
  5209  			txHistoryNonceKey: strconv.FormatUint(tx.Nonce(), 10),
  5210  		},
  5211  		// For receiving transactions, if the transaction is mined, it is
  5212  		// confirmed confirmed, because the value received will be part of
  5213  		// the available balance.
  5214  		Confirmed: blockHeight > 0,
  5215  	}, nil
  5216  }
  5217  
  5218  // WalletTransaction returns a transaction that either the wallet has made or
  5219  // one in which the wallet has received funds.
  5220  func (w *ETHWallet) WalletTransaction(ctx context.Context, txID string) (*asset.WalletTransaction, error) {
  5221  	txHash := common.HexToHash(txID)
  5222  	var localTx asset.WalletTransaction
  5223  	if w.withLocalTxRead(txHash, func(wt *extendedWalletTx) {
  5224  		localTx = *wt.WalletTransaction
  5225  	}) {
  5226  		// Do not modify tx in pending tx map. Only modify copy.
  5227  		// Confirmed should be true, because the value is part of the available
  5228  		// balance.
  5229  		if localTx.BlockNumber > 0 {
  5230  			localTx.Confirmed = true
  5231  		}
  5232  		return &localTx, nil
  5233  	}
  5234  	return w.getReceivingTransaction(ctx, txHash)
  5235  }
  5236  
  5237  // extractValueFromTransferLog checks the Transfer event logs in the
  5238  // transaction, finds the log that sends tokens to the wallet's address,
  5239  // and returns the value of the transfer.
  5240  func (w *TokenWallet) extractValueFromTransferLog(receipt *types.Receipt) (v uint64, err error) {
  5241  	return v, w.withTokenContractor(w.assetID, contractVersionNewest, func(c tokenContractor) error {
  5242  		v, err = c.parseTransfer(receipt)
  5243  		return err
  5244  	})
  5245  }
  5246  
  5247  func (w *TokenWallet) getReceivingTransaction(ctx context.Context, txHash common.Hash) (*asset.WalletTransaction, error) {
  5248  	receipt, _, err := w.node.transactionAndReceipt(ctx, txHash)
  5249  	if err != nil {
  5250  		return nil, err
  5251  	}
  5252  
  5253  	blockHeight := receipt.BlockNumber.Int64()
  5254  	value, err := w.extractValueFromTransferLog(receipt)
  5255  	if err != nil {
  5256  		w.log.Errorf("Error extracting value from transfer log: %v", err)
  5257  		return &asset.WalletTransaction{
  5258  			Type:        asset.Unknown,
  5259  			ID:          txHash.String(),
  5260  			BlockNumber: uint64(blockHeight),
  5261  			Confirmed:   blockHeight > 0,
  5262  		}, nil
  5263  	}
  5264  
  5265  	addr := w.addr.String()
  5266  	return &asset.WalletTransaction{
  5267  		Type:        asset.Receive,
  5268  		ID:          txHash.String(),
  5269  		BlockNumber: uint64(blockHeight),
  5270  		TokenID:     &w.assetID,
  5271  		Amount:      value,
  5272  		Recipient:   &addr,
  5273  		// For receiving transactions, if the transaction is mined, it is
  5274  		// confirmed confirmed, because the value received will be part of
  5275  		// the available balance.
  5276  		Confirmed: blockHeight > 0,
  5277  	}, nil
  5278  }
  5279  
  5280  func (w *TokenWallet) WalletTransaction(ctx context.Context, txID string) (*asset.WalletTransaction, error) {
  5281  	txHash := common.HexToHash(txID)
  5282  	var localTx asset.WalletTransaction
  5283  	if w.withLocalTxRead(txHash, func(wt *extendedWalletTx) {
  5284  		localTx = *wt.WalletTransaction
  5285  	}) {
  5286  		// Do not modify tx in pending tx map. Only modify copy.
  5287  		// Confirmed should be true, because the value is part of the available
  5288  		// balance.
  5289  		if localTx.BlockNumber > 0 {
  5290  			localTx.Confirmed = true
  5291  		}
  5292  		return &localTx, nil
  5293  	}
  5294  	return w.getReceivingTransaction(ctx, txHash)
  5295  }
  5296  
  5297  // providersFile reads a file located at ~/dextest/credentials.json.
  5298  // The file contains seed and provider information for wallets used for
  5299  // getgas, deploy, and nodeclient testing. If simnet providers are not
  5300  // specified, getFileCredentials will add the simnet alpha node.
  5301  type providersFile struct {
  5302  	Seed      dex.Bytes                                                   `json:"seed"`
  5303  	Providers map[string] /* symbol */ map[string] /* network */ []string `json:"providers"`
  5304  }
  5305  
  5306  // getFileCredentials reads the file at path and extracts the seed and the
  5307  // provider for the network.
  5308  func getFileCredentials(chain, path string, net dex.Network) (seed []byte, providers []string, err error) {
  5309  	b, err := os.ReadFile(path)
  5310  	if err != nil {
  5311  		return nil, nil, fmt.Errorf("error reading credentials file: %v", err)
  5312  	}
  5313  	var p providersFile
  5314  	if err := json.Unmarshal(b, &p); err != nil {
  5315  		return nil, nil, fmt.Errorf("error parsing credentials file: %v", err)
  5316  	}
  5317  	if len(p.Seed) == 0 {
  5318  		return nil, nil, fmt.Errorf("must provide both seeds in credentials file")
  5319  	}
  5320  	seed = p.Seed
  5321  	for _, uri := range p.Providers[chain][net.String()] {
  5322  		if !strings.HasPrefix(uri, "#") && !strings.HasPrefix(uri, ";") {
  5323  			providers = append(providers, uri)
  5324  		}
  5325  	}
  5326  	if len(providers) == 0 {
  5327  		return nil, nil, fmt.Errorf("no providers in the file at %s for chain %s, network %s", path, chain, net)
  5328  	}
  5329  	if net == dex.Simnet && len(providers) == 0 {
  5330  		u, _ := user.Current()
  5331  		switch chain {
  5332  		case "polygon":
  5333  			providers = []string{filepath.Join(u.HomeDir, "dextest", chain, "alpha", "bor", "bor.ipc")}
  5334  		default:
  5335  			providers = []string{filepath.Join(u.HomeDir, "dextest", chain, "alpha", "node", "geth.ipc")}
  5336  		}
  5337  	}
  5338  	return
  5339  }
  5340  
  5341  // quickNode constructs a multiRPCClient and a contractor for the specified
  5342  // asset. The client is connected and unlocked.
  5343  func quickNode(ctx context.Context, walletDir string, contractVer uint32,
  5344  	seed []byte, providers []string, wParams *GetGasWalletParams, net dex.Network, log dex.Logger) (*multiRPCClient, contractor, error) {
  5345  
  5346  	pw := []byte("abc")
  5347  	chainID := wParams.ChainCfg.ChainID.Int64()
  5348  
  5349  	if err := CreateEVMWallet(chainID, &asset.CreateWalletParams{
  5350  		Type:     walletTypeRPC,
  5351  		Seed:     seed,
  5352  		Pass:     pw,
  5353  		Settings: map[string]string{providersKey: strings.Join(providers, " ")},
  5354  		DataDir:  walletDir,
  5355  		Net:      net,
  5356  		Logger:   log,
  5357  	}, wParams.Compat, false); err != nil {
  5358  		return nil, nil, fmt.Errorf("error creating initiator wallet: %v", err)
  5359  	}
  5360  
  5361  	cl, err := newMultiRPCClient(walletDir, providers, log, wParams.ChainCfg, 3, net)
  5362  	if err != nil {
  5363  		return nil, nil, fmt.Errorf("error opening initiator rpc client: %v", err)
  5364  	}
  5365  
  5366  	if err = cl.connect(ctx); err != nil {
  5367  		return nil, nil, fmt.Errorf("error connecting: %v", err)
  5368  	}
  5369  
  5370  	success := false
  5371  	defer func() {
  5372  		if !success {
  5373  			cl.shutdown()
  5374  		}
  5375  	}()
  5376  
  5377  	if err = cl.unlock(string(pw)); err != nil {
  5378  		return nil, nil, fmt.Errorf("error unlocking initiator node: %v", err)
  5379  	}
  5380  
  5381  	var c contractor
  5382  	if wParams.Token == nil {
  5383  		ctor := contractorConstructors[contractVer]
  5384  		if ctor == nil {
  5385  			return nil, nil, fmt.Errorf("no contractor constructor for eth contract version %d", contractVer)
  5386  		}
  5387  		c, err = ctor(wParams.ContractAddr, cl.address(), cl.contractBackend())
  5388  		if err != nil {
  5389  			return nil, nil, fmt.Errorf("contractor constructor error: %v", err)
  5390  		}
  5391  	} else {
  5392  		ctor := tokenContractorConstructors[contractVer]
  5393  		if ctor == nil {
  5394  			return nil, nil, fmt.Errorf("no token contractor constructor for eth contract version %d", contractVer)
  5395  		}
  5396  		c, err = ctor(net, wParams.Token, cl.address(), cl.contractBackend())
  5397  		if err != nil {
  5398  			return nil, nil, fmt.Errorf("token contractor constructor error: %v", err)
  5399  		}
  5400  	}
  5401  	success = true
  5402  	return cl, c, nil
  5403  }
  5404  
  5405  // waitForConfirmation waits for the specified transaction to have > 0
  5406  // confirmations.
  5407  func waitForConfirmation(ctx context.Context, desc string, cl ethFetcher, txHash common.Hash, log dex.Logger) error {
  5408  	bestHdr, err := cl.bestHeader(ctx)
  5409  	if err != nil {
  5410  		return fmt.Errorf("error getting best header: %w", err)
  5411  	}
  5412  	ticker := time.NewTicker(stateUpdateTick)
  5413  	lastReport := time.Now()
  5414  	defer ticker.Stop()
  5415  	for {
  5416  		select {
  5417  		case <-ticker.C:
  5418  			hdr, _ := cl.bestHeader(ctx)
  5419  			if hdr != nil && hdr.Number.Cmp(bestHdr.Number) > 0 {
  5420  				bestHdr = hdr
  5421  				confs, err := cl.transactionConfirmations(ctx, txHash)
  5422  				if err != nil {
  5423  					if !errors.Is(err, asset.CoinNotFoundError) {
  5424  						return fmt.Errorf("error getting transaction confirmations: %v", err)
  5425  					}
  5426  					continue
  5427  				}
  5428  				if confs > 0 {
  5429  					return nil
  5430  				}
  5431  				if time.Since(lastReport) > time.Second*30 {
  5432  					log.Infof("Awaiting confirmations for %s tx %s", desc, txHash)
  5433  					lastReport = time.Now()
  5434  				}
  5435  			}
  5436  
  5437  		case <-ctx.Done():
  5438  			return ctx.Err()
  5439  		}
  5440  	}
  5441  }
  5442  
  5443  // runSimnetMiner starts a gouroutine to generate a simnet block every 5 seconds
  5444  // until the ctx is canceled. By default, the eth harness will mine a block
  5445  // every 15s. We want to speed it up a bit for e.g. GetGas testing.
  5446  func runSimnetMiner(ctx context.Context, symbol string, log dex.Logger) {
  5447  	log.Infof("Starting the simnet miner")
  5448  	go func() {
  5449  		tick := time.NewTicker(time.Second * 5)
  5450  		u, err := user.Current()
  5451  		if err != nil {
  5452  			log.Criticalf("cannot run simnet miner because we couldn't get the current user")
  5453  			return
  5454  		}
  5455  		for {
  5456  			select {
  5457  			case <-tick.C:
  5458  				log.Debugf("Mining a simnet block")
  5459  				mine := exec.CommandContext(ctx, "./mine-alpha", "1")
  5460  				mine.Dir = filepath.Join(u.HomeDir, "dextest", symbol, "harness-ctl")
  5461  				b, err := mine.CombinedOutput()
  5462  				if err != nil {
  5463  					log.Errorf("Mining error: %v", err)
  5464  					log.Errorf("Mining error output: %s", string(b))
  5465  				}
  5466  			case <-ctx.Done():
  5467  				return
  5468  			}
  5469  		}
  5470  	}()
  5471  }
  5472  
  5473  type getGas byte
  5474  
  5475  // GetGas provides access to the gas estimation utilities.
  5476  var GetGas getGas
  5477  
  5478  // GetGasWalletParams are the configuration parameters required to estimate
  5479  // swap contract gas usage.
  5480  type GetGasWalletParams struct {
  5481  	ChainCfg     *params.ChainConfig
  5482  	Gas          *dexeth.Gases
  5483  	Token        *dexeth.Token
  5484  	UnitInfo     *dex.UnitInfo
  5485  	BaseUnitInfo *dex.UnitInfo
  5486  	Compat       *CompatibilityData
  5487  	ContractAddr common.Address // Base chain contract addr.
  5488  }
  5489  
  5490  // ReadCredentials reads the credentials for the network from the credentials
  5491  // file.
  5492  func (getGas) ReadCredentials(chain, credentialsPath string, net dex.Network) (addr string, providers []string, err error) {
  5493  	seed, providers, err := getFileCredentials(chain, credentialsPath, net)
  5494  	if err != nil {
  5495  		return "", nil, err
  5496  	}
  5497  	privB, zero, err := privKeyFromSeed(seed)
  5498  	if err != nil {
  5499  		return "", nil, err
  5500  	}
  5501  	defer zero()
  5502  	privateKey, err := crypto.ToECDSA(privB)
  5503  	if err != nil {
  5504  		return "", nil, err
  5505  	}
  5506  
  5507  	addr = crypto.PubkeyToAddress(privateKey.PublicKey).String()
  5508  	return
  5509  }
  5510  
  5511  func getGetGasClientWithEstimatesAndBalances(ctx context.Context, net dex.Network, contractVer uint32, maxSwaps int,
  5512  	walletDir string, providers []string, seed []byte, wParams *GetGasWalletParams, log dex.Logger) (cl *multiRPCClient, c contractor,
  5513  	ethReq, swapReq, feeRate uint64, ethBal, tokenBal *big.Int, err error) {
  5514  
  5515  	cl, c, err = quickNode(ctx, walletDir, contractVer, seed, providers, wParams, net, log)
  5516  	if err != nil {
  5517  		return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error creating initiator wallet: %v", err)
  5518  	}
  5519  
  5520  	var success bool
  5521  	defer func() {
  5522  		if !success {
  5523  			cl.shutdown()
  5524  		}
  5525  	}()
  5526  
  5527  	base, tip, err := cl.currentFees(ctx)
  5528  	if err != nil {
  5529  		return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("Error estimating fee rate: %v", err)
  5530  	}
  5531  
  5532  	ethBal, err = cl.addressBalance(ctx, cl.address())
  5533  	if err != nil {
  5534  		return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error getting eth balance: %v", err)
  5535  	}
  5536  
  5537  	feeRate = dexeth.WeiToGweiCeil(new(big.Int).Add(tip, new(big.Int).Mul(base, big.NewInt(2))))
  5538  
  5539  	// Check that we have a balance for swaps and fees.
  5540  	g := wParams.Gas
  5541  	const swapVal = 1
  5542  	n := uint64(maxSwaps)
  5543  	swapReq = n * (n + 1) / 2 * swapVal // Sum of positive integers up to n
  5544  	m := n - 1                          // for n swaps there will be (0 + 1 + 2 + ... + n - 1) AddSwaps
  5545  	initGas := (g.Swap * n) + g.SwapAdd*(m*(m+1)/2)
  5546  	redeemGas := (g.Redeem * n) + g.RedeemAdd*(m*(m+1)/2)
  5547  
  5548  	fees := (initGas + redeemGas + defaultSendGasLimit) * // fees for participant wallet
  5549  		2 * // fudge factor
  5550  		6 / 5 * // base rate increase accommodation
  5551  		feeRate
  5552  
  5553  	isToken := wParams.Token != nil
  5554  	ethReq = fees + swapReq
  5555  	if isToken {
  5556  		tc := c.(tokenContractor)
  5557  		tokenBal, err = tc.balance(ctx)
  5558  		if err != nil {
  5559  			return nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("error fetching token balance: %v", err)
  5560  		}
  5561  
  5562  		fees += (g.Transfer*2 + g.Approve*2*2 /* two approvals */ + defaultSendGasLimit /* approval client fee funding tx */) *
  5563  			2 * 6 / 5 * feeRate // Adding 20% in case base rate goes up
  5564  		ethReq = fees
  5565  	}
  5566  	success = true
  5567  	return
  5568  }
  5569  
  5570  func (getGas) chainForAssetID(assetID uint32) string {
  5571  	ti := asset.TokenInfo(assetID)
  5572  	if ti == nil {
  5573  		return dex.BipIDSymbol(assetID)
  5574  	}
  5575  	return dex.BipIDSymbol(ti.ParentID)
  5576  }
  5577  
  5578  // EstimateFunding estimates how much funding is needed for estimating gas, and
  5579  // prints helpful messages for the user.
  5580  func (getGas) EstimateFunding(ctx context.Context, net dex.Network, assetID, contractVer uint32,
  5581  	maxSwaps int, credentialsPath string, wParams *GetGasWalletParams, log dex.Logger) error {
  5582  
  5583  	symbol := dex.BipIDSymbol(assetID)
  5584  	log.Infof("Estimating required funding for up to %d swaps of asset %s, contract version %d on %s", maxSwaps, symbol, contractVer, net)
  5585  
  5586  	seed, providers, err := getFileCredentials(GetGas.chainForAssetID(assetID), credentialsPath, net)
  5587  	if err != nil {
  5588  		return err
  5589  	}
  5590  
  5591  	walletDir, err := os.MkdirTemp("", "")
  5592  	if err != nil {
  5593  		return err
  5594  	}
  5595  	defer os.RemoveAll(walletDir)
  5596  
  5597  	cl, _, ethReq, swapReq, _, ethBalBig, tokenBalBig, err := getGetGasClientWithEstimatesAndBalances(ctx, net, contractVer, maxSwaps, walletDir, providers, seed, wParams, log)
  5598  	if err != nil {
  5599  		return fmt.Errorf("%s: getGetGasClientWithEstimatesAndBalances error: %w", symbol, err)
  5600  	}
  5601  	defer cl.shutdown()
  5602  	ethBal := dexeth.WeiToGwei(ethBalBig)
  5603  
  5604  	ui := wParams.UnitInfo
  5605  	assetFmt := ui.ConventionalString
  5606  	bui := wParams.BaseUnitInfo
  5607  	ethFmt := bui.ConventionalString
  5608  
  5609  	log.Info("Address:", cl.address())
  5610  	log.Infof("%s balance: %s", bui.Conventional.Unit, ethFmt(ethBal))
  5611  
  5612  	isToken := wParams.Token != nil
  5613  	tokenBalOK := true
  5614  	if isToken {
  5615  		log.Infof("%s required for fees: %s", bui.Conventional.Unit, ethFmt(ethReq))
  5616  
  5617  		ui, err := asset.UnitInfo(assetID)
  5618  		if err != nil {
  5619  			return fmt.Errorf("error getting unit info for %d: %v", assetID, err)
  5620  		}
  5621  
  5622  		tokenBal := wParams.Token.EVMToAtomic(tokenBalBig)
  5623  		log.Infof("%s balance: %s", ui.Conventional.Unit, assetFmt(tokenBal))
  5624  		log.Infof("%s required for trading: %s", ui.Conventional.Unit, assetFmt(swapReq))
  5625  		if tokenBal < swapReq {
  5626  			tokenBalOK = false
  5627  			log.Infof("❌ Insufficient %[2]s balance. Deposit %[1]s %[2]s before getting a gas estimate",
  5628  				assetFmt(swapReq-tokenBal), ui.Conventional.Unit)
  5629  		}
  5630  
  5631  	} else {
  5632  		log.Infof("%s required: %s (swaps) + %s (fees) = %s",
  5633  			ui.Conventional.Unit, ethFmt(swapReq), ethFmt(ethReq-swapReq), ethFmt(ethReq))
  5634  	}
  5635  
  5636  	if ethBal < ethReq {
  5637  		// Add 10% for fee drift.
  5638  		ethRecommended := ethReq * 11 / 10
  5639  		log.Infof("❌ Insufficient %s balance. Deposit about %s %s before getting a gas estimate",
  5640  			bui.Conventional.Unit, ethFmt(ethRecommended-ethBal), bui.Conventional.Unit)
  5641  	} else if tokenBalOK {
  5642  		log.Infof("👍 You have sufficient funding to run a gas estimate")
  5643  	}
  5644  
  5645  	return nil
  5646  }
  5647  
  5648  // Return returns the estimation wallet's base-chain or token balance to a
  5649  // specified address, if it is more than fees required to send.
  5650  func (getGas) Return(
  5651  	ctx context.Context,
  5652  	assetID uint32,
  5653  	credentialsPath,
  5654  	returnAddr string,
  5655  	wParams *GetGasWalletParams,
  5656  	net dex.Network,
  5657  	log dex.Logger,
  5658  ) error {
  5659  
  5660  	const contractVer = 0 // Doesn't matter
  5661  
  5662  	if !common.IsHexAddress(returnAddr) {
  5663  		return fmt.Errorf("supplied return address %q is not an Ethereum address", returnAddr)
  5664  	}
  5665  
  5666  	seed, providers, err := getFileCredentials(GetGas.chainForAssetID(assetID), credentialsPath, net)
  5667  	if err != nil {
  5668  		return err
  5669  	}
  5670  
  5671  	walletDir, err := os.MkdirTemp("", "")
  5672  	if err != nil {
  5673  		return err
  5674  	}
  5675  	defer os.RemoveAll(walletDir)
  5676  
  5677  	cl, _, err := quickNode(ctx, walletDir, contractVer, seed, providers, wParams, net, log)
  5678  	if err != nil {
  5679  		return fmt.Errorf("error creating initiator wallet: %v", err)
  5680  	}
  5681  	defer cl.shutdown()
  5682  
  5683  	baseRate, tipRate, err := cl.currentFees(ctx)
  5684  	if err != nil {
  5685  		return fmt.Errorf("Error estimating fee rate: %v", err)
  5686  	}
  5687  	maxFeeRate := new(big.Int).Add(tipRate, new(big.Int).Mul(baseRate, big.NewInt(2)))
  5688  
  5689  	return GetGas.returnFunds(ctx, cl, maxFeeRate, tipRate, common.HexToAddress(returnAddr), wParams.Token, wParams.UnitInfo, log, net)
  5690  }
  5691  
  5692  func (getGas) returnFunds(
  5693  	ctx context.Context,
  5694  	cl *multiRPCClient,
  5695  	maxFeeRate *big.Int,
  5696  	tipRate *big.Int,
  5697  	returnAddr common.Address,
  5698  	token *dexeth.Token, // nil for base chain
  5699  	ui *dex.UnitInfo,
  5700  	log dex.Logger,
  5701  	net dex.Network,
  5702  ) error {
  5703  
  5704  	bigEthBal, err := cl.addressBalance(ctx, cl.address())
  5705  	if err != nil {
  5706  		return fmt.Errorf("error getting eth balance: %v", err)
  5707  	}
  5708  	ethBal := dexeth.WeiToGwei(bigEthBal)
  5709  
  5710  	if token != nil {
  5711  		nt, found := token.NetTokens[net]
  5712  		if !found {
  5713  			return fmt.Errorf("no %s token for %s", token.Name, net)
  5714  		}
  5715  		var g dexeth.Gases
  5716  		for _, sc := range nt.SwapContracts {
  5717  			g = sc.Gas
  5718  			break
  5719  		}
  5720  		fees := g.Transfer * dexeth.WeiToGweiCeil(maxFeeRate)
  5721  		if fees > ethBal {
  5722  			return fmt.Errorf("not enough base chain balance (%s) to cover fees (%s)",
  5723  				dexeth.UnitInfo.ConventionalString(ethBal), dexeth.UnitInfo.ConventionalString(fees))
  5724  		}
  5725  
  5726  		tokenContract, err := erc20.NewIERC20(nt.Address, cl.contractBackend())
  5727  		if err != nil {
  5728  			return fmt.Errorf("NewIERC20 error: %v", err)
  5729  		}
  5730  
  5731  		callOpts := &bind.CallOpts{
  5732  			From:    cl.address(),
  5733  			Context: ctx,
  5734  		}
  5735  
  5736  		bigTokenBal, err := tokenContract.BalanceOf(callOpts, cl.address())
  5737  		if err != nil {
  5738  			return fmt.Errorf("error getting token balance: %w", err)
  5739  		}
  5740  
  5741  		txOpts, err := cl.txOpts(ctx, 0, g.Transfer, maxFeeRate, tipRate, nil)
  5742  		if err != nil {
  5743  			return fmt.Errorf("error generating tx opts: %w", err)
  5744  		}
  5745  
  5746  		tx, err := tokenContract.Transfer(txOpts, returnAddr, bigTokenBal)
  5747  		if err != nil {
  5748  			return fmt.Errorf("error transferring tokens : %w", err)
  5749  		}
  5750  		log.Infof("Sent %s in transaction %s", ui.ConventionalString(token.EVMToAtomic(bigTokenBal)), tx.Hash())
  5751  		return nil
  5752  	}
  5753  
  5754  	bigFees := new(big.Int).Mul(new(big.Int).SetUint64(defaultSendGasLimit), maxFeeRate)
  5755  
  5756  	fees := dexeth.WeiToGweiCeil(bigFees)
  5757  
  5758  	ethFmt := ui.ConventionalString
  5759  	if fees >= ethBal {
  5760  		return fmt.Errorf("balance is lower than projected fees: %s < %s", ethFmt(ethBal), ethFmt(fees))
  5761  	}
  5762  
  5763  	remainder := ethBal - fees
  5764  	txOpts, err := cl.txOpts(ctx, remainder, defaultSendGasLimit, maxFeeRate, tipRate, nil)
  5765  	if err != nil {
  5766  		return fmt.Errorf("error generating tx opts: %w", err)
  5767  	}
  5768  	tx, err := cl.sendTransaction(ctx, txOpts, returnAddr, nil)
  5769  	if err != nil {
  5770  		return fmt.Errorf("error sending funds: %w", err)
  5771  	}
  5772  	log.Infof("Sent %s in transaction %s", ui.ConventionalString(remainder), tx.Hash())
  5773  	return nil
  5774  }
  5775  
  5776  // Estimate gets estimates useful for populating dexeth.Gases fields. Initiation
  5777  // and redeeem transactions with 1, 2, ... , and n swaps per transaction will be
  5778  // sent, for a total of n * (n + 1) / 2 total swaps in 2 * n transactions. If
  5779  // this is a token, and additional 1 approval transaction and 1 transfer
  5780  // transaction will be sent. The transfer transaction will send 1 atom to a
  5781  // random address (with zero token balance), to maximize gas costs. This atom is
  5782  // not recoverable. If you run this function with insufficient or zero ETH
  5783  // and/or token balance on the seed, the function will error with a message
  5784  // indicating the amount of funding needed to run.
  5785  func (getGas) Estimate(ctx context.Context, net dex.Network, assetID, contractVer uint32, maxSwaps int,
  5786  	credentialsPath string, wParams *GetGasWalletParams, log dex.Logger) error {
  5787  
  5788  	symbol := dex.BipIDSymbol(assetID)
  5789  	log.Infof("Getting gas estimates for up to %d swaps of asset %s, contract version %d on %s", maxSwaps, symbol, contractVer, symbol)
  5790  
  5791  	isToken := wParams.Token != nil
  5792  
  5793  	seed, providers, err := getFileCredentials(GetGas.chainForAssetID(assetID), credentialsPath, net)
  5794  	if err != nil {
  5795  		return err
  5796  	}
  5797  
  5798  	walletDir, err := os.MkdirTemp("", "")
  5799  	if err != nil {
  5800  		return err
  5801  	}
  5802  	defer os.RemoveAll(walletDir)
  5803  
  5804  	cl, c, ethReq, swapReq, feeRate, ethBal, tokenBal, err := getGetGasClientWithEstimatesAndBalances(ctx, net, contractVer, maxSwaps, walletDir, providers, seed, wParams, log)
  5805  	if err != nil {
  5806  		return fmt.Errorf("%s: getGetGasClientWithEstimatesAndBalances error: %w", symbol, err)
  5807  	}
  5808  	defer cl.shutdown()
  5809  
  5810  	log.Infof("Initiator address: %s", cl.address())
  5811  
  5812  	ui := wParams.UnitInfo
  5813  	assetFmt := ui.ConventionalString
  5814  	bui := wParams.BaseUnitInfo
  5815  	bUnit := bui.Conventional.Unit
  5816  	ethFmt := bui.ConventionalString
  5817  
  5818  	atomicBal := dexeth.WeiToGwei(ethBal)
  5819  	log.Infof("%s balance: %s", bUnit, ethFmt(atomicBal))
  5820  	if atomicBal < ethReq {
  5821  		return fmt.Errorf("%s balance insufficient to get gas estimates. current: %[2]s, required ~ %[3]s %[1]s. send %[1]s to %[4]s",
  5822  			bUnit, ethFmt(atomicBal), ethFmt(ethReq*5/4), cl.address())
  5823  	}
  5824  
  5825  	// Run the miner now, in case we need it for the approval client preload.
  5826  	if net == dex.Simnet {
  5827  		symbolParts := strings.Split(symbol, ".") // e.g. usdc.polygon, usdc.eth
  5828  		runSimnetMiner(ctx, symbolParts[len(symbolParts)-1], log)
  5829  	}
  5830  
  5831  	var approvalClient *multiRPCClient
  5832  	var approvalContractor tokenContractor
  5833  	if isToken {
  5834  
  5835  		atomicBal := wParams.Token.EVMToAtomic(tokenBal)
  5836  
  5837  		convUnit := ui.Conventional.Unit
  5838  		log.Infof("%s balance: %s %s", strings.ToUpper(symbol), assetFmt(atomicBal), convUnit)
  5839  		log.Infof("%d %s required for swaps", swapReq, ui.AtomicUnit)
  5840  		log.Infof("%d gwei %s required for fees", ethReq, bui.Conventional.Unit)
  5841  		if atomicBal < swapReq {
  5842  			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",
  5843  				assetFmt(atomicBal), assetFmt(swapReq), convUnit, cl.address())
  5844  		}
  5845  
  5846  		var mrc contractor
  5847  		approvalClient, mrc, err = quickNode(ctx, filepath.Join(walletDir, "ac_dir"), contractVer, encode.RandomBytes(32), providers, wParams, net, log)
  5848  		if err != nil {
  5849  			return fmt.Errorf("error creating approval contract node: %v", err)
  5850  		}
  5851  		approvalContractor = mrc.(tokenContractor)
  5852  		defer approvalClient.shutdown()
  5853  
  5854  		// TODO: We're overloading by probably 140% here, but this is what
  5855  		// we've reserved in our fee checks. Is it worth recovering unused
  5856  		// balance?
  5857  		feePreload := wParams.Gas.Approve * 2 * 6 / 5 * feeRate
  5858  		txOpts, err := cl.txOpts(ctx, feePreload, defaultSendGasLimit, nil, nil, nil)
  5859  		if err != nil {
  5860  			return fmt.Errorf("error creating tx opts for sending fees for approval client: %v", err)
  5861  		}
  5862  
  5863  		tx, err := cl.sendTransaction(ctx, txOpts, approvalClient.address(), nil)
  5864  		if err != nil {
  5865  			return fmt.Errorf("error sending fee reserves to approval client: %v", err)
  5866  		}
  5867  		log.Infof("Funded approval client gas with %s in transaction %s", wParams.UnitInfo.FormatAtoms(feePreload), tx.Hash())
  5868  		if err = waitForConfirmation(ctx, "approval client fee funding", approvalClient, tx.Hash(), log); err != nil {
  5869  			return fmt.Errorf("error waiting for approval fee funding tx: %w", err)
  5870  		}
  5871  
  5872  	} else {
  5873  		log.Infof("%d gwei %s required for fees and swaps", ethReq, bui.Conventional.Unit)
  5874  	}
  5875  
  5876  	log.Debugf("Getting gas estimates")
  5877  	return getGasEstimates(ctx, cl, approvalClient, c, approvalContractor, maxSwaps, wParams.Gas, log)
  5878  }
  5879  
  5880  // getGasEstimate is used to get a gas table for an asset's contract(s). The
  5881  // provided gases, g, should be generous estimates of what the gas might be.
  5882  // Errors are thrown if the provided estimates are too small by more than a
  5883  // factor of 2. The account should already have a trading balance of at least
  5884  // maxSwaps gwei (token or eth), and sufficient eth balance to cover the
  5885  // requisite tx fees.
  5886  //
  5887  // acl (approval client) and ac (approval contractor) should be an ethFetcher
  5888  // and tokenContractor for a fresh account. They are used to get the approval
  5889  // gas estimate. These are only needed when the asset is a token. For eth, they
  5890  // can be nil.
  5891  func getGasEstimates(ctx context.Context, cl, acl ethFetcher, c contractor, ac tokenContractor,
  5892  	maxSwaps int, g *dexeth.Gases, log dex.Logger) (err error) {
  5893  
  5894  	tc, isToken := c.(tokenContractor)
  5895  
  5896  	stats := struct {
  5897  		swaps     []uint64
  5898  		redeems   []uint64
  5899  		refunds   []uint64
  5900  		approves  []uint64
  5901  		transfers []uint64
  5902  	}{}
  5903  
  5904  	avg := func(vs []uint64) uint64 {
  5905  		var sum uint64
  5906  		for _, v := range vs {
  5907  			sum += v
  5908  		}
  5909  		return sum / uint64(len(vs))
  5910  	}
  5911  
  5912  	avgDiff := func(vs []uint64) uint64 {
  5913  		diffs := make([]uint64, 0, len(vs)-1)
  5914  		for i := 0; i < len(vs)-1; i++ {
  5915  			diffs = append(diffs, vs[i+1]-vs[i])
  5916  		}
  5917  		return avg(diffs)
  5918  	}
  5919  
  5920  	recommendedGas := func(v uint64) uint64 {
  5921  		return v * 13 / 10
  5922  	}
  5923  
  5924  	baseRate, tipRate, err := cl.currentFees(ctx)
  5925  	if err != nil {
  5926  		return fmt.Errorf("error getting network fees: %v", err)
  5927  	}
  5928  
  5929  	defer func() {
  5930  		if len(stats.swaps) == 0 {
  5931  			return
  5932  		}
  5933  		firstSwap := stats.swaps[0]
  5934  		fmt.Printf("  First swap used %d gas Recommended Gases.Swap = %d\n", firstSwap, recommendedGas(firstSwap))
  5935  		if len(stats.swaps) > 1 {
  5936  			swapAdd := avgDiff(stats.swaps)
  5937  			fmt.Printf("    %d additional swaps averaged %d gas each. Recommended Gases.SwapAdd = %d\n",
  5938  				len(stats.swaps)-1, swapAdd, recommendedGas(swapAdd))
  5939  			fmt.Printf("    %+v \n", stats.swaps)
  5940  		}
  5941  		if len(stats.redeems) == 0 {
  5942  			return
  5943  		}
  5944  		firstRedeem := stats.redeems[0]
  5945  		fmt.Printf("  First redeem used %d gas. Recommended Gases.Redeem = %d\n", firstRedeem, recommendedGas(firstRedeem))
  5946  		if len(stats.redeems) > 1 {
  5947  			redeemAdd := avgDiff(stats.redeems)
  5948  			fmt.Printf("    %d additional redeems averaged %d gas each. recommended Gases.RedeemAdd = %d\n",
  5949  				len(stats.redeems)-1, redeemAdd, recommendedGas(redeemAdd))
  5950  			fmt.Printf("    %+v \n", stats.redeems)
  5951  		}
  5952  		redeemGas := avg(stats.refunds)
  5953  		fmt.Printf("  Average of %d refunds: %d. Recommended Gases.Refund = %d\n",
  5954  			len(stats.refunds), redeemGas, recommendedGas(redeemGas))
  5955  		fmt.Printf("    %+v \n", stats.refunds)
  5956  		if !isToken {
  5957  			return
  5958  		}
  5959  		approveGas := avg(stats.approves)
  5960  		fmt.Printf("  Average of %d approvals: %d. Recommended Gases.Approve = %d\n",
  5961  			len(stats.approves), approveGas, recommendedGas(approveGas))
  5962  		fmt.Printf("    %+v \n", stats.approves)
  5963  		transferGas := avg(stats.transfers)
  5964  		fmt.Printf("  Average of %d transfers: %d. Recommended Gases.Transfer = %d\n",
  5965  			len(stats.transfers), transferGas, recommendedGas(transferGas))
  5966  		fmt.Printf("    %+v \n", stats.transfers)
  5967  	}()
  5968  
  5969  	// Estimate approve for tokens.
  5970  	if isToken {
  5971  		sendApprove := func(cl ethFetcher, c tokenContractor) error {
  5972  			txOpts, err := cl.txOpts(ctx, 0, g.Approve*2, baseRate, tipRate, nil)
  5973  			if err != nil {
  5974  				return fmt.Errorf("error constructing signed tx opts for approve: %w", err)
  5975  			}
  5976  			tx, err := c.approve(txOpts, unlimitedAllowance)
  5977  			if err != nil {
  5978  				return fmt.Errorf("error estimating approve gas: %w", err)
  5979  			}
  5980  			if err = waitForConfirmation(ctx, "approval", cl, tx.Hash(), log); err != nil {
  5981  				return fmt.Errorf("error waiting for approve transaction: %w", err)
  5982  			}
  5983  			receipt, _, err := cl.transactionAndReceipt(ctx, tx.Hash())
  5984  			if err != nil {
  5985  				return fmt.Errorf("error getting receipt for approve tx: %w", err)
  5986  			}
  5987  			if err = checkTxStatus(receipt, g.Approve*2); err != nil {
  5988  				return fmt.Errorf("approve tx failed status check: [%w]. %d gas used out of %d available", err, receipt.GasUsed, g.Approve*2)
  5989  			}
  5990  
  5991  			log.Infof("%d gas used for approval tx", receipt.GasUsed)
  5992  			stats.approves = append(stats.approves, receipt.GasUsed)
  5993  			return nil
  5994  		}
  5995  
  5996  		log.Debugf("Sending approval transaction for random test client")
  5997  		if err = sendApprove(acl, ac); err != nil {
  5998  			return fmt.Errorf("error sending approve transaction for the new client: %w", err)
  5999  		}
  6000  
  6001  		log.Debugf("Sending approval transaction for initiator")
  6002  		if err = sendApprove(cl, tc); err != nil {
  6003  			return fmt.Errorf("error sending approve transaction for the initiator: %w", err)
  6004  		}
  6005  
  6006  		txOpts, err := cl.txOpts(ctx, 0, g.Transfer*2, baseRate, tipRate, nil)
  6007  		if err != nil {
  6008  			return fmt.Errorf("error constructing signed tx opts for transfer: %w", err)
  6009  		}
  6010  		log.Debugf("Sending transfer transaction")
  6011  		// Transfer should be to a random address to maximize costs. This is a
  6012  		// sacrificial atom.
  6013  		var randomAddr common.Address
  6014  		copy(randomAddr[:], encode.RandomBytes(20))
  6015  		transferTx, err := tc.transfer(txOpts, randomAddr, big.NewInt(1))
  6016  		if err != nil {
  6017  			return fmt.Errorf("transfer error: %w", err)
  6018  		}
  6019  		if err = waitForConfirmation(ctx, "transfer", cl, transferTx.Hash(), log); err != nil {
  6020  			return fmt.Errorf("error waiting for transfer tx: %w", err)
  6021  		}
  6022  		receipt, _, err := cl.transactionAndReceipt(ctx, transferTx.Hash())
  6023  		if err != nil {
  6024  			return fmt.Errorf("error getting tx receipt for transfer tx: %w", err)
  6025  		}
  6026  		if err = checkTxStatus(receipt, g.Transfer*2); err != nil {
  6027  			return fmt.Errorf("transfer tx failed status check: [%w]. %d gas used out of %d available", err, receipt.GasUsed, g.Transfer*2)
  6028  		}
  6029  		log.Infof("%d gas used for transfer tx", receipt.GasUsed)
  6030  		stats.transfers = append(stats.transfers, receipt.GasUsed)
  6031  	}
  6032  
  6033  	for n := 1; n <= maxSwaps; n++ {
  6034  		contracts := make([]*asset.Contract, 0, n)
  6035  		secrets := make([][32]byte, 0, n)
  6036  		for i := 0; i < n; i++ {
  6037  			secretB := encode.RandomBytes(32)
  6038  			var secret [32]byte
  6039  			copy(secret[:], secretB)
  6040  			secretHash := sha256.Sum256(secretB)
  6041  			contracts = append(contracts, &asset.Contract{
  6042  				Address:    cl.address().String(), // trading with self
  6043  				Value:      1,
  6044  				SecretHash: secretHash[:],
  6045  				LockTime:   uint64(time.Now().Add(-time.Hour).Unix()),
  6046  			})
  6047  			secrets = append(secrets, secret)
  6048  		}
  6049  
  6050  		var optsVal uint64
  6051  		if !isToken {
  6052  			optsVal = uint64(n)
  6053  		}
  6054  
  6055  		// Send the inits
  6056  		txOpts, err := cl.txOpts(ctx, optsVal, g.SwapN(n)*2, baseRate, tipRate, nil)
  6057  		if err != nil {
  6058  			return fmt.Errorf("error constructing signed tx opts for %d swaps: %v", n, err)
  6059  		}
  6060  		log.Debugf("Sending %d swaps", n)
  6061  		tx, err := c.initiate(txOpts, contracts)
  6062  		if err != nil {
  6063  			return fmt.Errorf("initiate error for %d swaps: %v", n, err)
  6064  		}
  6065  		if err = waitForConfirmation(ctx, "init", cl, tx.Hash(), log); err != nil {
  6066  			return fmt.Errorf("error waiting for init tx to be mined: %w", err)
  6067  		}
  6068  		receipt, _, err := cl.transactionAndReceipt(ctx, tx.Hash())
  6069  		if err != nil {
  6070  			return fmt.Errorf("error getting init tx receipt: %w", err)
  6071  		}
  6072  		if err = checkTxStatus(receipt, txOpts.GasLimit); err != nil {
  6073  			return fmt.Errorf("init tx failed status check: %w", err)
  6074  		}
  6075  		log.Infof("%d gas used for %d initiation txs", receipt.GasUsed, n)
  6076  		stats.swaps = append(stats.swaps, receipt.GasUsed)
  6077  
  6078  		// Estimate a refund
  6079  		var firstSecretHash [32]byte
  6080  		copy(firstSecretHash[:], contracts[0].SecretHash)
  6081  		refundGas, err := c.estimateRefundGas(ctx, firstSecretHash)
  6082  		if err != nil {
  6083  			return fmt.Errorf("error estimate refund gas: %w", err)
  6084  		}
  6085  		log.Infof("%d gas estimated for a refund", refundGas)
  6086  		stats.refunds = append(stats.refunds, refundGas)
  6087  
  6088  		redemptions := make([]*asset.Redemption, 0, n)
  6089  		for i, contract := range contracts {
  6090  			redemptions = append(redemptions, &asset.Redemption{
  6091  				Spends: &asset.AuditInfo{
  6092  					SecretHash: contract.SecretHash,
  6093  				},
  6094  				Secret: secrets[i][:],
  6095  			})
  6096  		}
  6097  
  6098  		txOpts, err = cl.txOpts(ctx, 0, g.RedeemN(n)*2, baseRate, tipRate, nil)
  6099  		if err != nil {
  6100  			return fmt.Errorf("error constructing signed tx opts for %d redeems: %v", n, err)
  6101  		}
  6102  		log.Debugf("Sending %d redemption txs", n)
  6103  		tx, err = c.redeem(txOpts, redemptions)
  6104  		if err != nil {
  6105  			return fmt.Errorf("redeem error for %d swaps: %v", n, err)
  6106  		}
  6107  		if err = waitForConfirmation(ctx, "redeem", cl, tx.Hash(), log); err != nil {
  6108  			return fmt.Errorf("error waiting for redeem tx to be mined: %w", err)
  6109  		}
  6110  		receipt, _, err = cl.transactionAndReceipt(ctx, tx.Hash())
  6111  		if err != nil {
  6112  			return fmt.Errorf("error getting redeem tx receipt: %w", err)
  6113  		}
  6114  		if err = checkTxStatus(receipt, txOpts.GasLimit); err != nil {
  6115  			return fmt.Errorf("redeem tx failed status check: %w", err)
  6116  		}
  6117  		log.Infof("%d gas used for %d redemptions", receipt.GasUsed, n)
  6118  		stats.redeems = append(stats.redeems, receipt.GasUsed)
  6119  	}
  6120  
  6121  	return nil
  6122  }
  6123  
  6124  // newTxOpts is a constructor for a TransactOpts.
  6125  func newTxOpts(ctx context.Context, from common.Address, val, maxGas uint64, maxFeeRate, gasTipCap *big.Int) *bind.TransactOpts {
  6126  	// We'll enforce dexeth.MinGasTipCap since the server does, but this isn't
  6127  	// necessarily a constant for all networks or under all conditions.
  6128  	minGasWei := dexeth.GweiToWei(dexeth.MinGasTipCap)
  6129  	if gasTipCap.Cmp(minGasWei) < 0 {
  6130  		gasTipCap.Set(minGasWei)
  6131  	}
  6132  	// This is enforced by concensus. We shouldn't be able to get here with a
  6133  	// swap tx.
  6134  	if gasTipCap.Cmp(maxFeeRate) > 0 {
  6135  		gasTipCap.Set(maxFeeRate)
  6136  	}
  6137  	return &bind.TransactOpts{
  6138  		Context:   ctx,
  6139  		From:      from,
  6140  		Value:     dexeth.GweiToWei(val),
  6141  		GasFeeCap: maxFeeRate,
  6142  		GasTipCap: gasTipCap,
  6143  		GasLimit:  maxGas,
  6144  	}
  6145  }
  6146  
  6147  func gases(contractVer uint32, versionedGases map[uint32]*dexeth.Gases) *dexeth.Gases {
  6148  	if contractVer != contractVersionNewest {
  6149  		return versionedGases[contractVer]
  6150  	}
  6151  	var bestVer uint32
  6152  	var bestGases *dexeth.Gases
  6153  	for ver, gases := range versionedGases {
  6154  		if ver >= bestVer {
  6155  			bestGases = gases
  6156  			bestVer = ver
  6157  		}
  6158  	}
  6159  	return bestGases
  6160  }