Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions typescript/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<bigint> {
async estimateGas(tx: Transaction, from?: Address): Promise<bigint> {
const estimate = await this.ethClient.estimateGas({
from: from?.ethAddress(),
to: tx.to?.ethAddress(),
data: tx.data ? eth.hexlify(tx.data) : undefined,
value: tx.value,
Expand Down Expand Up @@ -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;
}
Expand Down
249 changes: 249 additions & 0 deletions typescript/test/gas-estimation.test.ts
Original file line number Diff line number Diff line change
@@ -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}`
);
});
});
});