diff --git a/src/_tests/unit/qihdwallet-check-gas-limit.unit.test.ts b/src/_tests/unit/qihdwallet-check-gas-limit.unit.test.ts new file mode 100644 index 00000000..82393f9c --- /dev/null +++ b/src/_tests/unit/qihdwallet-check-gas-limit.unit.test.ts @@ -0,0 +1,126 @@ +import assert from 'assert'; +import { QiHDWallet } from '../../wallet/qi-hdwallet.js'; +import { Mnemonic } from '../../wallet/mnemonic.js'; +import { Zone } from '../../constants/zones.js'; +import { MockProvider } from './mockProvider.js'; +import { QiTransaction } from '../../transaction/index.js'; +import { Block } from '../../providers/index.js'; + +class TestQiHDWallet extends QiHDWallet { + public async checkGasLimit(tx: QiTransaction, zone: Zone): Promise { + return this['_checkGasLimit'](tx, zone); + } +} + +interface GasLimitTestCase { + name: string; + mnemonic: string; + zone: Zone; + blockGasLimit: bigint; + estimatedGas: bigint; + expectedResult: boolean; +} + +const testMnemonic = 'test test test test test test test test test test test junk'; + +describe('QiHDWallet: Gas Limit Tests', () => { + const testCases: GasLimitTestCase[] = [ + { + name: 'Gas limit is sufficient (well below 90%)', + mnemonic: testMnemonic, + zone: Zone.Cyprus1, + blockGasLimit: BigInt(30000), + estimatedGas: BigInt(21000), // 70% of block gas limit + expectedResult: true, + }, + { + name: 'Gas limit is insufficient (above 90%)', + mnemonic: testMnemonic, + zone: Zone.Cyprus1, + blockGasLimit: BigInt(20000), + estimatedGas: BigInt(19000), // 95% of block gas limit + expectedResult: false, + }, + { + name: 'Gas limit exactly at 90%', + mnemonic: testMnemonic, + zone: Zone.Cyprus1, + blockGasLimit: BigInt(20000), + estimatedGas: BigInt(18000), // exactly 90% of block gas limit + expectedResult: true, + }, + { + name: 'Gas limit slightly below 90%', + mnemonic: testMnemonic, + zone: Zone.Cyprus1, + blockGasLimit: BigInt(20000), + estimatedGas: BigInt(17900), // 89.5% of block gas limit + expectedResult: true, + }, + { + name: 'Gas limit slightly above 90%', + mnemonic: testMnemonic, + zone: Zone.Cyprus1, + blockGasLimit: BigInt(20000), + estimatedGas: BigInt(18100), // 90.5% of block gas limit + expectedResult: false, + }, + ]; + + testCases.forEach((testCase) => { + it(testCase.name, async () => { + const mnemonic = Mnemonic.fromPhrase(testCase.mnemonic); + const wallet = TestQiHDWallet.fromMnemonic(mnemonic); + + const mockProvider = new MockProvider({ network: BigInt(1) }); + + mockProvider.getBlock = async () => { + return { + header: { + gasLimit: testCase.blockGasLimit, + }, + } as Block; + }; + + mockProvider.estimateGas = async () => { + return testCase.estimatedGas; + }; + + wallet.connect(mockProvider); + + const tx = new QiTransaction(); + + const result = await wallet.checkGasLimit(tx, testCase.zone); + assert.equal( + result, + testCase.expectedResult, + `Expected gas limit check to return ${testCase.expectedResult} but got ${result}`, + ); + }); + }); + + it('should throw error when provider is not set', async () => { + const mnemonic = Mnemonic.fromPhrase(testMnemonic); + const wallet = TestQiHDWallet.fromMnemonic(mnemonic); + const tx = new QiTransaction(); + + await assert.rejects(async () => await wallet.checkGasLimit(tx, Zone.Cyprus1), { + message: 'Provider is not set', + }); + }); + + it('should throw error when block cannot be retrieved', async () => { + const mnemonic = Mnemonic.fromPhrase(testMnemonic); + const wallet = TestQiHDWallet.fromMnemonic(mnemonic); + const mockProvider = new MockProvider({ network: BigInt(1) }); + + mockProvider.getBlock = async () => null; + + wallet.connect(mockProvider); + const tx = new QiTransaction(); + + await assert.rejects(async () => await wallet.checkGasLimit(tx, Zone.Cyprus1), { + message: 'Failed to get the current block', + }); + }); +}); diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts index ccb30aa7..450a5728 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -715,6 +715,11 @@ export class QiHDWallet extends AbstractHDWallet { Number(chainId), ); + // verify tx gas is under block gas limit + if (await this._checkGasLimit(tx, zone)) { + throw new Error('Transaction gas limit exceeds block gas limit'); + } + // Sign the transaction const signedTx = await this.signTransaction(tx); @@ -895,6 +900,11 @@ export class QiHDWallet extends AbstractHDWallet { Number(chainId), ); + // verify tx gas is under block gas limit + if (await this._checkGasLimit(tx, originZone)) { + throw new Error('Transaction gas limit exceeds block gas limit'); + } + // Sign the transaction const signedTx = await this.signTransaction(tx); // Broadcast the transaction to the network using the provider @@ -989,6 +999,31 @@ export class QiHDWallet extends AbstractHDWallet { txOut, }; } + /** + * Checks if the estimated gas for a transaction is within the current block's gas limit. + * + * @private + * @param {QiTransaction} tx - The Qi transaction to check + * @param {Zone} zone - The zone where the transaction will be executed + * @returns {Promise} Returns true if the estimated gas is within block limit, false otherwise + * @throws {Error} If provider is not set or block cannot be retrieved + */ + private async _checkGasLimit(tx: QiTransaction, zone: Zone): Promise { + if (!this.provider) { + throw new Error('Provider is not set'); + } + const currentBlock = await this.provider.getBlock(toShard(zone), 'latest')!; + if (!currentBlock) { + throw new Error('Failed to get the current block'); + } + + const blockGasLimit = currentBlock.header.gasLimit; + + const txEstimatedGas = await this.provider.estimateGas(tx); + + // return true if txEstimatedGas is 90% or less of blockGasLimit + return txEstimatedGas <= (blockGasLimit * 9n) / 10n; + } /** * Gets a set of unused BIP44 addresses from the specified derivation path. It first checks if there are any unused