genlayer 0.39.0 → 0.39.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env bash
2
+ # Cut a release on the current stable branch.
3
+ #
4
+ # Bumps package.json, prepends CHANGELOG.md, commits, tags vX.Y.Z, and
5
+ # pushes both the branch commit and the tag. publish.yml takes over from
6
+ # the tag push (build → npm publish → GitHub Release).
7
+ #
8
+ # Releases are deliberate. There is no auto-bump on push; only this
9
+ # script is supposed to create release tags. Run from the major branch
10
+ # you want to ship a release on (e.g. v0.39 for v0.39.x).
11
+ #
12
+ # Usage:
13
+ # scripts/release.sh <X.Y.Z> # explicit semver — recommended
14
+ # scripts/release.sh patch # 0.39.1 → 0.39.2
15
+ # scripts/release.sh minor # 0.39.1 → 0.40.0 — refused unless --allow-major
16
+ # scripts/release.sh major # 0.39.1 → 1.0.0 — refused unless --allow-major
17
+ # scripts/release.sh --allow-major <X.Y.Z>
18
+ #
19
+ # Semver-zero rule: while the major is 0, the MINOR is the breaking-
20
+ # change boundary (per semver). 0.39 → 0.40 IS a major bump. The script
21
+ # refuses both `minor` and `major` keywords without --allow-major while
22
+ # the current major is 0.
23
+ #
24
+ # Pre-flight (each check refuses to proceed on failure):
25
+ # - On a v<MAJOR>[.<MINOR>] branch (refuses on main / feature branches)
26
+ # - Working tree clean
27
+ # - Local HEAD matches origin/<branch>
28
+ # - Latest CI run on HEAD is green
29
+
30
+ set -euo pipefail
31
+
32
+ ALLOW_MAJOR=0
33
+ if [ "${1:-}" = "--allow-major" ]; then
34
+ ALLOW_MAJOR=1
35
+ shift
36
+ fi
37
+
38
+ VERSION_ARG="${1:-}"
39
+ if [ -z "$VERSION_ARG" ]; then
40
+ echo "Usage: $0 [--allow-major] <X.Y.Z>|patch|minor|major" >&2
41
+ exit 2
42
+ fi
43
+
44
+ repo_root="$(git rev-parse --show-toplevel)"
45
+ cd "$repo_root"
46
+
47
+ branch="$(git rev-parse --abbrev-ref HEAD)"
48
+ if ! [[ "$branch" =~ ^v[0-9]+(\.[0-9]+)?(-dev)?$ ]]; then
49
+ cat >&2 <<EOF
50
+ Refusing to release from '$branch'.
51
+
52
+ Release branches in this repo are named after the major they ship
53
+ (v0.39, v0.40, ...) or the next-major dev line (v0.40-dev). main has
54
+ been retired — see CONTRIBUTING.md for the branch model.
55
+
56
+ If you intended to ship a v0.39.x release, run:
57
+ git checkout v0.39 && git pull --ff-only && scripts/release.sh ...
58
+ EOF
59
+ exit 1
60
+ fi
61
+
62
+ if [ -n "$(git status --porcelain)" ]; then
63
+ echo "Working tree not clean. Stash or commit first." >&2
64
+ exit 1
65
+ fi
66
+
67
+ git fetch --tags origin "$branch"
68
+ local_sha="$(git rev-parse HEAD)"
69
+ remote_sha="$(git rev-parse "origin/$branch")"
70
+ if [ "$local_sha" != "$remote_sha" ]; then
71
+ cat >&2 <<EOF
72
+ Local $branch ($local_sha) does not match origin/$branch ($remote_sha).
73
+ Pull (or push) before releasing.
74
+ EOF
75
+ exit 1
76
+ fi
77
+
78
+ if command -v gh >/dev/null 2>&1; then
79
+ status="$(gh run list --branch "$branch" --commit "$local_sha" --limit 1 --json conclusion --jq '.[0].conclusion' 2>/dev/null || echo "")"
80
+ case "$status" in
81
+ success) ;;
82
+ "" )
83
+ echo "Warning: no CI run found for $local_sha on $branch. Continuing anyway." >&2
84
+ ;;
85
+ *)
86
+ echo "Latest CI on $branch@$local_sha is '$status' (not success). Refusing to release a red commit." >&2
87
+ exit 1
88
+ ;;
89
+ esac
90
+ fi
91
+
92
+ current_version="$(node -p "require('./package.json').version")"
93
+
94
+ case "$VERSION_ARG" in
95
+ major|minor|patch)
96
+ next_version="$(node -e "
97
+ const semver = require('semver');
98
+ const cur = require('./package.json').version;
99
+ const inc = '$VERSION_ARG';
100
+ const out = semver.inc(cur, inc);
101
+ if (!out) { console.error('semver.inc failed for', cur, inc); process.exit(1); }
102
+ console.log(out);
103
+ ")"
104
+ ;;
105
+ *)
106
+ next_version="$VERSION_ARG"
107
+ ;;
108
+ esac
109
+
110
+ if ! node -e "if (!require('semver').valid('$next_version')) process.exit(1)"; then
111
+ echo "Not a valid semver: $next_version" >&2
112
+ exit 2
113
+ fi
114
+
115
+ cur_major="${current_version%%.*}"
116
+ next_major="${next_version%%.*}"
117
+ cur_minor="$(node -p "require('semver').minor('$current_version')")"
118
+ next_minor="$(node -p "require('semver').minor('$next_version')")"
119
+
120
+ if [ "$cur_major" = "0" ]; then
121
+ if [ "$next_major" != "0" ] || [ "$next_minor" != "$cur_minor" ]; then
122
+ if [ "$ALLOW_MAJOR" -ne 1 ]; then
123
+ cat >&2 <<EOF
124
+ Refusing $current_version → $next_version without --allow-major.
125
+
126
+ This package is still on a 0.x line, so the MINOR component is the
127
+ breaking-change boundary (per semver). 0.$cur_minor → 0.$next_minor
128
+ counts as a major bump and should land on a new branch (v0.$next_minor)
129
+ following the model in CONTRIBUTING.md.
130
+
131
+ If you actually want this on the current branch, pass --allow-major.
132
+ EOF
133
+ exit 1
134
+ fi
135
+ fi
136
+ elif [ "$next_major" != "$cur_major" ] && [ "$ALLOW_MAJOR" -ne 1 ]; then
137
+ cat >&2 <<EOF
138
+ Refusing major bump $current_version → $next_version without --allow-major.
139
+
140
+ In this repo's release model, a major bump means cutting a new branch
141
+ (v$next_major). Don't tag a major on top of the v$cur_major branch.
142
+ EOF
143
+ exit 1
144
+ fi
145
+
146
+ echo "Releasing v$next_version on $branch (was v$current_version)."
147
+
148
+ # release-it bumps + CHANGELOG + commit + tag + push in one shot.
149
+ # --no-npm.publish keeps npm out of the dev machine; --no-github.release
150
+ # defers the GH release to publish.yml on the tag arrival.
151
+ npx release-it "$next_version" --ci \
152
+ --no-npm.publish \
153
+ --no-github.release
154
+
155
+ echo
156
+ echo "Pushed v$next_version. publish.yml will fire on the tag and ship to npm."
157
+ echo "Track it at: https://github.com/genlayerlabs/genlayer-cli/actions"
@@ -4,8 +4,9 @@ import {BaseAction} from "../../lib/actions/BaseAction";
4
4
  import {pathToFileURL} from "url";
5
5
  import {TransactionStatus} from "genlayer-js/types";
6
6
  import {buildSync} from "esbuild";
7
+ import {ContractFeeCliOptions, parseTransactionFees, parseValidUntil} from "./fees";
7
8
 
8
- export interface DeployOptions {
9
+ export interface DeployOptions extends ContractFeeCliOptions {
9
10
  contract?: string;
10
11
  args?: any[];
11
12
  rpc?: string;
@@ -131,6 +132,10 @@ export class DeployAction extends BaseAction {
131
132
 
132
133
  const leaderOnly = false;
133
134
  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;
134
139
 
135
140
  this.setSpinnerText("Starting contract deployment...");
136
141
  this.log("Deployment Parameters:", deployParams);
@@ -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,0 +1,236 @@
1
+ import {hexToBytes, keccak256, toHex, type Hex} from "viem";
2
+
3
+ export interface ContractFeeCliOptions {
4
+ fees?: string;
5
+ feeValue?: string;
6
+ validUntil?: string;
7
+ }
8
+
9
+ const parseJsonObject = (value: string, optionName: string): Record<string, any> => {
10
+ let parsed: unknown;
11
+ try {
12
+ parsed = JSON.parse(value);
13
+ } catch (error) {
14
+ throw new Error(`${optionName} must be valid JSON.`);
15
+ }
16
+
17
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
18
+ throw new Error(`${optionName} must be a JSON object.`);
19
+ }
20
+
21
+ assertSafeJsonNumbers(parsed, optionName);
22
+ return parsed as Record<string, any>;
23
+ };
24
+
25
+ const assertSafeJsonNumbers = (value: unknown, path: string): void => {
26
+ if (typeof value === "number" && !Number.isSafeInteger(value)) {
27
+ throw new Error(`${path} contains an unsafe number. Quote large integer values as strings.`);
28
+ }
29
+
30
+ if (Array.isArray(value)) {
31
+ value.forEach((item, index) => assertSafeJsonNumbers(item, `${path}[${index}]`));
32
+ return;
33
+ }
34
+
35
+ if (value && typeof value === "object") {
36
+ for (const [key, item] of Object.entries(value)) {
37
+ assertSafeJsonNumbers(item, `${path}.${key}`);
38
+ }
39
+ }
40
+ };
41
+
42
+ const parseBigNumberishOption = (value: string | undefined, optionName: string): string | undefined => {
43
+ if (value === undefined) return undefined;
44
+ const trimmed = value.trim();
45
+ if (!/^(0x[0-9a-fA-F]+|[0-9]+)$/.test(trimmed)) {
46
+ throw new Error(`${optionName} must be a non-negative integer.`);
47
+ }
48
+ return trimmed;
49
+ };
50
+
51
+ const CALL_KEY_UNNAMED = "0x0000000000000000000000000000000000000000000000000000000000000000" as const;
52
+
53
+ const bytesToPaddedCallKey = (bytes: Uint8Array): Hex => {
54
+ if (bytes.length > 32) {
55
+ throw new Error("call key source bytes must be 32 bytes or fewer.");
56
+ }
57
+ return `0x${toHex(bytes).slice(2).padEnd(64, "0")}` as Hex;
58
+ };
59
+
60
+ const deriveInternalMessageCallKey = (methodName = ""): Hex => {
61
+ const methodBytes = new TextEncoder().encode(methodName);
62
+ if (methodBytes.length < 32) {
63
+ return bytesToPaddedCallKey(methodBytes);
64
+ }
65
+
66
+ const hashed = keccak256(methodBytes);
67
+ const lastByte = Number.parseInt(hashed.slice(-2), 16) | 1;
68
+ return `${hashed.slice(0, -2)}${lastByte.toString(16).padStart(2, "0")}` as Hex;
69
+ };
70
+
71
+ const deriveExternalMessageCallKey = (selectorOrCalldata: Hex): Hex => {
72
+ const bytes = hexToBytes(selectorOrCalldata);
73
+ if (bytes.length < 4) {
74
+ return CALL_KEY_UNNAMED;
75
+ }
76
+ return bytesToPaddedCallKey(bytes.slice(0, 4));
77
+ };
78
+
79
+ const normalizeMessageType = (messageType: unknown, index: number): 0 | 1 | undefined => {
80
+ if (messageType === undefined) {
81
+ return undefined;
82
+ }
83
+
84
+ if (typeof messageType === "number") {
85
+ if (messageType === 0 || messageType === 1) {
86
+ return messageType;
87
+ }
88
+ throw new Error(`--fees.messageAllocations[${index}].messageType must be "internal", "external", 0, or 1.`);
89
+ }
90
+
91
+ if (typeof messageType !== "string") {
92
+ return undefined;
93
+ }
94
+
95
+ const normalized = messageType.toLowerCase();
96
+ if (normalized === "internal") {
97
+ return 1;
98
+ }
99
+ if (normalized === "external") {
100
+ return 0;
101
+ }
102
+ throw new Error(`--fees.messageAllocations[${index}].messageType must be "internal" or "external".`);
103
+ };
104
+
105
+ const readStringField = (allocation: Record<string, any>, field: string, index: number): string => {
106
+ const value = allocation[field];
107
+ if (typeof value !== "string") {
108
+ throw new Error(`--fees.messageAllocations[${index}].${field} must be a string.`);
109
+ }
110
+ return value;
111
+ };
112
+
113
+ const assertFourByteSelector = (selector: string, field: string, index: number): void => {
114
+ if (!/^0x[0-9a-fA-F]{8}$/.test(selector)) {
115
+ throw new Error(`--fees.messageAllocations[${index}].${field} must be a 4-byte hex selector.`);
116
+ }
117
+ };
118
+
119
+ const assertHexBytes = (hex: string, field: string, index: number): void => {
120
+ if (!/^0x([0-9a-fA-F]{2})*$/.test(hex)) {
121
+ throw new Error(`--fees.messageAllocations[${index}].${field} must be even-length hex bytes.`);
122
+ }
123
+ };
124
+
125
+ const normalizeMessageAllocationCallKey = (
126
+ allocation: Record<string, any>,
127
+ messageType: 0 | 1 | undefined,
128
+ index: number,
129
+ ): Record<string, any> => {
130
+ const helperFields = [
131
+ "callKeyMethod",
132
+ "callKeySelector",
133
+ "callKeyCalldata",
134
+ "functionSelector",
135
+ ].filter((field) => allocation[field] !== undefined);
136
+
137
+ if (allocation.callKey !== undefined && helperFields.length > 0) {
138
+ throw new Error(`--fees.messageAllocations[${index}] cannot combine callKey with call-key helper fields.`);
139
+ }
140
+ if (helperFields.length > 1) {
141
+ throw new Error(`--fees.messageAllocations[${index}] must use only one call-key helper field.`);
142
+ }
143
+
144
+ const {
145
+ callKeyMethod,
146
+ callKeySelector,
147
+ callKeyCalldata,
148
+ functionSelector,
149
+ ...normalized
150
+ } = allocation;
151
+
152
+ if (helperFields.length === 0) {
153
+ return normalized;
154
+ }
155
+
156
+ const helperField = helperFields[0];
157
+ if (helperField === "callKeyMethod") {
158
+ if (messageType === 0) {
159
+ throw new Error(`--fees.messageAllocations[${index}].callKeyMethod requires an internal message allocation.`);
160
+ }
161
+ normalized.messageType = messageType ?? 1;
162
+ normalized.callKey = deriveInternalMessageCallKey(readStringField(allocation, helperField, index));
163
+ return normalized;
164
+ }
165
+
166
+ if (messageType === 1) {
167
+ throw new Error(`--fees.messageAllocations[${index}].${helperField} requires an external message allocation.`);
168
+ }
169
+
170
+ const selectorOrCalldata = readStringField(allocation, helperField, index);
171
+ if (helperField === "callKeySelector" || helperField === "functionSelector") {
172
+ assertFourByteSelector(selectorOrCalldata, helperField, index);
173
+ } else {
174
+ assertHexBytes(selectorOrCalldata, helperField, index);
175
+ }
176
+ normalized.messageType = messageType ?? 0;
177
+ normalized.callKey = deriveExternalMessageCallKey(selectorOrCalldata as `0x${string}`);
178
+ return normalized;
179
+ };
180
+
181
+ const normalizeMessageTypes = (fees: Record<string, any>): Record<string, any> => {
182
+ if (!Array.isArray(fees.messageAllocations)) {
183
+ return fees;
184
+ }
185
+
186
+ return {
187
+ ...fees,
188
+ messageAllocations: fees.messageAllocations.map((allocation: any, index: number) => {
189
+ if (!allocation || typeof allocation !== "object" || Array.isArray(allocation)) {
190
+ throw new Error(`--fees.messageAllocations[${index}] must be an object.`);
191
+ }
192
+
193
+ const messageType = normalizeMessageType(allocation.messageType, index);
194
+ return normalizeMessageAllocationCallKey({
195
+ ...allocation,
196
+ ...(messageType === undefined ? {} : {messageType}),
197
+ }, messageType, index);
198
+ }),
199
+ };
200
+ };
201
+
202
+ export const parseTransactionFees = (options: ContractFeeCliOptions): Record<string, any> | undefined => {
203
+ const feeValue = parseBigNumberishOption(options.feeValue, "--fee-value");
204
+ let fees = options.fees ? parseJsonObject(options.fees, "--fees") : undefined;
205
+
206
+ if (!fees && feeValue === undefined) {
207
+ return undefined;
208
+ }
209
+
210
+ fees = normalizeMessageTypes(fees ?? {});
211
+ if (feeValue !== undefined) {
212
+ fees.feeValue = feeValue;
213
+ }
214
+ return fees;
215
+ };
216
+
217
+ export const parseFeeEstimateOptions = (options: Pick<ContractFeeCliOptions, "fees">): Record<string, any> | undefined => {
218
+ if (!options.fees) {
219
+ return undefined;
220
+ }
221
+
222
+ const parsed = normalizeMessageTypes(parseJsonObject(options.fees, "--fees"));
223
+ if (parsed.distribution && typeof parsed.distribution === "object" && !Array.isArray(parsed.distribution)) {
224
+ const {distribution, messageAllocations, ...rest} = parsed;
225
+ return {
226
+ ...distribution,
227
+ ...(messageAllocations !== undefined ? {messageAllocations} : {}),
228
+ ...rest,
229
+ };
230
+ }
231
+ return parsed;
232
+ };
233
+
234
+ export const parseValidUntil = (options: ContractFeeCliOptions): string | undefined => {
235
+ return parseBigNumberishOption(options.validUntil, "--valid-until");
236
+ };
@@ -5,6 +5,7 @@ import {CallAction, CallOptions} from "./call";
5
5
  import {WriteAction, WriteOptions} from "./write";
6
6
  import {SchemaAction, SchemaOptions} from "./schema";
7
7
  import {CodeAction, CodeOptions} from "./code";
8
+ import {EstimateFeesAction, EstimateFeesOptions} from "./estimateFees";
8
9
 
9
10
  const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
10
11
  const ADDR_PREFIX_RE = /^addr#([0-9a-fA-F]{40})$/;
@@ -81,12 +82,33 @@ const ARGS_HELP = [
81
82
  ' dict: \'{"key": "value"}\'',
82
83
  ].join("\n");
83
84
 
85
+ const FEES_HELP = [
86
+ "Transaction fee options JSON passed to genlayer-js.",
87
+ "Example:",
88
+ ' \'{"distribution":{"leaderTimeunitsAllocation":"100","validatorTimeunitsAllocation":"200","rotations":["0"]}}\'',
89
+ "Omit --fee-value to let genlayer-js derive the fee deposit from FeeManager or Studio.",
90
+ 'Message allocation messageType may be "internal", "external", 0, or 1.',
91
+ "Use callKeyMethod for internal messages, or callKeySelector/callKeyCalldata for external messages.",
92
+ ].join("\n");
93
+
94
+ const FEE_ESTIMATE_HELP = [
95
+ "Fee estimate options JSON passed to genlayer-js estimateTransactionFees.",
96
+ "You may pass either flat estimate options or a transaction fee object with distribution/messageAllocations.",
97
+ "Example:",
98
+ ' \'{"distribution":{"leaderTimeunitsAllocation":"100","validatorTimeunitsAllocation":"200","rotations":["0"]}}\'',
99
+ 'Message allocation messageType may be "internal", "external", 0, or 1.',
100
+ "Use callKeyMethod for internal messages, or callKeySelector/callKeyCalldata for external messages.",
101
+ ].join("\n");
102
+
84
103
  export function initializeContractsCommands(program: Command) {
85
104
  program
86
105
  .command("deploy")
87
106
  .description("Deploy intelligent contracts")
88
107
  .option("--contract <contractPath>", "Path to the smart contract to deploy")
89
108
  .option("--rpc <rpcUrl>", "RPC URL for the network")
109
+ .option("--fees <json>", FEES_HELP)
110
+ .option("--fee-value <wei>", "Fee deposit value to send with the transaction")
111
+ .option("--valid-until <unixTimestamp>", "Unix timestamp after which the transaction is invalid")
90
112
  .option("--args <args...>", ARGS_HELP, parseArg, [])
91
113
  .action(async (options: DeployOptions) => {
92
114
  const deployer = new DeployAction();
@@ -117,6 +139,9 @@ export function initializeContractsCommands(program: Command) {
117
139
  .command("write <contractAddress> <method>")
118
140
  .description("Sends a transaction to a contract method that modifies the state")
119
141
  .option("--rpc <rpcUrl>", "RPC URL for the network")
142
+ .option("--fees <json>", FEES_HELP)
143
+ .option("--fee-value <wei>", "Fee deposit value to send with the transaction")
144
+ .option("--valid-until <unixTimestamp>", "Unix timestamp after which the transaction is invalid")
120
145
  .option(
121
146
  "--args <args...>",
122
147
  ARGS_HELP,
@@ -128,6 +153,24 @@ export function initializeContractsCommands(program: Command) {
128
153
  await writeAction.write({contractAddress, method, ...options});
129
154
  });
130
155
 
156
+ program
157
+ .command("estimate-fees [contractAddress] [method]")
158
+ .description("Build a transaction fee preset, optionally from a Studio/localnet write simulation")
159
+ .option("--rpc <rpcUrl>", "RPC URL for the network")
160
+ .option("--fees <json>", FEE_ESTIMATE_HELP)
161
+ .option("--json", "Print the fee estimate as JSON without spinner output")
162
+ .option("--include-report", "Include simulation fee accounting/report in the generated estimate output")
163
+ .option(
164
+ "--args <args...>",
165
+ ARGS_HELP,
166
+ parseArg,
167
+ [],
168
+ )
169
+ .action(async (contractAddress: string | undefined, method: string | undefined, options: EstimateFeesOptions) => {
170
+ const estimateFeesAction = new EstimateFeesAction();
171
+ await estimateFeesAction.estimate({contractAddress, method, ...options});
172
+ });
173
+
131
174
  program
132
175
  .command("schema <contractAddress>")
133
176
  .description("Get the schema for a deployed contract")
@@ -1,8 +1,9 @@
1
1
  // import {simulator} from "genlayer-js/chains";
2
2
  // import type {GenLayerClient} from "genlayer-js/types";
3
3
  import {BaseAction} from "../../lib/actions/BaseAction";
4
+ import {ContractFeeCliOptions, parseTransactionFees, parseValidUntil} from "./fees";
4
5
 
5
- export interface WriteOptions {
6
+ export interface WriteOptions extends ContractFeeCliOptions {
6
7
  args: any[];
7
8
  rpc?: string;
8
9
  }
@@ -17,23 +18,35 @@ export class WriteAction extends BaseAction {
17
18
  method,
18
19
  args,
19
20
  rpc,
21
+ fees,
22
+ feeValue,
23
+ validUntil,
20
24
  }: {
21
25
  contractAddress: string;
22
26
  method: string;
23
27
  args: any[];
24
28
  rpc?: string;
29
+ fees?: string;
30
+ feeValue?: string;
31
+ validUntil?: string;
25
32
  }): Promise<void> {
26
33
  const client = await this.getClient(rpc);
27
34
  await client.initializeConsensusSmartContract();
28
35
  this.startSpinner(`Calling write method ${method} on contract at ${contractAddress}...`);
29
36
 
30
37
  try {
31
- const hash = await client.writeContract({
38
+ const writeParams: any = {
32
39
  address: contractAddress as any,
33
40
  functionName: method,
34
41
  args,
35
42
  value: 0n,
36
- });
43
+ };
44
+ const parsedFees = parseTransactionFees({fees, feeValue, validUntil});
45
+ const parsedValidUntil = parseValidUntil({fees, feeValue, validUntil});
46
+ if (parsedFees) writeParams.fees = parsedFees;
47
+ if (parsedValidUntil !== undefined) writeParams.validUntil = parsedValidUntil;
48
+
49
+ const hash = await client.writeContract(writeParams);
37
50
  this.log("Write Transaction Hash:", hash);
38
51
 
39
52
  const result = await client.waitForTransactionReceipt({