diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index b7d27b3404..7d348b93cc 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -2197,7 +2197,7 @@ func testAvailableFund(t *testing.T, segwit bool, walletType string) { } ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: 0, MaxSwapCount: 1, MaxFeeRate: tBTC.MaxFeeRate, @@ -2762,7 +2762,7 @@ func TestFundEdges(t *testing.T) { unspents := []*ListUnspentResult{p2pkhUnspent} node.listUnspent = unspents ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: swapVal, MaxSwapCount: lots, MaxFeeRate: tBTC.MaxFeeRate, @@ -2986,7 +2986,7 @@ func TestFundEdgesSegwit(t *testing.T) { unspents := []*ListUnspentResult{p2wpkhUnspent} node.listUnspent = unspents ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: swapVal, MaxSwapCount: lots, MaxFeeRate: tBTC.MaxFeeRate, @@ -4365,7 +4365,7 @@ func testPreSwap(t *testing.T, segwit bool, walletType string) { } form := &asset.PreSwapForm{ - Version: version, + AssetVersion: version, LotSize: tLotSize, Lots: lots, MaxFeeRate: tBTC.MaxFeeRate, @@ -4411,8 +4411,8 @@ func testPreRedeem(t *testing.T, segwit bool, walletType string) { defer shutdown() preRedeem, err := wallet.PreRedeem(&asset.PreRedeemForm{ - Version: version, - Lots: 5, + AssetVersion: version, + Lots: 5, }) // Shouldn't actually be any path to error. if err != nil { diff --git a/client/asset/btc/livetest/livetest.go b/client/asset/btc/livetest/livetest.go index ba1859afcc..a2e37fd616 100644 --- a/client/asset/btc/livetest/livetest.go +++ b/client/asset/btc/livetest/livetest.go @@ -266,7 +266,7 @@ func Run(t *testing.T, cfg *Config) { checkAmt("second", rig.secondWallet) ord := &asset.Order{ - Version: 0, + AssetVersion: 0, Value: contractValue * 3, MaxSwapCount: lots * 3, MaxFeeRate: cfg.Asset.MaxFeeRate, diff --git a/client/asset/btc/simnet_test.go b/client/asset/btc/simnet_test.go index 7e70260892..a87058d054 100644 --- a/client/asset/btc/simnet_test.go +++ b/client/asset/btc/simnet_test.go @@ -367,7 +367,7 @@ func testWalletTxBalanceSync(t *testing.T, fromWallet, toWallet *ExchangeWalletF } order := &asset.Order{ - Value: toSatoshi(1), + AssetVersion: toSatoshi(1), FeeSuggestion: 10, MaxSwapCount: 1, MaxFeeRate: 20, diff --git a/client/asset/dcr/dcr_test.go b/client/asset/dcr/dcr_test.go index 6634a7e117..c01ad4a836 100644 --- a/client/asset/dcr/dcr_test.go +++ b/client/asset/dcr/dcr_test.go @@ -876,7 +876,7 @@ func TestAvailableFund(t *testing.T) { } ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: 0, MaxSwapCount: 1, MaxFeeRate: tDCR.MaxFeeRate, @@ -2595,7 +2595,7 @@ func TestFundEdges(t *testing.T) { node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent} ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: swapVal, MaxSwapCount: lots, MaxFeeRate: tDCR.MaxFeeRate, @@ -3871,7 +3871,7 @@ func TestPreSwap(t *testing.T) { node.unspent = []walletjson.ListUnspentResult{p2pkhUnspent} form := &asset.PreSwapForm{ - Version: version, + AssetVersion: version, LotSize: tLotSize, Lots: lots, MaxFeeRate: tDCR.MaxFeeRate, @@ -3913,8 +3913,8 @@ func TestPreRedeem(t *testing.T) { defer shutdown() preRedeem, err := wallet.PreRedeem(&asset.PreRedeemForm{ - Version: version, - Lots: 5, + AssetVersion: version, + Lots: 5, }) // Shouldn't actually be any path to error. if err != nil { diff --git a/client/asset/dcr/simnet_test.go b/client/asset/dcr/simnet_test.go index e2cceb3cb9..25f8fc9eb5 100644 --- a/client/asset/dcr/simnet_test.go +++ b/client/asset/dcr/simnet_test.go @@ -348,7 +348,7 @@ func runTest(t *testing.T, splitTx bool) { } ord := &asset.Order{ - Version: tDCR.Version, + AssetVersion: tDCR.Version, Value: contractValue * 3, MaxSwapCount: lots * 3, MaxFeeRate: tDCR.MaxFeeRate, diff --git a/client/asset/estimation.go b/client/asset/estimation.go index ad53c5e305..0d0c664eeb 100644 --- a/client/asset/estimation.go +++ b/client/asset/estimation.go @@ -38,8 +38,9 @@ type RedeemEstimate struct { // PreSwapForm can be used to get a swap fees estimate. type PreSwapForm struct { - // Version is the asset version. Most backends only support one version. - Version uint32 + // AssetVersion is the server's asset version. Most backends only support + // one version. + AssetVersion uint32 // LotSize is the lot size for the calculation. For quote assets, LotSize // should be based on either the user's limit order rate, or some measure // of the current market rate. @@ -83,8 +84,9 @@ type PreSwap struct { // PreRedeemForm can be used to get a redemption estimate. type PreRedeemForm struct { - // Version is the asset version. Most backends only support one version. - Version uint32 + // AssetVersion is the asset version. Most backends only support one + // version. + AssetVersion uint32 // Lots is the number of lots in the order. Lots uint64 // FeeSuggestion is a suggested fee from the server. diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index 74a7ba69dd..cf6ea71af4 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -40,7 +40,6 @@ import ( ethmath "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/params" "github.com/tyler-smith/go-bip39" ) @@ -91,15 +90,6 @@ const ( // see DecodeCoinID func for details. coinIDTakerFoundMakerRedemption = "TakerFoundMakerRedemption:" - // maxTxFeeGwei is the default max amount of eth that can be used in one - // transaction. This is set by the host in the case of providers. The - // internal node currently has no max but also cannot be used since the - // merge. - // - // TODO: Find a way to ask the host about their config set max fee and - // gas values. - maxTxFeeGwei = 1_000_000_000 - LiveEstimateFailedError = dex.ErrorKind("live gas estimate failed") // txAgeOut is the amount of time after which we forego any tx @@ -175,8 +165,6 @@ var ( Seeded: true, GuideLink: "https://github.com/decred/dcrdex/blob/master/docs/wiki/Ethereum.md", }, - // MaxSwapsInTx and MaxRedeemsInTx are set in (Wallet).Info, since - // the value cannot be known until we connect and get network info. }, IsAccountBased: true, } @@ -199,31 +187,6 @@ var ( } ) -// perTxGasLimit is the most gas we can use on a transaction. It is the lower of -// either the per tx or per block gas limit. -func perTxGasLimit(gasFeeLimit uint64) uint64 { - // maxProportionOfBlockGasLimitToUse sets the maximum proportion of a - // block's gas limit that a swap and redeem transaction will use. Since it - // is set to 4, the max that will be used is 25% (1/4) of the block's gas - // limit. - const maxProportionOfBlockGasLimitToUse = 4 - - // blockGasLimit is the amount of gas we can use in one transaction - // according to the block gas limit. - - // Ethereum GasCeil: 30_000_000, Polygon: 8_000_000 - blockGasLimit := ethconfig.Defaults.Miner.GasCeil / maxProportionOfBlockGasLimitToUse - - // txGasLimit is the amount of gas we can use in one transaction - // according to the default transaction gas fee limit. - txGasLimit := maxTxFeeGwei / gasFeeLimit - - if blockGasLimit > txGasLimit { - return txGasLimit - } - return blockGasLimit -} - // safeConfs returns the confirmations for a given tip and block number, // returning 0 if the block number is zero or if the tip is lower than the // block number. @@ -356,7 +319,7 @@ type Balance struct { } // ethFetcher represents a blockchain information fetcher. In practice, it is -// satisfied by *nodeClient. For testing, it can be satisfied by a stub. +// satisfied by *multiRPCClient. For testing, it can be satisfied by a stub. type ethFetcher interface { address() common.Address addressBalance(ctx context.Context, addr common.Address) (*big.Int, error) @@ -435,11 +398,12 @@ type baseWallet struct { multiBalanceAddress common.Address multiBalanceContract *multibal.MultiBalanceV0 - baseChainID uint32 - chainCfg *params.ChainConfig - chainID int64 - compat *CompatibilityData - tokens map[uint32]*dexeth.Token + baseChainID uint32 + chainCfg *params.ChainConfig + chainID int64 + compat *CompatibilityData + tokens map[uint32]*dexeth.Token + maxTxFeeGwei uint64 startingBlocks atomic.Uint64 @@ -491,9 +455,6 @@ type assetWallet struct { versionedContracts map[uint32]common.Address versionedGases map[uint32]*dexeth.Gases - maxSwapGas uint64 - maxRedeemGas uint64 - lockedFunds struct { mtx sync.RWMutex initiateReserves uint64 @@ -545,17 +506,54 @@ type TokenWallet struct { netToken *dexeth.NetToken } -func (w *assetWallet) maxSwapsAndRedeems() (maxSwaps, maxRedeems uint64) { - txGasLimit := perTxGasLimit(atomic.LoadUint64(&w.gasFeeLimitV)) - return txGasLimit / w.maxSwapGas, txGasLimit / w.maxRedeemGas +// perTxGasLimit is the most gas we can use on a transaction. It is the lower of +// either the block's gas limit or the limit based on our maximum allowable +// fees. +func (w *assetWallet) perTxGasLimit(feeRateGwei uint64) uint64 { + blockGasLimit := w.tip().GasLimit + maxFeeBasedGasLimit := w.maxTxFeeGwei / feeRateGwei + if maxFeeBasedGasLimit < blockGasLimit { + return maxFeeBasedGasLimit + } + return blockGasLimit +} + +// maxSwapsOrRedeems calculates the maximum number of swaps or redemptions that +// can go in a single transaction. If feeRateGwei is not provided, the +// prevailing fee rate will be used. +func (w *assetWallet) maxSwapsOrRedeems(oneGas, gasAdd uint64, feeRateGwei uint64) (n int, err error) { + feeRate := dexeth.GweiToWei(feeRateGwei) + if feeRateGwei == 0 { + feeRate, err = w.currentFeeRate(w.ctx) + if err != nil { + return 0, err + } + } + + txGasLimit := w.perTxGasLimit(dexeth.WeiToGweiCeil(feeRate)) + if oneGas > txGasLimit { + return 0, fmt.Errorf("tx limit zero") + } + return 1 + int((txGasLimit-oneGas)/gasAdd), nil +} + +var _ asset.MaxMatchesCounter = (*assetWallet)(nil) + +// MaxSwaps is the maximum matches than can go in a swap tx. +func (w *assetWallet) MaxSwaps(assetVer uint32, feeRateGwei uint64) (int, error) { + g := w.gases(contractVersion(assetVer)) + return w.maxSwapsOrRedeems(g.Swap, g.SwapAdd, feeRateGwei) +} + +// MaxRedeems is the maximum matches than can go in a redeem tx. +func (w *assetWallet) MaxRedeems(assetVer uint32) (int, error) { + g := w.gases(contractVersion(assetVer)) + return w.maxSwapsOrRedeems(g.Redeem, g.RedeemAdd, 0) } // Info returns basic information about the wallet and asset. func (w *assetWallet) Info() *asset.WalletInfo { wi := w.wi - maxSwaps, maxRedeems := w.maxSwapsAndRedeems() - wi.MaxSwapsInTx = maxSwaps - wi.MaxRedeemsInTx = maxRedeems return &wi } @@ -593,13 +591,14 @@ func privKeyFromSeed(seed []byte) (pk []byte, zero func(), err error) { return pk, extKey.Zero, nil } -// contractVersion converts a server version to a contract version. It applies -// to both tokens and eth right now, but that may not always be the case. -func contractVersion(serverVer uint32) uint32 { - if serverVer == asset.VersionNewest { +// contractVersion converts a server's asset protocol version to a swap contract +// version. It applies to both tokens and eth right now, but that may not always +// be the case. +func contractVersion(assetVer uint32) uint32 { + if assetVer == asset.VersionNewest { return dexeth.ContractVersionNewest } - return dexeth.ProtocolVersion(serverVer).ContractVersion() + return dexeth.ProtocolVersion(assetVer).ContractVersion() } func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams, compat *CompatibilityData, skipConnect bool) error { @@ -713,6 +712,7 @@ func newWallet(assetCFG *asset.WalletConfig, logger dex.Logger, net dex.Network) WalletInfo: WalletInfo, Net: net, DefaultProviders: defaultProviders, + MaxTxFeeGwei: dexeth.GweiFactor, // 1 ETH }) } @@ -731,6 +731,9 @@ type EVMWalletConfig struct { MultiBalAddress common.Address // If empty, separate calls for N tokens + 1 WalletInfo asset.WalletInfo Net dex.Network + // MaxTxFeeGwei is the absolute maximum fees we will allow for a single tx. + // It should be set to a relatively large value. + MaxTxFeeGwei uint64 } func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) { @@ -773,6 +776,7 @@ func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) { gasFeeLimitV: gasFeeLimit, wallets: make(map[uint32]*assetWallet), multiBalanceAddress: cfg.MultiBalAddress, + maxTxFeeGwei: cfg.MaxTxFeeGwei, } var maxSwapGas, maxRedeemGas uint64 @@ -785,23 +789,12 @@ func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) { } } - txGasLimit := perTxGasLimit(gasFeeLimit) - - if maxSwapGas == 0 || txGasLimit < maxSwapGas { - return nil, errors.New("max swaps cannot be zero or undefined") - } - if maxRedeemGas == 0 || txGasLimit < maxRedeemGas { - return nil, errors.New("max redeems cannot be zero or undefined") - } - aw := &assetWallet{ baseWallet: eth, log: cfg.Logger, assetID: assetID, versionedContracts: cfg.BaseChainContracts, versionedGases: cfg.VersionedGases, - maxSwapGas: maxSwapGas, - maxRedeemGas: maxRedeemGas, emit: cfg.AssetCfg.Emit, findRedemptionReqs: make(map[string]*findRedemptionRequest), pendingApprovals: make(map[uint32]*pendingApproval), @@ -814,11 +807,6 @@ func NewEVMWallet(cfg *EVMWalletConfig) (w *ETHWallet, err error) { wi: cfg.WalletInfo, } - maxSwaps, maxRedeems := aw.maxSwapsAndRedeems() - - cfg.Logger.Debugf("ETH wallet will support a maximum of %d swaps and %d redeems per transaction.", - maxSwaps, maxRedeems) - aw.wallets = map[uint32]*assetWallet{ assetID: aw, } @@ -948,7 +936,7 @@ func (w *ETHWallet) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) bestHdr.Number, confirmedNonce, nextNonce, len(pendingTxs), highestPendingNonce, lowestPendingNonce) } - height := w.currentTip.Number + height := bestHdr.Number // NOTE: We should be using the tipAtConnect to set Progress in SyncStatus. atomic.StoreInt64(&w.tipAtConnect, height.Int64()) w.log.Infof("Connected to eth (%s), at height %d", w.walletType, height) @@ -1006,11 +994,15 @@ func (w *TokenWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) { return &wg, nil } -// tipHeight gets the current best header's tip height. -func (w *baseWallet) tipHeight() uint64 { +func (w *baseWallet) tip() *types.Header { w.tipMtx.RLock() defer w.tipMtx.RUnlock() - return w.currentTip.Number.Uint64() + return w.currentTip +} + +// tipHeight gets the current best header's tip height. +func (w *baseWallet) tipHeight() uint64 { + return w.tip().Number.Uint64() } // Reconfigure attempts to reconfigure the wallet. @@ -1239,25 +1231,6 @@ func (w *ETHWallet) OpenTokenWallet(tokenCfg *asset.TokenConfig) (asset.Wallet, return nil, fmt.Errorf("could not find token with ID %d on network %s", w.assetID, w.net) } - var maxSwapGas, maxRedeemGas uint64 - for _, contract := range netToken.SwapContracts { - if contract.Gas.Swap > maxSwapGas { - maxSwapGas = contract.Gas.Swap - } - if contract.Gas.Redeem > maxRedeemGas { - maxRedeemGas = contract.Gas.Redeem - } - } - - txGasLimit := perTxGasLimit(atomic.LoadUint64(&w.gasFeeLimitV)) - - if maxSwapGas == 0 || txGasLimit < maxSwapGas { - return nil, errors.New("max swaps cannot be zero or undefined") - } - if maxRedeemGas == 0 || txGasLimit < maxRedeemGas { - return nil, errors.New("max redeems cannot be zero or undefined") - } - contracts := make(map[uint32]common.Address) gases := make(map[uint32]*dexeth.Gases) for ver, c := range netToken.SwapContracts { @@ -1271,8 +1244,6 @@ func (w *ETHWallet) OpenTokenWallet(tokenCfg *asset.TokenConfig) (asset.Wallet, assetID: tokenCfg.AssetID, versionedContracts: contracts, versionedGases: gases, - maxSwapGas: maxSwapGas, - maxRedeemGas: maxRedeemGas, emit: tokenCfg.Emit, peersChange: tokenCfg.PeersChange, findRedemptionReqs: make(map[string]*findRedemptionRequest), @@ -1443,19 +1414,19 @@ func (w *TokenWallet) MaxOrder(ord *asset.MaxOrderForm) (*asset.SwapEstimate, er ord.RedeemVersion, ord.RedeemAssetID, w.parent) } -func (w *assetWallet) maxOrder(lotSize uint64, maxFeeRate uint64, serverVer uint32, - redeemServerVer, redeemAssetID uint32, feeWallet *assetWallet) (*asset.SwapEstimate, error) { +func (w *assetWallet) maxOrder(lotSize uint64, maxFeeRate uint64, initAssetVer, + redeemAssetVer, redeemAssetID uint32, feeWallet *assetWallet) (*asset.SwapEstimate, error) { balance, err := w.Balance() if err != nil { return nil, err } - contractVer := contractVersion(serverVer) + initContractVer := contractVersion(initAssetVer) // Get the refund gas. - if g := w.gases(contractVer); g == nil { + if g := w.gases(initContractVer); g == nil { return nil, fmt.Errorf("no gas table") } - g, err := w.initGasEstimate(1, contractVer, contractVersion(redeemServerVer), redeemAssetID) + g, err := w.initGasEstimate(1, initContractVer, contractVersion(redeemAssetVer), redeemAssetID, maxFeeRate) liveEstimateFailed := errors.Is(err, LiveEstimateFailedError) if err != nil && !liveEstimateFailed { return nil, fmt.Errorf("gasEstimate error: %w", err) @@ -1485,7 +1456,7 @@ func (w *assetWallet) maxOrder(lotSize uint64, maxFeeRate uint64, serverVer uint FeeReservesPerLot: feeReservesPerLot, }, nil } - return w.estimateSwap(lots, lotSize, maxFeeRate, contractVer, feeReservesPerLot) + return w.estimateSwap(lots, lotSize, maxFeeRate, initContractVer, feeReservesPerLot) } // PreSwap gets order estimates based on the available funds and the wallet @@ -1501,7 +1472,7 @@ func (w *TokenWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) { } func (w *assetWallet) preSwap(req *asset.PreSwapForm, feeWallet *assetWallet) (*asset.PreSwap, error) { - maxEst, err := w.maxOrder(req.LotSize, req.MaxFeeRate, req.Version, + maxEst, err := w.maxOrder(req.LotSize, req.MaxFeeRate, req.AssetVersion, req.RedeemVersion, req.RedeemAssetID, feeWallet) if err != nil { return nil, err @@ -1512,7 +1483,7 @@ func (w *assetWallet) preSwap(req *asset.PreSwapForm, feeWallet *assetWallet) (* } est, err := w.estimateSwap(req.Lots, req.LotSize, req.MaxFeeRate, - contractVersion(req.Version), maxEst.FeeReservesPerLot) + contractVersion(req.AssetVersion), maxEst.FeeReservesPerLot) if err != nil { return nil, err } @@ -1528,11 +1499,11 @@ func (w *baseWallet) MaxFundingFees(_ uint32, _ uint64, _ map[string]string) uin } // SingleLotSwapRefundFees returns the fees for a swap transaction for a single lot. -func (w *assetWallet) SingleLotSwapRefundFees(serverVer uint32, feeSuggestion uint64, _ bool) (swapFees uint64, refundFees uint64, err error) { - contractVer := contractVersion(serverVer) +func (w *assetWallet) SingleLotSwapRefundFees(assetVer uint32, feeSuggestion uint64, _ bool) (swapFees uint64, refundFees uint64, err error) { + contractVer := contractVersion(assetVer) g := w.gases(contractVer) if g == nil { - return 0, 0, fmt.Errorf("no gases known for %d contract version %d", w.assetID, contractVersion(serverVer)) + return 0, 0, fmt.Errorf("no gases known for %d contract version %d", w.assetID, contractVersion(assetVer)) } return g.Swap * feeSuggestion, g.Refund * feeSuggestion, nil } @@ -1585,7 +1556,7 @@ func (w *assetWallet) gases(contractVer uint32) *dexeth.Gases { // PreRedeem generates an estimate of the range of redemption fees that could // be assessed. func (w *assetWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, error) { - oneRedeem, nRedeem, err := w.redeemGas(int(req.Lots), contractVersion(req.Version)) + oneRedeem, nRedeem, err := w.redeemGas(int(req.Lots), contractVersion(req.AssetVersion)) if err != nil { return nil, err } @@ -1599,10 +1570,10 @@ func (w *assetWallet) PreRedeem(req *asset.PreRedeemForm) (*asset.PreRedeem, err } // SingleLotRedeemFees returns the fees for a redeem transaction for a single lot. -func (w *assetWallet) SingleLotRedeemFees(serverVer uint32, feeSuggestion uint64) (fees uint64, err error) { - g := w.gases(contractVersion(serverVer)) +func (w *assetWallet) SingleLotRedeemFees(assetVer uint32, feeSuggestion uint64) (fees uint64, err error) { + g := w.gases(contractVersion(assetVer)) if g == nil { - return 0, fmt.Errorf("no gases known for %d, constract version %d", w.assetID, contractVersion(serverVer)) + return 0, fmt.Errorf("no gases known for %d, constract version %d", w.assetID, contractVersion(assetVer)) } return g.Redeem * feeSuggestion, nil } @@ -1660,10 +1631,10 @@ func (w *ETHWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint6 dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) } - contractVer := contractVersion(ord.Version) + contractVer := contractVersion(ord.AssetVersion) g, err := w.initGasEstimate(int(ord.MaxSwapCount), contractVer, - ord.RedeemVersion, ord.RedeemAssetID) + ord.RedeemVersion, ord.RedeemAssetID, ord.MaxFeeRate) if err != nil { return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) } @@ -1700,7 +1671,7 @@ func (w *TokenWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uin dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) } - contractVer := contractVersion(ord.Version) + contractVer := contractVersion(ord.AssetVersion) approvalStatus, err := w.approvalStatus(contractVer) if err != nil { return nil, nil, 0, fmt.Errorf("error getting approval status: %v", err) @@ -1716,7 +1687,7 @@ func (w *TokenWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uin } g, err := w.initGasEstimate(int(ord.MaxSwapCount), contractVer, - ord.RedeemVersion, ord.RedeemAssetID) + ord.RedeemVersion, ord.RedeemAssetID, ord.MaxFeeRate) if err != nil { return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) } @@ -1753,7 +1724,7 @@ func (w *ETHWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]ass dex.BipIDSymbol(w.assetID), ord.MaxFeeRate, w.gasFeeLimit()) } - g, err := w.initGasEstimate(1, ord.Version, ord.RedeemVersion, ord.RedeemAssetID) + g, err := w.initGasEstimate(1, ord.Version, ord.RedeemVersion, ord.RedeemAssetID, ord.MaxFeeRate) if err != nil { return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) } @@ -1806,7 +1777,7 @@ func (w *TokenWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) ([]a } g, err := w.initGasEstimate(1, ord.Version, - ord.RedeemVersion, ord.RedeemAssetID) + ord.RedeemVersion, ord.RedeemAssetID, ord.MaxFeeRate) if err != nil { return nil, nil, 0, fmt.Errorf("error estimating swap gas: %v", err) } @@ -1860,17 +1831,17 @@ type gasEstimate struct { // initGasEstimate gets the best available gas estimate for n initiations. A // live estimate is checked against the server's configured values and our own // known values and errors or logs generated in certain cases. -func (w *assetWallet) initGasEstimate(n int, initVer, redeemVer, redeemAssetID uint32) (est *gasEstimate, err error) { +func (w *assetWallet) initGasEstimate(n int, initContractVer, redeemContractVer, redeemAssetID uint32, feeRateGwei uint64) (est *gasEstimate, err error) { est = new(gasEstimate) // Get the refund gas. - if g := w.gases(initVer); g == nil { + if g := w.gases(initContractVer); g == nil { return nil, fmt.Errorf("no gas table") } else { // scoping g est.Refund = g.Refund } - est.Swap, est.nSwap, err = w.swapGas(n, initVer) + est.Swap, est.nSwap, err = w.swapGas(n, initContractVer, feeRateGwei) if err != nil && !errors.Is(err, LiveEstimateFailedError) { return nil, err } @@ -1881,7 +1852,7 @@ func (w *assetWallet) initGasEstimate(n int, initVer, redeemVer, redeemAssetID u if redeemW := w.wallet(redeemAssetID); redeemW != nil { var er error - est.Redeem, est.nRedeem, er = redeemW.redeemGas(n, redeemVer) + est.Redeem, est.nRedeem, er = redeemW.redeemGas(n, redeemContractVer) if err != nil { return nil, fmt.Errorf("error calculating fee-family redeem gas: %w", er) } @@ -1896,69 +1867,46 @@ func (w *assetWallet) initGasEstimate(n int, initVer, redeemVer, redeemAssetID u // cannot get a live estimate from the contractor, which will happen if the // wallet has no balance. A live gas estimate will always be attempted, and used // if our expected gas values are lower (anomalous). -func (w *assetWallet) swapGas(n int, contractVer uint32) (oneSwap, nSwap uint64, err error) { +func (w *assetWallet) swapGas(n int, contractVer uint32, feeRateGwei uint64) (oneSwapGas, nSwapGas uint64, err error) { g := w.gases(contractVer) if g == nil { return 0, 0, fmt.Errorf("no gases known for %d contract version %d", w.assetID, contractVer) } - oneSwap = g.Swap - - // We have no way of updating the value of SwapAdd without a version change, - // but we're not gonna worry about accuracy for nSwap, since it's only used - // for estimates and never for dex-validated values like order funding. - nSwap = oneSwap + uint64(n-1)*g.SwapAdd + oneSwapGas = g.Swap // The amount we can estimate and ultimately the amount we can use in a // single transaction is limited by the block gas limit or the tx gas // limit. Core will use the largest gas among all versions when // determining the maximum number of swaps that can be in one - // transaction. Limit our gas estimate to the same number of swaps. - nMax := n - maxSwaps, _ := w.maxSwapsAndRedeems() - var nRemain, nFull int - if uint64(n) > maxSwaps { - nMax = int(maxSwaps) - nFull = n / nMax - nSwap = (oneSwap + uint64(nMax-1)*g.SwapAdd) * uint64(nFull) - nRemain = n % nMax - if nRemain != 0 { - nSwap += oneSwap + uint64(nRemain-1)*g.SwapAdd - } + // transaction. Limit our gas estimate to the same number of swaps/redeems.. + maxSwapsPerTx, err := w.maxSwapsOrRedeems(g.Swap, g.SwapAdd, feeRateGwei) + if err != nil { + return 0, 0, fmt.Errorf("error calculating max swaps: %w", err) } - // If a live estimate is greater than our estimate from configured values, - // use the live estimate with a warning. - gasEst, err := w.estimateInitGas(w.ctx, nMax, contractVer) - if err != nil { - err = errors.Join(err, LiveEstimateFailedError) - return - // Or we could go with what we know? But this estimate error could be a - // hint that the transaction would fail, and we don't have a way to - // recover from that. Play it safe and allow caller to retry assuming - // the error is transient with the provider. - // w.log.Errorf("(%d) error estimating swap gas (using expected gas cap instead): %v", w.assetID, err) - // return oneSwap, nSwap, true, nil - } - if nMax != n { - // If we needed to adjust the max earlier, and the estimate did - // not error, multiply the estimate by the number of full - // transactions and add the estimate of the remainder. - gasEst *= uint64(nFull) - if nRemain > 0 { - remainEst, err := w.estimateInitGas(w.ctx, nRemain, contractVer) - if err != nil { - w.log.Errorf("(%d) error estimating swap gas for remainder: %v", w.assetID, err) - return 0, 0, err - } - gasEst += remainEst + if nFull := n / maxSwapsPerTx; nFull > 0 { + fullGas := g.SwapN(maxSwapsPerTx) + if fullGasEst, err := w.estimateInitGas(w.ctx, maxSwapsPerTx, contractVer); err != nil { + w.log.Errorf("(%d) error estimating swap gas for full txs: %v", w.assetID, err) + return 0, 0, errors.Join(err, LiveEstimateFailedError) + } else if fullGasEst > fullGas { + w.log.Warnf("%d-tx (full) swap gas estimate %d is greater than the server's configured value %d. Using live estimate + 10%%.", + nFull, fullGasEst, fullGas) + fullGas = fullGasEst * 11 / 10 } + nSwapGas = uint64(nFull) * fullGas } - if gasEst > nSwap { - w.log.Warnf("Swap gas estimate %d is greater than the server's configured value %d. Using live estimate + 10%%.", gasEst, nSwap) - nSwap = gasEst * 11 / 10 // 10% buffer - if n == 1 && nSwap > oneSwap { - oneSwap = nSwap + if nRemain := n & maxSwapsPerTx; nRemain > 0 { + remainGas := g.SwapN(nRemain) + if remainGasEst, err := w.estimateInitGas(w.ctx, nRemain, contractVer); err != nil { + w.log.Errorf("(%d) error estimating swap gas for remainder: %v", w.assetID, err) + return 0, 0, errors.Join(err, LiveEstimateFailedError) + } else if remainGasEst > remainGas { + w.log.Warnf("%d-tx swap gas estimate %d is greater than the server's configured value %d. Using live estimate + 10%%.", + nRemain, remainGasEst, remainGas) + remainGas = remainGasEst * 11 / 10 } + nSwapGas += remainGas } return } @@ -1973,7 +1921,7 @@ func (w *assetWallet) redeemGas(n int, contractVer uint32) (oneGas, nGas uint64, redeemGas := g.Redeem // Not concerned with the accuracy of nGas. It's never used outside of // best case estimates. - return redeemGas, redeemGas + (uint64(n)-1)*g.RedeemAdd, nil + return redeemGas, g.RedeemN(n), nil } // approvalGas gets the best available estimate for an approval tx, which is @@ -2176,9 +2124,9 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 swapVal += contract.Value } - contractVer := contractVersion(swaps.Version) + contractVer := contractVersion(swaps.AssetVersion) n := len(swaps.Contracts) - oneSwap, nSwap, err := w.swapGas(n, contractVer) + oneSwap, nSwap, err := w.swapGas(n, contractVer, swaps.FeeRate) if err != nil { return fail("error getting gas fees: %v", err) } @@ -2291,8 +2239,8 @@ func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uin } n := len(swaps.Contracts) - contractVer := contractVersion(swaps.Version) - oneSwap, nSwap, err := w.swapGas(n, contractVer) + contractVer := contractVersion(swaps.AssetVersion) + oneSwap, nSwap, err := w.swapGas(n, contractVer, swaps.FeeRate) if err != nil { return fail("error getting gas fees: %v", err) } @@ -2323,11 +2271,11 @@ func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uin return fail("Swap: initiate error: %w", err) } - if w.netToken.SwapContracts[swaps.Version] == nil { - return fail("unable to find contract address for asset %d contract version %d", w.assetID, swaps.Version) + if w.netToken.SwapContracts[swaps.AssetVersion] == nil { + return fail("unable to find contract address for asset %d contract version %d", w.assetID, swaps.AssetVersion) } - contractAddr := w.netToken.SwapContracts[swaps.Version].Address.String() + contractAddr := w.netToken.SwapContracts[contractVer].Address.String() txHash := tx.Hash() receipts := make([]asset.Receipt, 0, n) @@ -2774,8 +2722,8 @@ func (w *ETHWallet) ReserveNRedemptions(n uint64, ver uint32, maxFeeRate uint64) // ReserveNRedemptions locks funds for redemption. It is an error if there // is insufficient spendable balance. // Part of the AccountLocker interface. -func (w *TokenWallet) ReserveNRedemptions(n uint64, serverVer uint32, maxFeeRate uint64) (uint64, error) { - g := w.gases(serverVer) +func (w *TokenWallet) ReserveNRedemptions(n uint64, assetVer uint32, maxFeeRate uint64) (uint64, error) { + g := w.gases(contractVersion(assetVer)) if g == nil { return 0, fmt.Errorf("no gas table") } @@ -2820,8 +2768,8 @@ func (w *TokenWallet) ReReserveRedemption(req uint64) error { // ReserveNRefunds locks funds for doing refunds. It is an error if there // is insufficient spendable balance. Part of the AccountLocker interface. -func (w *ETHWallet) ReserveNRefunds(n uint64, serverVer uint32, maxFeeRate uint64) (uint64, error) { - g := w.gases(contractVersion(serverVer)) +func (w *ETHWallet) ReserveNRefunds(n uint64, assetVer uint32, maxFeeRate uint64) (uint64, error) { + g := w.gases(contractVersion(assetVer)) if g == nil { return 0, errors.New("no gas table") } @@ -2830,8 +2778,8 @@ func (w *ETHWallet) ReserveNRefunds(n uint64, serverVer uint32, maxFeeRate uint6 // ReserveNRefunds locks funds for doing refunds. It is an error if there // is insufficient spendable balance. Part of the AccountLocker interface. -func (w *TokenWallet) ReserveNRefunds(n uint64, serverVer uint32, maxFeeRate uint64) (uint64, error) { - g := w.gases(contractVersion(serverVer)) +func (w *TokenWallet) ReserveNRefunds(n uint64, assetVer uint32, maxFeeRate uint64) (uint64, error) { + g := w.gases(contractVersion(assetVer)) if g == nil { return 0, errors.New("no gas table") } @@ -3809,9 +3757,7 @@ func (eth *ETHWallet) checkForNewBlocks(ctx context.Context) { bestHash := bestHdr.Hash() // This method is called frequently. Don't hold write lock // unless tip has changed. - eth.tipMtx.RLock() - currentTipHash := eth.currentTip.Hash() - eth.tipMtx.RUnlock() + currentTipHash := eth.tip().Hash() if currentTipHash == bestHash { return } @@ -4506,8 +4452,8 @@ func (w *assetWallet) withContractor(contractVer uint32, f func(contractor) erro } // withTokenContractor runs the provided function with the tokenContractor. -func (w *assetWallet) withTokenContractor(assetID, ver uint32, f func(tokenContractor) error) error { - return w.withContractor(ver, func(c contractor) error { +func (w *assetWallet) withTokenContractor(assetID, contractVer uint32, f func(tokenContractor) error) error { + return w.withContractor(contractVer, func(c contractor) error { tc, is := c.(tokenContractor) if !is { return fmt.Errorf("contractor for %d %T is not a tokenContractor", assetID, c) diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index 7e2642830a..bddc67e818 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -1296,12 +1296,11 @@ func tassetWallet(assetID uint32) (asset.Wallet, *assetWallet, *tMempoolNode, co confirmedNonceAt: new(big.Int), pendingTxs: make([]*extendedWalletTx, 0), txDB: &tTxDB{}, - currentTip: &types.Header{Number: new(big.Int)}, + currentTip: &types.Header{Number: new(big.Int), GasLimit: 30_000_000}, finalizeConfs: txConfsNeededToConfirm, + maxTxFeeGwei: dexeth.GweiFactor, // 1 ETH }, versionedGases: versionedGases, - maxSwapGas: versionedGases[0].Swap, - maxRedeemGas: versionedGases[0].Redeem, log: tLogger.SubLogger(strings.ToUpper(dex.BipIDSymbol(assetID))), assetID: assetID, contractorV0: c, @@ -1931,7 +1930,7 @@ func testFundOrderReturnCoinsFundingCoins(t *testing.T, assetID uint32) { } order := asset.Order{ - Version: fromAsset.Version, + AssetVersion: fromAsset.Version, Value: walletBalanceGwei / 2, MaxSwapCount: 2, MaxFeeRate: fromAsset.MaxFeeRate, @@ -2561,7 +2560,7 @@ func TestPreSwap(t *testing.T) { node.balErr = test.balErr preSwap, err := w.PreSwap(&asset.PreSwapForm{ - Version: assetCfg.Version, + AssetVersion: assetCfg.Version, LotSize: lotSize, Lots: test.lots, MaxFeeRate: assetCfg.MaxFeeRate, @@ -2700,7 +2699,7 @@ func testSwap(t *testing.T, assetID uint32) { if err != nil { t.Fatalf("failed to decode contract data: %v", err) } - if swaps.Version != contractVer { + if contractVersion(swaps.AssetVersion) != contractVer { t.Fatal("wrong contract version") } chkLocator := acToLocator(contractVer, contract, dexeth.GweiToWei(contract.Value), node.addr) @@ -2777,11 +2776,11 @@ func testSwap(t *testing.T, assetID uint32) { } inputs := refreshWalletAndFundCoins(5, []uint64{ethToGwei(2)}, 1) swaps := asset.Swaps{ - Version: assetCfg.Version, - Inputs: inputs, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + AssetVersion: assetCfg.Version, + Inputs: inputs, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("error initialize but no send", swaps, true) node.tContractor.initErr = nil @@ -2798,22 +2797,22 @@ func testSwap(t *testing.T, assetID uint32) { inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2)}, 1) swaps = asset.Swaps{ - Version: assetCfg.Version, - Inputs: inputs, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + AssetVersion: assetCfg.Version, + Inputs: inputs, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("one contract, don't lock change", swaps, false) // Test one contract with locking change inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2)}, 1) swaps = asset.Swaps{ - Version: assetCfg.Version, - Inputs: inputs, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: true, + AssetVersion: assetCfg.Version, + Inputs: inputs, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: true, } testSwap("one contract, lock change", swaps, false) @@ -2834,33 +2833,33 @@ func testSwap(t *testing.T, assetID uint32) { } inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(3)}, 2) swaps = asset.Swaps{ - Version: assetCfg.Version, - Inputs: inputs, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + AssetVersion: assetCfg.Version, + Inputs: inputs, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("two contracts", swaps, false) // Test error when funding coins are not enough to cover swaps inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(1)}, 2) swaps = asset.Swaps{ - Version: assetCfg.Version, - Inputs: inputs, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + AssetVersion: assetCfg.Version, + Inputs: inputs, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("funding coins not enough balance", swaps, true) // Ensure when funds are exactly the same as required works properly inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2) + (2 * 200 * dexeth.InitGas(1, 0))}, 2) swaps = asset.Swaps{ - Inputs: inputs, - Version: assetCfg.Version, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + Inputs: inputs, + AssetVersion: assetCfg.Version, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("exact change", swaps, false) @@ -2875,11 +2874,11 @@ func testSwap(t *testing.T, assetID uint32) { inputs = refreshWalletAndFundCoins(5, []uint64{ethToGwei(2) + (2 * 200 * dexeth.InitGas(1, 1))}, 2) swaps = asset.Swaps{ - Inputs: inputs, - Version: assetCfg.Version, - Contracts: contracts, - FeeRate: assetCfg.MaxFeeRate, - LockChange: false, + Inputs: inputs, + AssetVersion: assetCfg.Version, + Contracts: contracts, + FeeRate: assetCfg.MaxFeeRate, + LockChange: false, } testSwap("v1", swaps, false) } @@ -2889,7 +2888,7 @@ func TestPreRedeem(t *testing.T) { defer shutdown() form := &asset.PreRedeemForm{ - Version: tETHV0.Version, + AssetVersion: tETHV0.Version, Lots: 5, FeeSuggestion: 100, } @@ -2907,7 +2906,7 @@ func TestPreRedeem(t *testing.T) { w, _, _, shutdown2 := tassetWallet(usdcTokenID) defer shutdown2() - form.Version = tTokenV0.Version + form.AssetVersion = tTokenV0.Version node.tokenContractor.allow = unlimitedAllowanceReplenishThreshold preRedeem, err = w.PreRedeem(form) @@ -3870,7 +3869,9 @@ func TestSwapConfirmation(t *testing.T) { state := &dexeth.SwapState{ Value: dexeth.GweiToWei(1), } - hdr := &types.Header{} + hdr := &types.Header{ + GasLimit: 30_000_000, + } node.tContractor.swapMap[secretHash] = state @@ -4885,70 +4886,6 @@ func testEstimateSendTxFee(t *testing.T, assetID uint32) { } } -// This test will fail if new versions of the eth or the test token -// contract (that require more gas) are added. -func TestMaxSwapRedeemLots(t *testing.T) { - t.Run("eth", func(t *testing.T) { testMaxSwapRedeemLots(t, BipID) }) - t.Run("token", func(t *testing.T) { testMaxSwapRedeemLots(t, usdcTokenID) }) -} - -func testMaxSwapRedeemLots(t *testing.T, assetID uint32) { - drv := &Driver{} - logger := dex.StdOutLogger("ETHTEST", dex.LevelOff) - tmpDir := t.TempDir() - - settings := map[string]string{providersKey: "a.ipc"} - err := CreateEVMWallet(dexeth.ChainIDs[dex.Testnet], &asset.CreateWalletParams{ - Type: walletTypeRPC, - Seed: encode.RandomBytes(32), - Pass: encode.RandomBytes(32), - Settings: settings, - DataDir: tmpDir, - Net: dex.Testnet, - Logger: logger, - }, &testnetCompatibilityData, true) - if err != nil { - t.Fatalf("CreateEVMWallet error: %v", err) - } - - wallet, err := drv.Open(&asset.WalletConfig{ - Type: walletTypeRPC, - Settings: settings, - DataDir: tmpDir, - }, logger, dex.Testnet) - if err != nil { - t.Fatalf("driver open error: %v", err) - } - - if assetID != BipID { - eth, _ := wallet.(*ETHWallet) - eth.net = dex.Simnet - wallet, err = eth.OpenTokenWallet(&asset.TokenConfig{ - AssetID: assetID, - }) - if err != nil { - t.Fatal(err) - } - } - - info := wallet.Info() - if assetID == BipID { - if info.MaxSwapsInTx != 28 { - t.Fatalf("expected 28 for max swaps but got %d", info.MaxSwapsInTx) - } - if info.MaxRedeemsInTx != 63 { - t.Fatalf("expected 63 for max redemptions but got %d", info.MaxRedeemsInTx) - } - } else { - if info.MaxSwapsInTx != 20 { - t.Fatalf("expected 20 for max swaps but got %d", info.MaxSwapsInTx) - } - if info.MaxRedeemsInTx != 45 { - t.Fatalf("expected 45 for max redemptions but got %d", info.MaxRedeemsInTx) - } - } -} - func TestSwapOrRedemptionFeesPaid(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/client/asset/eth/multirpc_live_test.go b/client/asset/eth/multirpc_live_test.go index 28cff731e9..99d6b4e6ce 100644 --- a/client/asset/eth/multirpc_live_test.go +++ b/client/asset/eth/multirpc_live_test.go @@ -119,3 +119,11 @@ func TestMainnetCompliance(t *testing.T) { func TestReceiptsHaveEffectiveGasPrice(t *testing.T) { mt.TestReceiptsHaveEffectiveGasPrice(t) } + +func TestBlockStats(t *testing.T) { + mt.BlockStats(t, 5, 1024, dex.Mainnet) +} + +func TestTestnetBlockStats(t *testing.T) { + mt.BlockStats(t, 5, 1024, dex.Testnet) +} diff --git a/client/asset/eth/multirpc_test_util.go b/client/asset/eth/multirpc_test_util.go index 6bae75c605..0a51c1163f 100644 --- a/client/asset/eth/multirpc_test_util.go +++ b/client/asset/eth/multirpc_test_util.go @@ -389,30 +389,32 @@ func (m *MRPCTest) FeeHistory(t *testing.T, net dex.Network, blockTimeSecs, days }) } -func (m *MRPCTest) TipCaps(t *testing.T, net dex.Network) { +func (m *MRPCTest) BlockStats(t *testing.T, steps, skipN int, net dex.Network) { m.withClient(t, net, func(ctx context.Context, cl *multiRPCClient) { if err := cl.withAny(ctx, func(ctx context.Context, p *provider) error { blk, err := p.ec.BlockByNumber(ctx, nil) if err != nil { return err } - h := blk.Number() - const m = 20 // how many txs - var n int - for { + tip := blk.Number() + for step := 0; step < steps; step++ { + if step != 0 { + blk, err = p.ec.BlockByNumber(ctx, tip.Add(tip, big.NewInt(int64(-skipN*step)))) + if err != nil { + return err + } + } txs := blk.Transactions() - fmt.Printf("##### Block %d has %d transactions \n", h, len(txs)) + fmt.Printf("##### Block %d, %d transactions, gas limit = %d \n", blk.Number(), len(txs), blk.GasLimit()) + const maxTxs = 10 + var n int for _, tx := range txs { - n++ + fmt.Println("##### Tx tip cap =", fmtFee(tx.GasTipCap())) - } - if n >= m { - break - } - h.Add(h, big.NewInt(-1)) - blk, err = p.ec.BlockByNumber(ctx, h) - if err != nil { - return fmt.Errorf("error getting block %d: %w", h, err) + n++ + if n >= maxTxs { + break + } } } diff --git a/client/asset/interface.go b/client/asset/interface.go index 68af08d7f4..3f3b81e546 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -325,12 +325,6 @@ type WalletInfo struct { // UnitInfo is the information about unit names and conversion factors for // the asset. UnitInfo dex.UnitInfo `json:"unitinfo"` - // MaxSwapsInTx is the max amount of swaps that this wallet can do in a - // single transaction. - MaxSwapsInTx uint64 - // MaxRedeemsInTx is the max amount of redemptions that this wallet can do - // in a single transaction. - MaxRedeemsInTx uint64 // IsAccountBased should be set to true for account-based (EVM) assets, so // that a common seed will be generated and wallets will generate the // same address. @@ -1340,8 +1334,8 @@ type AuditInfo struct { // Swaps is the details needed to broadcast a swap contract(s). type Swaps struct { - // Version is the asset version. - Version uint32 + // AssetVersion is the server's asset version.`` + AssetVersion uint32 // Inputs are the Coins being spent. Inputs Coins // Contract is the contract data. @@ -1392,9 +1386,9 @@ type RedeemForm struct { // Order is order details needed for FundOrder. type Order struct { - // Version is the asset version of the "from" asset with the init + // AssetVersion is the asset version of the "from" asset with the init // transaction. - Version uint32 + AssetVersion uint32 // Value is the amount required to satisfy the order. The Value does not // include fees. Fees will be calculated internally based on the number of // possible swaps (MaxSwapCount) and the exchange's configuration @@ -1478,6 +1472,12 @@ type GeocodeRedeemer interface { RedeemGeocode(code []byte, msg string) (dex.Bytes, uint64, error) } +// MaxMatchesCounter counts the maximum number of matches that can go in a tx. +type MaxMatchesCounter interface { + MaxSwaps(serverVer uint32, feeRate uint64) (int, error) + MaxRedeems(serverVer uint32) (int, error) +} + // WalletNotification can be any asynchronous information the wallet needs // to convey. type WalletNotification any diff --git a/client/asset/polygon/multirpc_live_test.go b/client/asset/polygon/multirpc_live_test.go index 551be982b0..fa9d62bd92 100644 --- a/client/asset/polygon/multirpc_live_test.go +++ b/client/asset/polygon/multirpc_live_test.go @@ -110,16 +110,16 @@ func TestTestnetFees(t *testing.T) { mt.FeeHistory(t, dex.Testnet, 3, 90) } -func TestTestnetTipCaps(t *testing.T) { - mt.TipCaps(t, dex.Testnet) +func TestBlockStats(t *testing.T) { + mt.BlockStats(t, 5, 1024, dex.Mainnet) } -func TestFees(t *testing.T) { - mt.FeeHistory(t, dex.Mainnet, 3, 365) +func TestTestnetBlockStats(t *testing.T) { + mt.BlockStats(t, 5, 1024, dex.Testnet) } -func TestTipCaps(t *testing.T) { - mt.TipCaps(t, dex.Mainnet) +func TestFees(t *testing.T) { + mt.FeeHistory(t, dex.Mainnet, 3, 365) } func TestReceiptsHaveEffectiveGasPrice(t *testing.T) { diff --git a/client/asset/polygon/polygon.go b/client/asset/polygon/polygon.go index 3dd3a390ec..4574a79dbe 100644 --- a/client/asset/polygon/polygon.go +++ b/client/asset/polygon/polygon.go @@ -12,6 +12,7 @@ import ( "decred.org/dcrdex/client/asset" "decred.org/dcrdex/client/asset/eth" "decred.org/dcrdex/dex" + dexeth "decred.org/dcrdex/dex/networks/eth" dexpolygon "decred.org/dcrdex/dex/networks/polygon" "github.com/ethereum/go-ethereum/common" ) @@ -84,8 +85,6 @@ var ( Seeded: true, NoAuth: true, }, - // MaxSwapsInTx and MaxRedeemsInTx are set in (Wallet).Info, since - // the value cannot be known until we connect and get network info. }, IsAccountBased: true, } @@ -152,6 +151,7 @@ func (d *Driver) Open(cfg *asset.WalletConfig, logger dex.Logger, net dex.Networ WalletInfo: WalletInfo, Net: net, DefaultProviders: defaultProviders, + MaxTxFeeGwei: 1000 * dexeth.GweiFactor, // 1000 POL }) } diff --git a/client/asset/zec/zec_test.go b/client/asset/zec/zec_test.go index 7795d32073..a3fef741be 100644 --- a/client/asset/zec/zec_test.go +++ b/client/asset/zec/zec_test.go @@ -538,7 +538,7 @@ func TestFundOrder(t *testing.T) { } ord := &asset.Order{ - Version: version, + AssetVersion: version, Value: 0, MaxSwapCount: 1, Options: make(map[string]string), @@ -1335,10 +1335,10 @@ func TestPreSwap(t *testing.T) { } form := &asset.PreSwapForm{ - Version: version, - LotSize: tLotSize, - Lots: lots, - Immediate: false, + AssetVersion: version, + LotSize: tLotSize, + Lots: lots, + Immediate: false, } setFunds(minReq) diff --git a/client/core/core.go b/client/core/core.go index f3572b99d2..73347f2c4e 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -5077,7 +5077,7 @@ func (c *Core) MaxBuy(host string, baseID, quoteID uint32, rate uint64) (*MaxOrd } preRedeem, err := baseWallet.PreRedeem(&asset.PreRedeemForm{ - Version: baseAsset.Version, + AssetVersion: baseAsset.Version, Lots: maxBuy.Lots, FeeSuggestion: redeemFeeSuggestion, }) @@ -5136,7 +5136,7 @@ func (c *Core) MaxSell(host string, base, quote uint32) (*MaxOrderEstimate, erro } preRedeem, err := quoteWallet.PreRedeem(&asset.PreRedeemForm{ - Version: quoteAsset.Version, + AssetVersion: quoteAsset.Version, Lots: maxSell.Lots, FeeSuggestion: redeemFeeSuggestion, }) @@ -5722,7 +5722,7 @@ func (c *Core) PreOrder(form *TradeForm) (*OrderEstimate, error) { } swapEstimate, err := wallets.fromWallet.PreSwap(&asset.PreSwapForm{ - Version: assetConfigs.fromAsset.Version, + AssetVersion: assetConfigs.fromAsset.Version, LotSize: swapLotSize, Lots: lots, MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, @@ -5737,7 +5737,7 @@ func (c *Core) PreOrder(form *TradeForm) (*OrderEstimate, error) { } redeemEstimate, err := wallets.toWallet.PreRedeem(&asset.PreRedeemForm{ - Version: assetConfigs.toAsset.Version, + AssetVersion: assetConfigs.toAsset.Version, Lots: lots, FeeSuggestion: redeemFeeSuggestion, SelectedOptions: form.Options, @@ -6235,7 +6235,7 @@ func (c *Core) prepareTradeRequest(pw []byte, form *TradeForm) (*tradeRequest, e } coins, redeemScripts, fundingFees, err := fromWallet.FundOrder(&asset.Order{ - Version: assetConfigs.fromAsset.Version, + AssetVersion: assetConfigs.fromAsset.Version, Value: fundQty, MaxSwapCount: lots, MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, diff --git a/client/core/core_test.go b/client/core/core_test.go index d470d38b54..fdd473a4bd 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -715,6 +715,7 @@ type TXCWallet struct { reserves atomic.Uint64 findBond *asset.BondDetails findBondErr error + maxSwaps, maxRedeems int confirmRedemptionResult *asset.ConfirmRedemptionStatus confirmRedemptionErr error @@ -1122,6 +1123,15 @@ func (w *TXCWallet) WalletTransaction(context.Context, dex.Bytes) (*asset.Wallet return nil, nil } +var _ asset.MaxMatchesCounter = (*TXCWallet)(nil) + +func (w *TXCWallet) MaxSwaps(serverVer uint32, feeRate uint64) (int, error) { + return w.maxSwaps, nil +} +func (w *TXCWallet) MaxRedeems(serverVer uint32) (int, error) { + return w.maxRedeems, nil +} + type TAccountLocker struct { *TXCWallet reserveNRedemptions uint64 @@ -9148,8 +9158,8 @@ func TestMaxSwapsRedeemsInTx(t *testing.T) { tCore.wallets[tUTXOAssetB.ID] = btcWallet walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) - tDcrWallet.info.MaxSwapsInTx = 4 - tBtcWallet.info.MaxRedeemsInTx = 4 + tDcrWallet.maxSwaps = 4 + tDcrWallet.maxRedeems = 4 lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, 0, 0) oid := lo.ID() @@ -9213,7 +9223,7 @@ func TestMaxSwapsRedeemsInTx(t *testing.T) { t.Helper() for i := range expected { if expected[i] != len(wallet.lastRedeems[i].Redemptions) { - t.Fatalf("expected %d swaps but got %d", expected[i], len(wallet.lastRedeems[i].Redemptions)) + t.Fatalf("expected %d redeems but got %d", expected[i], len(wallet.lastRedeems[i].Redemptions)) } } } diff --git a/client/core/trade.go b/client/core/trade.go index e69489b8d0..15bd2a09af 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -2284,7 +2284,7 @@ func (t *trackedTrade) revokeMatch(matchID order.MatchID, fromServer bool) error // // This method modifies match fields and MUST be called with the trackedTrade // mutex lock held for writes. -func (c *Core) swapMatches(t *trackedTrade, matches []*matchTracker) error { +func (c *Core) swapMatches(t *trackedTrade, matches []*matchTracker) (err error) { errs := newErrorSet("swapMatches order %s - ", t.ID()) groupables := make([]*matchTracker, 0, len(matches)) // Over-allocating if there are suspect matches var suspects []*matchTracker @@ -2295,36 +2295,75 @@ func (c *Core) swapMatches(t *trackedTrade, matches []*matchTracker) error { groupables = append(groupables, m) } } + feeRate := t.bestSwapGroupFeeRate(matches) if len(groupables) > 0 { - maxSwapsInTx := int(t.wallets.fromWallet.Info().MaxSwapsInTx) + var maxSwapsInTx int + if counter, is := t.wallets.fromWallet.Wallet.(asset.MaxMatchesCounter); is { + if maxSwapsInTx, err = counter.MaxSwaps(t.metaData.FromVersion, feeRate); err != nil { + t.dc.log.Meter("failed_swap_count", time.Minute*10).Warn("Failed to count swap txs: %v", err) + } + } if maxSwapsInTx <= 0 || len(groupables) < maxSwapsInTx { - c.swapMatchGroup(t, groupables, errs) + c.swapMatchGroup(t, groupables, feeRate, errs) } else { for i := 0; i < len(groupables); i += maxSwapsInTx { if i+maxSwapsInTx < len(groupables) { - c.swapMatchGroup(t, groupables[i:i+maxSwapsInTx], errs) + c.swapMatchGroup(t, groupables[i:i+maxSwapsInTx], feeRate, errs) } else { - c.swapMatchGroup(t, groupables[i:], errs) + c.swapMatchGroup(t, groupables[i:], feeRate, errs) } } } } for _, m := range suspects { - c.swapMatchGroup(t, []*matchTracker{m}, errs) + c.swapMatchGroup(t, []*matchTracker{m}, feeRate, errs) } return errs.ifAny() } +// bestSwapGroupRate gets the most appropriate fee rate for a group of swaps. +func (t *trackedTrade) bestSwapGroupFeeRate(matches []*matchTracker) uint64 { + var highestFeeRate uint64 + for _, match := range matches { + if match.FeeRateSwap > highestFeeRate { + highestFeeRate = match.FeeRateSwap + } + } + // Use a higher swap fee rate if a local estimate is higher than the + // prescribed rate, but not higher than the funded (max) rate. + if highestFeeRate < t.metaData.MaxFeeRate { + freshRate := t.wallets.fromWallet.feeRate() + if freshRate == 0 { // either not a FeeRater, or FeeRate failed + freshRate = t.dc.bestBookFeeSuggestion(t.wallets.fromWallet.AssetID) + } + if freshRate > t.metaData.MaxFeeRate { + freshRate = t.metaData.MaxFeeRate + } + if highestFeeRate < freshRate { + t.dc.log.Infof("Prescribed %v fee rate %v looks low, using %v", + t.wallets.fromWallet.Symbol, highestFeeRate, freshRate) + highestFeeRate = freshRate + } + } + return highestFeeRate +} + // swapMatchGroup will send a transaction with swap outputs for the specified // matches. // // This method modifies match fields and MUST be called with the trackedTrade // mutex lock held for writes. -func (c *Core) swapMatchGroup(t *trackedTrade, matches []*matchTracker, errs *errorSet) { +func (c *Core) swapMatchGroup(t *trackedTrade, matches []*matchTracker, highestFeeRate uint64, errs *errorSet) { + // Ensure swap is not sent with a zero fee rate. + if highestFeeRate == 0 { + errs.add("swap cannot proceed with a zero fee rate") + return + } + // Prepare the asset.Contracts. contracts := make([]*asset.Contract, len(matches)) // These matches may have different fee rates, matched in different epochs. - var highestFeeRate uint64 + for i, match := range matches { value := match.Quantity if !match.trade.Sell { @@ -2345,10 +2384,6 @@ func (c *Core) swapMatchGroup(t *trackedTrade, matches []*matchTracker, errs *er SecretHash: match.MetaData.Proof.SecretHash, LockTime: uint64(lockTime), } - - if match.FeeRateSwap > highestFeeRate { - highestFeeRate = match.FeeRateSwap - } } lockChange := true @@ -2396,41 +2431,18 @@ func (c *Core) swapMatchGroup(t *trackedTrade, matches []*matchTracker, errs *er return } - // Use a higher swap fee rate if a local estimate is higher than the - // prescribed rate, but not higher than the funded (max) rate. - if highestFeeRate < t.metaData.MaxFeeRate { - freshRate := fromWallet.feeRate() - if freshRate == 0 { // either not a FeeRater, or FeeRate failed - freshRate = t.dc.bestBookFeeSuggestion(fromWallet.AssetID) - } - if freshRate > t.metaData.MaxFeeRate { - freshRate = t.metaData.MaxFeeRate - } - if highestFeeRate < freshRate { - c.log.Infof("Prescribed %v fee rate %v looks low, using %v", - fromWallet.Symbol, highestFeeRate, freshRate) - highestFeeRate = freshRate - } - } - - // Ensure swap is not sent with a zero fee rate. - if highestFeeRate == 0 { - errs.add("swap cannot proceed with a zero fee rate") - return - } - // swapMatches is no longer idempotent after this point. // Send the swap. If the swap fails, set the swapErr flag for all matches. // A more sophisticated solution might involve tracking the error time too // and trying again in certain circumstances. swaps := &asset.Swaps{ - Version: t.metaData.FromVersion, - Inputs: inputs, - Contracts: contracts, - FeeRate: highestFeeRate, - LockChange: lockChange, - Options: t.options, + AssetVersion: t.metaData.FromVersion, + Inputs: inputs, + Contracts: contracts, + FeeRate: highestFeeRate, + LockChange: lockChange, + Options: t.options, } receipts, change, fees, err := fromWallet.Swap(swaps) if err != nil { @@ -2659,7 +2671,7 @@ func (c *Core) sendInitAsync(t *trackedTrade, match *matchTracker, coinID, contr // // This method modifies match fields and MUST be called with the trackedTrade // mutex lock held for writes. -func (c *Core) redeemMatches(t *trackedTrade, matches []*matchTracker) error { +func (c *Core) redeemMatches(t *trackedTrade, matches []*matchTracker) (err error) { errs := newErrorSet("redeemMatches order %s - ", t.ID()) groupables := make([]*matchTracker, 0, len(matches)) // Over-allocating if there are suspect matches var suspects []*matchTracker @@ -2674,7 +2686,12 @@ func (c *Core) redeemMatches(t *trackedTrade, matches []*matchTracker) error { if !t.wallets.toWallet.connected() { return errWalletNotConnected // don't ungroup, just return } - maxRedeemsInTx := int(t.wallets.toWallet.Info().MaxRedeemsInTx) + var maxRedeemsInTx int + if counter, is := t.wallets.fromWallet.Wallet.(asset.MaxMatchesCounter); is { + if maxRedeemsInTx, err = counter.MaxRedeems(t.metaData.FromVersion); err != nil { + t.dc.log.Meter("failed_redeem_count", time.Minute*10).Warn("Failed to count redeem txs: %v", err) + } + } if maxRedeemsInTx <= 0 || len(groupables) < maxRedeemsInTx { c.redeemMatchGroup(t, groupables, errs) } else {