Skip to content

Commit ca083ab

Browse files
authored
feat: add v0.6 fee-aware commands (#340)
1 parent 82b7958 commit ca083ab

19 files changed

Lines changed: 1042 additions & 26 deletions

‎README.md‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,15 @@ USAGE:
169169
genlayer deploy [options]
170170
genlayer call <contractAddress> <method> [options]
171171
genlayer write <contractAddress> <method> [options]
172+
genlayer estimate-fees [contractAddress] [method] [options]
172173
genlayer schema <contractAddress> [options]
173174

174175
OPTIONS (deploy):
175176
--contract <contractPath> (Optional) Path to the intelligent contract to deploy
176177
--rpc <rpcUrl> RPC URL for the network
178+
--fees <json> Transaction fee options JSON passed to genlayer-js
179+
--fee-value <wei> Explicit fee deposit value
180+
--valid-until <timestamp> Unix timestamp after which the transaction is invalid
177181
--args <args...> Contract arguments (see Argument Types below)
178182

179183
OPTIONS (call):
@@ -182,23 +186,69 @@ OPTIONS (call):
182186

183187
OPTIONS (write):
184188
--rpc <rpcUrl> RPC URL for the network
189+
--fees <json> Transaction fee options JSON passed to genlayer-js
190+
--fee-value <wei> Explicit fee deposit value
191+
--valid-until <timestamp> Unix timestamp after which the transaction is invalid
185192
--args <args...> Method arguments (see Argument Types below)
186193

194+
OPTIONS (estimate-fees):
195+
--rpc <rpcUrl> RPC URL for the network
196+
--fees <json> Fee estimate options JSON, or a transaction fee object
197+
--include-report Include simulation fee accounting/report in the generated estimate output
198+
--args <args...> Method arguments for simulation-derived estimates
199+
187200
OPTIONS (schema):
188201
--rpc <rpcUrl> RPC URL for the network
189202

190203
EXAMPLES:
191204
genlayer deploy
192205
genlayer deploy --contract ./my_contract.gpy
193206
genlayer deploy --contract ./my_contract.gpy --args "arg1" "arg2" 123
207+
genlayer deploy --contract ./my_contract.gpy --fees '{"distribution":{"leaderTimeunitsAllocation":"100","validatorTimeunitsAllocation":"200","rotations":["0"]}}'
194208
genlayer call 0x123456789abcdef greet --args "Hello World!"
195209
genlayer write 0x123456789abcdef updateValue --args 42
210+
genlayer write 0x123456789abcdef updateValue --fees '{"distribution":{"leaderTimeunitsAllocation":"100","validatorTimeunitsAllocation":"200","rotations":["0"]}}' --args 42
211+
genlayer estimate-fees
212+
genlayer estimate-fees 0x123456789abcdef updateValue --args 42
196213
genlayer write 0x123456789abcdef sendReward --args 0x6857Ed54CbafaA74Fc0357145eC0ee1536ca45A0
197214
genlayer write 0x123456789abcdef setScores --args '[1, 2, 3]'
198215
genlayer write 0x123456789abcdef setConfig --args '{"timeout": 30, "retries": 5}'
199216
genlayer schema 0x123456789abcdef
200217
```
201218
219+
##### Transaction Fee Options
220+
221+
`--fees` accepts the same transaction fee object as `genlayer-js`. Quote large
222+
integer values as strings to preserve precision. `messageAllocations[].messageType`
223+
may be `"internal"`, `"external"`, `0`, or `1`.
224+
225+
For targeted message budgets, the CLI can derive GenVM call keys before passing
226+
the JSON to `genlayer-js`:
227+
228+
```bash
229+
genlayer estimate-fees 0x123456789abcdef settle \
230+
--fees '{"messageAllocations":[{"messageType":"internal","recipient":"0x0000000000000000000000000000000000000001","callKeyMethod":"settle_campaign","budget":"700000"}]}'
231+
232+
genlayer write 0x123456789abcdef sendReward \
233+
--fees '{"messageAllocations":[{"messageType":"external","recipient":"0x0000000000000000000000000000000000000002","callKeySelector":"0xa9059cbb","budget":"210000"}]}'
234+
```
235+
236+
Use `callKeyMethod` for internal GenVM messages, `callKeySelector` for a 4-byte
237+
EVM selector, or `callKeyCalldata` for full external calldata. Explicit
238+
`callKey` is still accepted for advanced cases.
239+
240+
If `--fees` includes a `distribution` and `--fee-value` is omitted, the SDK
241+
derives the fee deposit from FeeManager on network backends, or from
242+
`sim_getFeeConfig` on Studio. Use `--fee-value` only when you need to force an
243+
explicit deposit value.
244+
245+
`estimate-fees` prints the SDK fee preset. Without a contract/method it calls
246+
`estimateTransactionFees`. With a contract/method it uses the SDK target-write
247+
estimation helper so the preset reflects the observed Studio fee accounting
248+
report. Add `--include-report` to use the explicit simulate-and-derive path and
249+
include the simulation fee accounting and execution fee report next to the
250+
preset for reproducible gas-unit debugging.
251+
202252
##### Argument Types
203253
204254
The `--args` option automatically detects and converts values to the correct type:
@@ -522,4 +572,3 @@ We welcome contributions to GenLayerJS SDK! Whether it's new features, improved
522572
## License
523573
524574
This project is licensed under the ... License - see the [LICENSE](LICENSE) file for details.
525-

‎package-lock.json‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"dotenv": "^17.0.0",
6767
"ethers": "^6.13.4",
6868
"fs-extra": "^11.3.0",
69-
"genlayer-js": "^1.0.0",
69+
"genlayer-js": "^1.1.8",
7070
"inquirer": "^12.0.0",
7171
"keytar": "^7.9.0",
7272
"node-fetch": "^3.0.0",

‎src/commands/contracts/deploy.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import {BaseAction} from "../../lib/actions/BaseAction";
44
import {pathToFileURL} from "url";
55
import {TransactionStatus} from "genlayer-js/types";
66
import {buildSync} from "esbuild";
7+
import {ContractFeeCliOptions, parseTransactionFees, parseValidUntil} from "./fees";
78

8-
export interface DeployOptions {
9+
export interface DeployOptions extends ContractFeeCliOptions {
910
contract?: string;
1011
args?: any[];
1112
rpc?: string;
@@ -131,6 +132,10 @@ export class DeployAction extends BaseAction {
131132

132133
const leaderOnly = false;
133134
const deployParams: any = {code: contractCode, args: options.args, leaderOnly};
135+
const fees = parseTransactionFees(options);
136+
const validUntil = parseValidUntil(options);
137+
if (fees) deployParams.fees = fees;
138+
if (validUntil !== undefined) deployParams.validUntil = validUntil;
134139

135140
this.setSpinnerText("Starting contract deployment...");
136141
this.log("Deployment Parameters:", deployParams);
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import {BaseAction} from "../../lib/actions/BaseAction";
2+
import {ContractFeeCliOptions, parseFeeEstimateOptions} from "./fees";
3+
4+
export interface EstimateFeesOptions extends Pick<ContractFeeCliOptions, "fees"> {
5+
args?: any[];
6+
rpc?: string;
7+
json?: boolean;
8+
includeReport?: boolean;
9+
}
10+
11+
const toTransactionFees = (estimate: Record<string, any>): Record<string, any> => ({
12+
distribution: estimate.distribution,
13+
...(estimate.messageAllocations ? {messageAllocations: estimate.messageAllocations} : {}),
14+
feeValue: estimate.feeValue ?? estimate.fee_value,
15+
});
16+
17+
const toJsonSafe = (value: any): any => {
18+
if (typeof value === "bigint") return value.toString();
19+
if (Array.isArray(value)) return value.map(toJsonSafe);
20+
if (value && typeof value === "object") {
21+
return Object.fromEntries(
22+
Object.entries(value)
23+
.filter(([, item]) => item !== undefined)
24+
.map(([key, item]) => [key, toJsonSafe(item)]),
25+
);
26+
}
27+
return value;
28+
};
29+
30+
const simulationFeeReport = (simulation: Record<string, any>): Record<string, any> | undefined => (
31+
simulation.feeReport ?? simulation.feeAccounting?.execution_fee_report
32+
);
33+
34+
const withSimulationReport = (estimate: unknown, simulation: unknown): unknown => {
35+
if (!simulation || typeof simulation !== "object" || Array.isArray(simulation)) {
36+
return estimate;
37+
}
38+
39+
const simulationRecord = simulation as Record<string, any>;
40+
return {
41+
...(estimate && typeof estimate === "object" && !Array.isArray(estimate)
42+
? estimate as Record<string, any>
43+
: {estimate}),
44+
simulation: {
45+
feeAccounting: simulationRecord.feeAccounting,
46+
feeReport: simulationFeeReport(simulationRecord),
47+
},
48+
};
49+
};
50+
51+
export class EstimateFeesAction extends BaseAction {
52+
constructor() {
53+
super();
54+
}
55+
56+
async estimate({
57+
contractAddress,
58+
method,
59+
args,
60+
rpc,
61+
fees,
62+
json,
63+
includeReport,
64+
}: EstimateFeesOptions & {
65+
contractAddress?: string;
66+
method?: string;
67+
}): Promise<void> {
68+
try {
69+
const client = await this.getClient(rpc, true);
70+
await client.initializeConsensusSmartContract();
71+
const estimateOptions = parseFeeEstimateOptions({fees});
72+
73+
if (!json) this.startSpinner("Estimating transaction fees...");
74+
let estimate: unknown;
75+
76+
if (contractAddress || method) {
77+
if (!contractAddress || !method) {
78+
this.failSpinner("Both contractAddress and method are required for simulation-derived fee estimates.");
79+
return;
80+
}
81+
82+
if (!json) this.setSpinnerText(`Simulating ${method} on ${contractAddress}...`);
83+
if (!includeReport && typeof client.estimateTransactionFeesForWrite === "function") {
84+
estimate = await client.estimateTransactionFeesForWrite({
85+
...(estimateOptions ?? {}),
86+
address: contractAddress as any,
87+
functionName: method,
88+
args: args ?? [],
89+
});
90+
} else {
91+
if (typeof client.simulateWriteContract !== "function") {
92+
this.failSpinner("The active genlayer-js client does not support write simulation.");
93+
return;
94+
}
95+
if (typeof client.estimateTransactionFeesFromSimulation !== "function") {
96+
this.failSpinner("The active genlayer-js client does not support simulation-derived fee estimates.");
97+
return;
98+
}
99+
100+
const initialEstimate = await client.estimateTransactionFees(estimateOptions);
101+
const simulation = await client.simulateWriteContract({
102+
address: contractAddress as any,
103+
functionName: method,
104+
args: args ?? [],
105+
includeReceipt: true,
106+
fees: toTransactionFees(initialEstimate as Record<string, any>),
107+
});
108+
estimate = await client.estimateTransactionFeesFromSimulation({
109+
...(estimateOptions ?? {}),
110+
simulation,
111+
});
112+
if (includeReport) {
113+
estimate = withSimulationReport(estimate, simulation);
114+
}
115+
}
116+
} else {
117+
if (includeReport) {
118+
this.failSpinner("--include-report requires both contractAddress and method.");
119+
return;
120+
}
121+
estimate = await client.estimateTransactionFees(estimateOptions);
122+
}
123+
124+
if (json) {
125+
console.log(JSON.stringify(toJsonSafe(estimate)));
126+
} else {
127+
this.succeedSpinner("Fee estimate generated", toJsonSafe(estimate));
128+
}
129+
} catch (error) {
130+
this.failSpinner("Error estimating transaction fees", error);
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)