genlayer 0.39.1 → 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.
- package/.claude/skills/release/SKILL.md +89 -0
- package/.github/e2e-track +1 -0
- package/.github/workflows/e2e-housekeeper.yml +84 -0
- package/.github/workflows/e2e.yml +740 -0
- package/.github/workflows/publish.yml +42 -29
- package/.github/workflows/validate-code.yml +1 -1
- package/CHANGELOG.md +13 -0
- package/CONTRIBUTING.md +24 -0
- package/README.md +50 -1
- package/dist/index.js +418 -48
- package/package.json +3 -4
- package/scripts/release.sh +157 -0
- package/src/commands/contracts/deploy.ts +6 -1
- package/src/commands/contracts/estimateFees.ts +133 -0
- package/src/commands/contracts/fees.ts +236 -0
- package/src/commands/contracts/index.ts +43 -0
- package/src/commands/contracts/write.ts +16 -3
- package/src/commands/localnet/validators.ts +4 -5
- package/src/lib/clients/jsonRpcClient.ts +9 -4
- package/src/lib/clients/system.ts +19 -17
- package/tests/actions/deploy.test.ts +49 -0
- package/tests/actions/estimateFees.test.ts +271 -0
- package/tests/actions/validators.test.ts +5 -5
- package/tests/actions/write.test.ts +47 -0
- package/tests/commands/deploy.test.ts +25 -0
- package/tests/commands/estimateFees.test.ts +98 -0
- package/tests/commands/write.test.ts +26 -0
- package/tests/libs/jsonRpcClient.test.ts +18 -0
- package/tests/libs/system.test.ts +36 -3
- package/tsconfig.json +2 -3
|
@@ -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
|
|
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({
|