diff --git a/typescript/src/client/client.ts b/typescript/src/client/client.ts index e639e95..c059139 100644 --- a/typescript/src/client/client.ts +++ b/typescript/src/client/client.ts @@ -213,9 +213,12 @@ export class Client { /** * Estimate gas for a transaction. * @param tx Transaction to estimate gas for + * @param from Optional sender address for the gas estimation. Required for accurate + * estimation of transactions that depend on msg.sender (e.g., ERC-20 transfers). */ - async estimateGas(tx: Transaction): Promise { + async estimateGas(tx: Transaction, from?: Address): Promise { const estimate = await this.ethClient.estimateGas({ + from: from?.ethAddress(), to: tx.to?.ethAddress(), data: tx.data ? eth.hexlify(tx.data) : undefined, value: tx.value, @@ -366,8 +369,9 @@ export class Client { // Create the initial transaction used to estimate gas const tx = new Transaction(params.data, gas, gasPrice, nonce, params.to, params.value); - // Estimate gas cost for the transaction - tx.gas = await this.estimateGas(tx); + // Estimate gas cost for the transaction, passing the signer's address if available + // This is required for accurate estimation of transactions that depend on msg.sender + tx.gas = await this.estimateGas(tx, params.signer?.address()); return tx; } diff --git a/typescript/test/gas-estimation.test.ts b/typescript/test/gas-estimation.test.ts new file mode 100644 index 0000000..ff9f6ad --- /dev/null +++ b/typescript/test/gas-estimation.test.ts @@ -0,0 +1,249 @@ +import { beforeAll, describe, expect, test } from 'vitest'; +import { ABIFromJSON, Account, AddressFromHex, Client, Contract, Transaction } from '../radius'; +import { createTestClient, getFundedAccount } from './helpers'; + +// ERC-20 ABI for SBC token operations +const ERC20_ABI = `[ + {"constant":true,"inputs":[{"name":"account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"type":"function"}, + {"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"type":"function"}, + {"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"type":"function"}, + {"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"type":"function"}, + {"constant":false,"inputs":[{"name":"to","type":"address"},{"name":"value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"type":"function"}, + {"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"type":"function"} +]`; + +// Radius Testnet SBC Token address +const SBC_TOKEN_ADDRESS = '0xF966020a30946A64B39E2e243049036367590858'; + +// Recipient address (Anvil Account 1) +const RECIPIENT_ADDRESS = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'; + +describe('Gas Estimation Bug Fix Tests', async () => { + let client: Client; + let fundedAccount: Account; + let setupFailed = false; + let sbcContract: Contract; + + beforeAll(async () => { + const endpoint: string = process.env.RADIUS_ENDPOINT; + const privateKey: string = process.env.RADIUS_PRIVATE_KEY; + if (!endpoint || !privateKey) { + setupFailed = true; + console.log('Skipping tests due to missing environment variables'); + return; + } + + client = await createTestClient(endpoint); + if (!client) { + setupFailed = true; + console.log('Skipping tests due to failed client creation'); + return; + } + + fundedAccount = await getFundedAccount(client, privateKey); + if (!fundedAccount) { + setupFailed = true; + console.log('Skipping tests due to failed account creation'); + return; + } + + // Create SBC token contract instance + const sbcAddress = AddressFromHex(SBC_TOKEN_ADDRESS); + const sbcAbi = ABIFromJSON(ERC20_ABI); + sbcContract = new Contract(sbcAddress, sbcAbi); + }); + + describe('ERC-20 Token Transfer with Gas Estimation Fix', () => { + test('ERC-20 token transfer succeeds with the fix', async () => { + if (setupFailed) return; + + // First check if we have SBC balance + const balanceResult = await sbcContract.call( + client, + 'balanceOf', + fundedAccount.address().hex() + ); + const sbcBalance = balanceResult[0] as bigint; + console.log(`Current SBC balance: ${sbcBalance}`); + + if (sbcBalance < BigInt(1)) { + console.log('Insufficient SBC balance for transfer test - skipping'); + return; + } + + // Transfer 1 wei unit of SBC token to recipient + const recipientAddress = RECIPIENT_ADDRESS; + const amount = BigInt(1); + + // This was previously failing with "transfer from the zero address" error + // because gas estimation didn't include the 'from' field + const receipt = await sbcContract.execute( + client, + fundedAccount.signer, + 'transfer', + recipientAddress, + amount + ); + + expect(receipt).toBeDefined(); + expect(receipt.status).toBe(1); + console.log(`SBC transfer tx hash: ${receipt.txHash.hex()}`); + console.log(`Transaction status: ${receipt.status} (1 = success)`); + }); + }); + + describe('Gas Estimation with Explicit From Parameter', () => { + test('estimateGas with explicit from parameter returns valid estimate for ERC-20 transfer', async () => { + if (setupFailed) return; + + // Encode the transfer function call + const recipientAddress = RECIPIENT_ADDRESS; + const amount = BigInt(1); + const data = sbcContract.abi.pack('transfer', recipientAddress, amount); + + // Create a transaction object for gas estimation + const tx = new Transaction( + data, + BigInt(0), // gas will be estimated + BigInt(0), // gasPrice + undefined, // nonce + sbcContract.address(), // to + BigInt(0) // value + ); + + // Call estimateGas with explicit 'from' parameter + // This should work correctly now that the fix passes the from address + const gasEstimate = await client.estimateGas(tx, fundedAccount.address()); + + console.log(`Gas estimate for ERC-20 transfer: ${gasEstimate}`); + expect(gasEstimate).toBeGreaterThan(BigInt(0)); + // ERC-20 transfers typically use around 50,000-65,000 gas + expect(gasEstimate).toBeGreaterThan(BigInt(20000)); + expect(gasEstimate).toBeLessThan(BigInt(500000)); + }); + + test('estimateGas with from parameter works for contract calls', async () => { + if (setupFailed) return; + + // Encode a balanceOf call (read-only, but still can estimate gas) + const data = sbcContract.abi.pack('balanceOf', fundedAccount.address().hex()); + + const tx = new Transaction( + data, + BigInt(0), + BigInt(0), + undefined, + sbcContract.address(), + BigInt(0) + ); + + const gasEstimate = await client.estimateGas(tx, fundedAccount.address()); + + console.log(`Gas estimate for balanceOf call: ${gasEstimate}`); + expect(gasEstimate).toBeGreaterThan(BigInt(0)); + }); + }); + + describe('Backward Compatibility', () => { + test('estimateGas without from parameter works for native transfers', async () => { + if (setupFailed) return; + + const recipientAddress = AddressFromHex(RECIPIENT_ADDRESS); + + // Create a simple native value transfer transaction + const tx = new Transaction( + new Uint8Array(), // empty data for native transfer + BigInt(0), + BigInt(0), + undefined, + recipientAddress, + BigInt(100) // transfer 100 wei + ); + + // Call estimateGas without the 'from' parameter + // This should still work for simple transactions + const gasEstimate = await client.estimateGas(tx); + + console.log(`Gas estimate for native transfer (no from): ${gasEstimate}`); + expect(gasEstimate).toBeGreaterThan(BigInt(0)); + // Native transfers use 21,000 gas base + safety margin + expect(gasEstimate).toBeGreaterThanOrEqual(BigInt(21000)); + }); + + test('estimateGas without from parameter works for simple contract deployments', async () => { + if (setupFailed) return; + + // Simple contract bytecode (just returns) + const simpleBytecode = new Uint8Array([ + 0x60, + 0x00, // PUSH1 0x00 + 0x60, + 0x00, // PUSH1 0x00 + 0xf3, // RETURN + ]); + + // Create a contract deployment transaction (no 'to' address) + const tx = new Transaction( + simpleBytecode, + BigInt(0), + BigInt(0), + undefined, + undefined, // no 'to' for deployment + BigInt(0) + ); + + // Call estimateGas without 'from' parameter + const gasEstimate = await client.estimateGas(tx); + + console.log(`Gas estimate for contract deployment (no from): ${gasEstimate}`); + expect(gasEstimate).toBeGreaterThan(BigInt(0)); + }); + }); + + describe('Regression Test: ERC-20 Transfer via Contract.execute', () => { + test('Contract.execute correctly estimates gas for ERC-20 transfers', async () => { + if (setupFailed) return; + + // Check SBC balance first + const balanceResult = await sbcContract.call( + client, + 'balanceOf', + fundedAccount.address().hex() + ); + const sbcBalance = balanceResult[0] as bigint; + + if (sbcBalance < BigInt(2)) { + console.log('Insufficient SBC balance - skipping regression test'); + return; + } + + // Get initial recipient balance + const initialRecipientBalance = ( + await sbcContract.call(client, 'balanceOf', RECIPIENT_ADDRESS) + )[0] as bigint; + + // Execute transfer - this internally uses prepareTx which now passes signer address to estimateGas + const amount = BigInt(1); + const receipt = await sbcContract.execute( + client, + fundedAccount.signer, + 'transfer', + RECIPIENT_ADDRESS, + amount + ); + + expect(receipt).toBeDefined(); + expect(receipt.status).toBe(1); + + // Verify the transfer actually occurred + const finalRecipientBalance = ( + await sbcContract.call(client, 'balanceOf', RECIPIENT_ADDRESS) + )[0] as bigint; + + expect(finalRecipientBalance).toBe(initialRecipientBalance + amount); + console.log( + `Recipient balance increased from ${initialRecipientBalance} to ${finalRecipientBalance}` + ); + }); + }); +});