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
9 changes: 8 additions & 1 deletion src/commands/contracts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import {BaseAction} from "../../lib/actions/BaseAction";
import {pathToFileURL} from "url";
import {formatStakingAmount} from "genlayer-js";
import {buildSync} from "esbuild";
import {ContractFeeCliOptions, parseValidUntil, resolveTransactionFees} from "./fees";
import {ContractFeeCliOptions, parseGasLimit, parseValidUntil, resolveTransactionFees} from "./fees";
import {assertSuccessfulExecution, transactionConsensusStatus} from "./execution";

export interface DeployOptions extends ContractFeeCliOptions {
contract?: string;
args?: any[];
rpc?: string;
/**
* Explicit outer EVM gas limit for the deployment transaction, bypassing
* eth_estimateGas. See #402.
*/
gas?: string;
}

export interface DeployScriptsOptions {
Expand Down Expand Up @@ -133,6 +138,8 @@ export class DeployAction extends BaseAction {

const leaderOnly = false;
const deployParams: any = {code: contractCode, args: options.args, leaderOnly};
const parsedGas = parseGasLimit(options.gas);
if (parsedGas !== undefined) deployParams.gas = parsedGas;
const fees = await resolveTransactionFees(client, options, {
deployTargeted: true,
profileTarget: {kind: "deploy"},
Expand Down
17 changes: 17 additions & 0 deletions src/commands/contracts/fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,20 @@ export const resolveTransactionFees = async (
export const parseValidUntil = (options: ContractFeeCliOptions): string | undefined => {
return parseBigNumberishOption(options.validUntil, "--valid-until");
};

/**
* Parses an explicit `--gas` CLI override into a bigint, or returns
* undefined if not provided. Passed straight through to genlayer-js's
* `gas` option on writeContract/deployContract, bypassing eth_estimateGas
* entirely (see #402: an exact estimate can itself cause the outer EVM
* transaction to revert before GenVM is reached).
*/
export const parseGasLimit = (gas: string | undefined): bigint | undefined => {
const parsed = parseBigNumberishOption(gas, "--gas");
if (parsed === undefined) return undefined;
const value = BigInt(parsed);
if (value <= 0n) {
throw new Error("--gas must be a positive integer.");
}
return value;
};
8 changes: 8 additions & 0 deletions src/commands/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export function initializeContractsCommands(program: Command) {
.option("--appeal-rounds <count>", "Override fee profile appeal rounds")
.option("--fee-value <wei>", "Fee deposit value to send with the transaction")
.option("--valid-until <unixTimestamp>", "Unix timestamp after which the transaction is invalid")
.option(
"--gas <gasLimit>",
"Explicit outer EVM gas limit, bypassing eth_estimateGas (use if an exact gas estimate reverts the transaction before GenVM)",
)
.option("--args <args...>", ARGS_HELP, parseArg, [])
.action(async (options: DeployOptions) => {
const deployer = new DeployAction();
Expand Down Expand Up @@ -149,6 +153,10 @@ export function initializeContractsCommands(program: Command) {
.option("--appeal-rounds <count>", "Override fee profile appeal rounds")
.option("--fee-value <wei>", "Fee deposit value to send with the transaction")
.option("--valid-until <unixTimestamp>", "Unix timestamp after which the transaction is invalid")
.option(
"--gas <gasLimit>",
"Explicit outer EVM gas limit, bypassing eth_estimateGas (use if an exact gas estimate reverts the transaction before GenVM)",
)
.option("--args <args...>", ARGS_HELP, parseArg, [])
.action(async (contractAddress: string, method: string, options: WriteOptions) => {
const writeAction = new WriteAction();
Expand Down
11 changes: 10 additions & 1 deletion src/commands/contracts/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
// import type {GenLayerClient} from "genlayer-js/types";
import {formatStakingAmount} from "genlayer-js";
import {BaseAction} from "../../lib/actions/BaseAction";
import {ContractFeeCliOptions, parseValidUntil, resolveTransactionFees} from "./fees";
import {ContractFeeCliOptions, parseGasLimit, parseValidUntil, resolveTransactionFees} from "./fees";
import {assertSuccessfulExecution, transactionConsensusStatus} from "./execution";

export interface WriteOptions extends ContractFeeCliOptions {
args: any[];
rpc?: string;
/**
* Explicit outer EVM gas limit for the transaction, bypassing
* eth_estimateGas. See #402: an exact gas estimate can itself cause the
* outer transaction to revert before GenVM is reached.
*/
gas?: string;
}

export class WriteAction extends BaseAction {
Expand All @@ -26,6 +32,7 @@ export class WriteAction extends BaseAction {
appealRounds,
feeValue,
validUntil,
gas,
}: WriteOptions & {
contractAddress: string;
method: string;
Expand All @@ -41,6 +48,8 @@ export class WriteAction extends BaseAction {
args,
value: 0n,
};
const parsedGas = parseGasLimit(gas);
if (parsedGas !== undefined) writeParams.gas = parsedGas;
const parsedFees = await resolveTransactionFees(
client,
{fees, feeProfile, feePreset, appealRounds, feeValue, validUntil},
Expand Down
68 changes: 68 additions & 0 deletions tests/actions/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,74 @@ describe("DeployAction", () => {
expect(mockClient.deployContract).toHaveReturnedWith(Promise.resolve("mocked_tx_hash"));
});

test("passes an explicit --gas override through to deployContract as a bigint (#402)", async () => {
const options: DeployOptions = {
contract: "/mocked/contract/path",
args: [1, 2, 3],
gas: "3000000",
};
const contractContent = "contract code";

vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(contractContent);
vi.mocked(mockClient.deployContract).mockResolvedValue("mocked_tx_hash");
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue({
statusName: "ACCEPTED",
txExecutionResultName: "FINISHED_WITH_RETURN",
data: {contract_address: "0xdasdsadasdasdada"},
});

await deployer.deploy(options);

expect(mockClient.deployContract).toHaveBeenCalledWith({
code: contractContent,
args: [1, 2, 3],
leaderOnly: false,
gas: 3_000_000n,
});
});

test("omits gas from deployContract params when --gas is not provided", async () => {
const options: DeployOptions = {
contract: "/mocked/contract/path",
args: [1, 2, 3],
};
const contractContent = "contract code";

vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(contractContent);
vi.mocked(mockClient.deployContract).mockResolvedValue("mocked_tx_hash");
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue({
statusName: "ACCEPTED",
txExecutionResultName: "FINISHED_WITH_RETURN",
data: {contract_address: "0xdasdsadasdasdada"},
});

await deployer.deploy(options);

const calledWith = vi.mocked(mockClient.deployContract).mock.calls[0][0] as any;
expect(calledWith.gas).toBeUndefined();
});

test("rejects an invalid --gas value before calling deployContract", async () => {
const options: DeployOptions = {
contract: "/mocked/contract/path",
args: [1, 2, 3],
gas: "not-a-number",
};

vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue("contract code");

await deployer.deploy(options);

expect(deployer["failSpinner"]).toHaveBeenCalledWith(
"Error deploying contract",
expect.objectContaining({message: "--gas must be a non-negative integer."}),
);
expect(mockClient.deployContract).not.toHaveBeenCalled();
});

test("deploys contract with fee options", async () => {
const options: DeployOptions = {
contract: "/mocked/contract/path",
Expand Down
70 changes: 70 additions & 0 deletions tests/actions/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,76 @@ describe("WriteAction", () => {
});
});

test("passes an explicit --gas override through to writeContract as a bigint (#402)", async () => {
const mockHash = "0xMockedTransactionHash";
const mockReceipt = {statusName: "ACCEPTED", txExecutionResultName: "FINISHED_WITH_RETURN"};

vi.mocked(mockClient.writeContract).mockResolvedValue(mockHash);
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);

await writeAction.write({
contractAddress: "0xMockedContract",
method: "updateData",
args: [42],
gas: "2000000",
} as any);

expect(mockClient.writeContract).toHaveBeenCalledWith({
address: "0xMockedContract",
functionName: "updateData",
args: [42],
value: 0n,
gas: 2_000_000n,
});
});

test("omits gas from writeContract params when --gas is not provided", async () => {
const mockHash = "0xMockedTransactionHash";
const mockReceipt = {statusName: "ACCEPTED", txExecutionResultName: "FINISHED_WITH_RETURN"};

vi.mocked(mockClient.writeContract).mockResolvedValue(mockHash);
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);

await writeAction.write({
contractAddress: "0xMockedContract",
method: "updateData",
args: [42],
} as any);

const calledWith = vi.mocked(mockClient.writeContract).mock.calls[0][0] as any;
expect(calledWith.gas).toBeUndefined();
});

test("rejects a non-numeric --gas value before calling writeContract", async () => {
await writeAction.write({
contractAddress: "0xMockedContract",
method: "updateData",
args: [42],
gas: "not-a-number",
} as any);

expect(writeAction["failSpinner"]).toHaveBeenCalledWith(
"Error during write operation",
expect.objectContaining({message: "--gas must be a non-negative integer."}),
);
expect(mockClient.writeContract).not.toHaveBeenCalled();
});

test("rejects a zero --gas value before calling writeContract", async () => {
await writeAction.write({
contractAddress: "0xMockedContract",
method: "updateData",
args: [42],
gas: "0",
} as any);

expect(writeAction["failSpinner"]).toHaveBeenCalledWith(
"Error during write operation",
expect.objectContaining({message: "--gas must be a positive integer."}),
);
expect(mockClient.writeContract).not.toHaveBeenCalled();
});

test("calls writeContract with fee options", async () => {
const mockHash = "0xMockedTransactionHash";
const mockReceipt = {statusName: "ACCEPTED", txExecutionResultName: "FINISHED_WITH_RETURN"};
Expand Down
18 changes: 18 additions & 0 deletions tests/commands/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ describe("deploy command", () => {
});
});

test("DeployAction.deploy is called with --gas flag (#402)", async () => {
program.parse([
"node",
"test",
"deploy",
"--contract",
"./path/to/contract",
"--gas",
"3000000",
]);
expect(DeployAction).toHaveBeenCalledTimes(1);
expect(DeployAction.prototype.deploy).toHaveBeenCalledWith({
contract: "./path/to/contract",
args: [],
gas: "3000000",
});
});

test("DeployAction.deploy is called with positional arguments", async () => {
program.parse([
"node",
Expand Down
19 changes: 19 additions & 0 deletions tests/commands/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ describe("write command", () => {
});
});

test("WriteAction.write is called with --gas flag (#402)", async () => {
program.parse([
"node",
"test",
"write",
"0xMockedContract",
"setData",
"--gas",
"2000000",
]);
expect(WriteAction).toHaveBeenCalledTimes(1);
expect(WriteAction.prototype.write).toHaveBeenCalledWith({
contractAddress: "0xMockedContract",
method: "setData",
args: [],
gas: "2000000",
});
});

test("WriteAction.write is called with positional arguments and options", async () => {
program.parse([
"node",
Expand Down
Loading