|
| 1 | +import { FeeData } from 'ethers'; |
| 2 | +import { ethers } from 'hardhat'; |
| 3 | +import { CHAIN_IDS } from './chainIds'; |
| 4 | + |
| 5 | +/** Transaction override fields accepted by ethers v6 ContractFactory.deploy(). */ |
| 6 | +export type TxOverrides = { |
| 7 | + gasLimit?: number; |
| 8 | + maxFeePerGas?: bigint; |
| 9 | + maxPriorityFeePerGas?: bigint; |
| 10 | + gasPrice?: bigint; |
| 11 | +}; |
| 12 | + |
| 13 | +/** |
| 14 | + * Gas strategies for batcher contract deployment. |
| 15 | + * |
| 16 | + * default |
| 17 | + * Hardhat auto-detects the fee model and estimates gas via eth_estimateGas. |
| 18 | + * Suitable for any chain whose node accepts EIP-1559 (type-2) estimation params. |
| 19 | + * |
| 20 | + * eip1559-fees |
| 21 | + * Hardhat auto-estimates gasLimit, but maxFeePerGas / maxPriorityFeePerGas are |
| 22 | + * supplied explicitly. Use when a chain's eth_feeHistory is unreliable but |
| 23 | + * eth_estimateGas still works correctly with type-2 params. |
| 24 | + * |
| 25 | + * manual-gas |
| 26 | + * Both fee params and gasLimit are supplied explicitly. |
| 27 | + * Required when eth_estimateGas rejects type-2 params (causing Hardhat's internal |
| 28 | + * estimation to hang or fail). Gas is pre-estimated with a plain { from, data } |
| 29 | + * call, which all nodes accept regardless of fee model. |
| 30 | + * Use forceLegacy: true when the node also rejects type-2 send transactions |
| 31 | + * (i.e. it advertises EIP-1559 fee data but only mines legacy type-0 txs). |
| 32 | + */ |
| 33 | +export type GasStrategy = 'default' | 'eip1559-fees' | 'manual-gas'; |
| 34 | + |
| 35 | +type ChainGasConfig = |
| 36 | + | { strategy: 'default' } |
| 37 | + | { strategy: 'eip1559-fees' } |
| 38 | + | { |
| 39 | + strategy: 'manual-gas'; |
| 40 | + /** Minimum priority fee in gwei; prevents zero-tip txs on chains that require it. Default: 1. */ |
| 41 | + minPriorityFeeGwei?: number; |
| 42 | + /** Extra gas added on top of the estimate as a percentage, e.g. 20 = +20%. Default: 20. */ |
| 43 | + gasBufferPct?: number; |
| 44 | + /** Cap the final gasLimit to 95% of the latest block's gas limit. Default: false. */ |
| 45 | + capToBlockGasLimit?: boolean; |
| 46 | + /** |
| 47 | + * Force a legacy type-0 transaction (gasPrice only) even when the node reports |
| 48 | + * EIP-1559 fee data. Required for chains whose mempool advertises EIP-1559 support |
| 49 | + * but silently drops type-2 transactions (e.g. Hemi). |
| 50 | + */ |
| 51 | + forceLegacy?: boolean; |
| 52 | + }; |
| 53 | + |
| 54 | +/** |
| 55 | + * Per-chain gas configuration for batcher contract deployment. |
| 56 | + * Add an entry here whenever a new chain requires non-default gas handling. |
| 57 | + */ |
| 58 | +const CHAIN_GAS_CONFIGS: Partial<Record<number, ChainGasConfig>> = { |
| 59 | + // eip1559-fees — node fee data is unreliable; supply explicit EIP-1559 params, |
| 60 | + // let Hardhat estimate the gasLimit via eth_estimateGas. |
| 61 | + [CHAIN_IDS.MONAD_TESTNET]: { strategy: 'eip1559-fees' }, |
| 62 | + [CHAIN_IDS.WORLD]: { strategy: 'eip1559-fees' }, |
| 63 | + [CHAIN_IDS.WORLD_TESTNET]: { strategy: 'eip1559-fees' }, |
| 64 | + [CHAIN_IDS.SONEIUM]: { strategy: 'eip1559-fees' }, |
| 65 | + [CHAIN_IDS.SONEIUM_TESTNET]: { strategy: 'eip1559-fees' }, |
| 66 | + [CHAIN_IDS.WEMIX]: { strategy: 'eip1559-fees' }, |
| 67 | + [CHAIN_IDS.WEMIX_TESTNET]: { strategy: 'eip1559-fees' }, |
| 68 | + |
| 69 | + // manual-gas — eth_estimateGas hangs or fails with type-2 params on these chains; |
| 70 | + // gas is pre-estimated with a plain { from, data } call and passed explicitly. |
| 71 | + // |
| 72 | + // Hemi: node advertises EIP-1559 fee data but only mines legacy (type-0) txs, |
| 73 | + // so forceLegacy ensures we always send gasPrice instead of maxFeePerGas. |
| 74 | + [CHAIN_IDS.HEMIETH]: { strategy: 'manual-gas', forceLegacy: true }, |
| 75 | + [CHAIN_IDS.HEMIETH_TESTNET]: { strategy: 'manual-gas', forceLegacy: true }, |
| 76 | + // Somnia: block gas limit is low; cap the gasLimit to avoid exceeding it. |
| 77 | + [CHAIN_IDS.SOMNIA]: { strategy: 'manual-gas', capToBlockGasLimit: true }, |
| 78 | + [CHAIN_IDS.SOMNIA_TESTNET]: { |
| 79 | + strategy: 'manual-gas', |
| 80 | + capToBlockGasLimit: true |
| 81 | + } |
| 82 | +}; |
| 83 | + |
| 84 | +export function getChainGasConfig(chainId: number): ChainGasConfig { |
| 85 | + return CHAIN_GAS_CONFIGS[chainId] ?? { strategy: 'default' }; |
| 86 | +} |
| 87 | + |
| 88 | +/** |
| 89 | + * Resolves the final TxOverrides for Batcher.deploy() based on the chain's gas strategy. |
| 90 | + * |
| 91 | + * @param chainId - Current network chain ID |
| 92 | + * @param feeData - Result of provider.getFeeData() |
| 93 | + * @param deployer - The signer that will send the deploy tx |
| 94 | + * @param deployTxReq - The unsigned deploy transaction (from ContractFactory.getDeployTransaction) |
| 95 | + * @param fromAddress - The deployer's address string |
| 96 | + */ |
| 97 | +export async function resolveGasOverrides( |
| 98 | + chainId: number, |
| 99 | + feeData: FeeData, |
| 100 | + deployer: Awaited<ReturnType<typeof ethers.getSigner>>, |
| 101 | + deployTxReq: { data?: string | null }, |
| 102 | + fromAddress: string |
| 103 | +): Promise<TxOverrides | undefined> { |
| 104 | + const config = getChainGasConfig(chainId); |
| 105 | + |
| 106 | + if (config.strategy === 'default') { |
| 107 | + return undefined; |
| 108 | + } |
| 109 | + |
| 110 | + if (config.strategy === 'eip1559-fees') { |
| 111 | + return { |
| 112 | + maxFeePerGas: feeData.maxFeePerGas ?? feeData.gasPrice ?? undefined, |
| 113 | + maxPriorityFeePerGas: |
| 114 | + feeData.maxPriorityFeePerGas ?? feeData.gasPrice ?? undefined |
| 115 | + }; |
| 116 | + } |
| 117 | + |
| 118 | + // manual-gas: pre-estimate gas with a plain { from, data } call (no fee-model params), |
| 119 | + // then pass the result as an explicit gasLimit. This bypasses Hardhat's internal |
| 120 | + // eth_estimateGas, which sends type-2 params and hangs on chains like Hemi and Somnia. |
| 121 | + const minPriorityFee = |
| 122 | + BigInt(config.minPriorityFeeGwei ?? 1) * 1_000_000_000n; |
| 123 | + const gasBufferPct = BigInt(config.gasBufferPct ?? 20); |
| 124 | + |
| 125 | + const has1559 = |
| 126 | + feeData.maxFeePerGas != null && feeData.maxPriorityFeePerGas != null; |
| 127 | + |
| 128 | + let overrides: TxOverrides; |
| 129 | + |
| 130 | + if (has1559 && !config.forceLegacy) { |
| 131 | + const priority = |
| 132 | + (feeData.maxPriorityFeePerGas ?? 0n) > 0n |
| 133 | + ? (feeData.maxPriorityFeePerGas as bigint) |
| 134 | + : minPriorityFee; |
| 135 | + const base = feeData.maxFeePerGas ?? feeData.gasPrice ?? minPriorityFee; |
| 136 | + const maxFee = base > priority * 2n ? base : priority * 2n; |
| 137 | + overrides = { maxFeePerGas: maxFee, maxPriorityFeePerGas: priority }; |
| 138 | + } else { |
| 139 | + overrides = { |
| 140 | + gasPrice: feeData.gasPrice ?? 6_000_000_000n |
| 141 | + }; |
| 142 | + } |
| 143 | + |
| 144 | + // Estimate gas without fee-model params so the call succeeds on nodes that |
| 145 | + // reject type-2 estimation requests. |
| 146 | + const est = await deployer.estimateGas({ |
| 147 | + ...deployTxReq, |
| 148 | + from: fromAddress |
| 149 | + }); |
| 150 | + const estWithBuffer = (est * (100n + gasBufferPct)) / 100n; |
| 151 | + let chosenGasLimit = Number(estWithBuffer); |
| 152 | + |
| 153 | + if (config.capToBlockGasLimit) { |
| 154 | + const latestBlock = await ethers.provider.getBlock('latest'); |
| 155 | + if (latestBlock?.gasLimit) { |
| 156 | + const blockLimit = Number(latestBlock.gasLimit); |
| 157 | + chosenGasLimit = Math.min(chosenGasLimit, Math.floor(blockLimit * 0.95)); |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + overrides.gasLimit = chosenGasLimit; |
| 162 | + return overrides; |
| 163 | +} |
0 commit comments