diff --git a/src/server/blockchain/CeloAdminWallet.js b/src/server/blockchain/CeloAdminWallet.js index 5048136e..02089bed 100644 --- a/src/server/blockchain/CeloAdminWallet.js +++ b/src/server/blockchain/CeloAdminWallet.js @@ -5,8 +5,8 @@ const { celo, env } = conf const options = { ethereum: celo, network: `${env}-celo`, - maxFeePerGas: undefined, // will force use of estimatefees - maxPriorityFeePerGas: (2e8).toFixed(0), + maxFeePerGas: (50e9).toFixed(0), // floor to stay above Celo base fee spikes + maxPriorityFeePerGas: (1e9).toFixed(0), // floor for tips fetchGasPrice: false, faucetTxCost: 250000 } diff --git a/src/server/blockchain/Web3Wallet.js b/src/server/blockchain/Web3Wallet.js index 5dd63487..2c983195 100644 --- a/src/server/blockchain/Web3Wallet.js +++ b/src/server/blockchain/Web3Wallet.js @@ -1203,6 +1203,18 @@ export class Web3Wallet { } } + pickFeeWithFloor(value: string | number | void, floor: string | number | void) { + if (value === null || value === undefined) { + return floor + } + + if (floor === null || floor === undefined) { + return value + } + + return BigInt(value.toString()) < BigInt(floor.toString()) ? floor : value + } + /** * Normalize gas pricing parameters - handles legacy gasPrice vs EIP-1559 fees * Deduplicates gas pricing logic used in sendTransaction and sendNative @@ -1233,24 +1245,36 @@ export class Web3Wallet { }) return { gasPrice, maxFeePerGas: undefined, maxPriorityFeePerGas: undefined } } else { - // Network supports EIP-1559, process EIP-1559 fees - // Use instance defaults if not provided - maxFeePerGas = maxFeePerGas !== undefined ? maxFeePerGas : this.maxFeePerGas - maxPriorityFeePerGas = maxPriorityFeePerGas !== undefined ? maxPriorityFeePerGas : this.maxPriorityFeePerGas + // Network supports EIP-1559, process EIP-1559 fees. + // Instance values are treated as floors so live provider estimates can rise above them. + const maxFeeFloor = maxFeePerGas !== undefined ? maxFeePerGas : this.maxFeePerGas + const maxPriorityFloor = maxPriorityFeePerGas !== undefined ? maxPriorityFeePerGas : this.maxPriorityFeePerGas + + const needsEstimation = !maxFeePerGas || !maxPriorityFeePerGas + + if (needsEstimation) { + try { + const { baseFee, priorityFee } = await this.getFeeEstimates() + maxFeePerGas = this.pickFeeWithFloor(baseFee, maxFeeFloor) + maxPriorityFeePerGas = this.pickFeeWithFloor(priorityFee, maxPriorityFloor) + } catch (error) { + logger.warn('Failed to estimate EIP-1559 fees, falling back to configured values', { + error: error.message, + network: this.network, + networkId: this.networkId + }) + maxFeePerGas = maxFeeFloor + maxPriorityFeePerGas = maxPriorityFloor + } + } else { + maxFeePerGas = maxFeeFloor + maxPriorityFeePerGas = maxPriorityFloor + } // Convert to numbers for comparison let maxFeeNum = toNumber(maxFeePerGas) let maxPriorityNum = toNumber(maxPriorityFeePerGas) - // Fetch fee estimates if EIP-1559 fees are not provided or invalid - if (!maxFeeNum || !maxPriorityNum) { - const { baseFee, priorityFee } = await this.getFeeEstimates() - maxFeePerGas = maxFeeNum || baseFee - maxPriorityFeePerGas = maxPriorityNum || priorityFee - maxFeeNum = toNumber(maxFeePerGas) - maxPriorityNum = toNumber(maxPriorityFeePerGas) - } - // Ensure maxFeePerGas >= maxPriorityFeePerGas (EIP-1559 requirement) if (maxPriorityNum > 0 && (maxFeeNum === 0 || maxFeeNum < maxPriorityNum)) { logger.warn('maxFeePerGas < maxPriorityFeePerGas or is 0, adjusting maxFeePerGas', { diff --git a/src/server/blockchain/__tests__/web3wallet.gas.test.js b/src/server/blockchain/__tests__/web3wallet.gas.test.js new file mode 100644 index 00000000..76522953 --- /dev/null +++ b/src/server/blockchain/__tests__/web3wallet.gas.test.js @@ -0,0 +1,103 @@ +jest.mock('../../db/mongo-db') + +import { Web3Wallet } from '../Web3Wallet' + +const createTestWallet = options => + new Web3Wallet( + 'TestWallet', + { + env: 'test', + mnemonic: '', + network: 'test', + kmsEnabled: false, + fuse: { + network_id: 42220, + httpWeb3Provider: 'http://localhost:8545' + } + }, + { + ethereum: { + network_id: 42220, + httpWeb3Provider: 'http://localhost:8545' + }, + network: 'test-celo', + ...options + }, + false + ) + +describe('Web3Wallet gas pricing', () => { + const logger = { + debug: jest.fn(), + warn: jest.fn() + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + test('prefers provider EIP-1559 fees when they are above configured floors', async () => { + const wallet = createTestWallet({ + maxFeePerGas: '40000000000', + maxPriorityFeePerGas: '1000000000' + }) + + wallet.supportsEIP1559 = jest.fn().mockResolvedValue(true) + wallet.getFeeEstimates = jest.fn().mockResolvedValue({ + baseFee: 52000000000, + priorityFee: 2000000000 + }) + + const gas = await wallet.normalizeGasPricing({}, logger) + + expect(gas).toEqual({ + gasPrice: undefined, + maxFeePerGas: 52000000000, + maxPriorityFeePerGas: 2000000000 + }) + }) + + test('uses configured floors when provider underbids Celo fees', async () => { + const wallet = createTestWallet({ + maxFeePerGas: '40000000000', + maxPriorityFeePerGas: '1000000000' + }) + + wallet.supportsEIP1559 = jest.fn().mockResolvedValue(true) + wallet.getFeeEstimates = jest.fn().mockResolvedValue({ + baseFee: 30000000000, + priorityFee: 200000000 + }) + + const gas = await wallet.normalizeGasPricing({}, logger) + + expect(gas).toEqual({ + gasPrice: undefined, + maxFeePerGas: '40000000000', + maxPriorityFeePerGas: '1000000000' + }) + }) + + test('falls back to configured floors when fee estimation fails', async () => { + const wallet = createTestWallet({ + maxFeePerGas: '40000000000', + maxPriorityFeePerGas: '1000000000' + }) + + wallet.supportsEIP1559 = jest.fn().mockResolvedValue(true) + wallet.getFeeEstimates = jest.fn().mockRejectedValue(new Error('rpc failure')) + + const gas = await wallet.normalizeGasPricing({}, logger) + + expect(gas).toEqual({ + gasPrice: undefined, + maxFeePerGas: '40000000000', + maxPriorityFeePerGas: '1000000000' + }) + expect(logger.warn).toHaveBeenCalledWith('Failed to estimate EIP-1559 fees, falling back to configured values', { + error: 'rpc failure', + network: 'test-celo', + networkId: 42220 + }) + }) +})