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