genlayer 0.38.13 → 0.38.15

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.
@@ -37,11 +37,9 @@ jobs:
37
37
 
38
38
  - uses: actions/setup-node@v4
39
39
  with:
40
- node-version: "22"
40
+ node-version: "24"
41
41
  registry-url: "https://registry.npmjs.org"
42
42
 
43
- - run: npm install -g npm@latest
44
-
45
43
  - run: npm ci
46
44
 
47
45
  - name: Release
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.38.15 (2026-04-14)
4
+
5
+ ### Bug Fixes
6
+
7
+ * preserve platform-specific optional deps in lockfile ([af7436b](https://github.com/genlayerlabs/genlayer-cli/commit/af7436b8d4f8f56582c38dd8006efa4946ea8964))
8
+
9
+ ## [0.38.14](https://github.com/genlayerlabs/genlayer-cli/compare/v0.38.13...v0.38.14) (2026-04-02)
10
+
3
11
  ## 0.38.13 (2026-04-01)
4
12
 
5
13
  ## 0.38.12 (2026-04-01)
package/dist/index.js CHANGED
@@ -20078,7 +20078,7 @@ var require_cli_table3 = __commonJS({
20078
20078
  import { program } from "commander";
20079
20079
 
20080
20080
  // package.json
20081
- var version = "0.38.13";
20081
+ var version = "0.38.15";
20082
20082
  var package_default = {
20083
20083
  name: "genlayer",
20084
20084
  version,
@@ -20148,7 +20148,7 @@ var package_default = {
20148
20148
  dotenv: "^17.0.0",
20149
20149
  ethers: "^6.13.4",
20150
20150
  "fs-extra": "^11.3.0",
20151
- "genlayer-js": "^0.27.9",
20151
+ "genlayer-js": "^0.28.5",
20152
20152
  inquirer: "^12.0.0",
20153
20153
  keytar: "^7.9.0",
20154
20154
  "node-fetch": "^3.0.0",
@@ -33738,7 +33738,7 @@ init_toHex();
33738
33738
  init_keccak256();
33739
33739
  init_formatEther();
33740
33740
 
33741
- // node_modules/genlayer-js/dist/chunk-C4Z24PT6.js
33741
+ // node_modules/genlayer-js/dist/chunk-SGAVFNGA.js
33742
33742
  var chains_exports = {};
33743
33743
  __export2(chains_exports, {
33744
33744
  localnet: () => localnet,
@@ -37755,6 +37755,7 @@ var localnet = defineChain({
37755
37755
  stakingContract: null,
37756
37756
  feeManagerContract: null,
37757
37757
  roundsStorageContract: null,
37758
+ appealsContract: null,
37758
37759
  defaultNumberOfInitialValidators: 5,
37759
37760
  defaultConsensusMaxRotations: 3
37760
37761
  });
@@ -41768,6 +41769,7 @@ var studionet = defineChain({
41768
41769
  stakingContract: null,
41769
41770
  feeManagerContract: null,
41770
41771
  roundsStorageContract: null,
41772
+ appealsContract: null,
41771
41773
  defaultNumberOfInitialValidators: 5,
41772
41774
  defaultConsensusMaxRotations: 3
41773
41775
  });
@@ -47261,6 +47263,7 @@ var testnetAsimov = defineChain({
47261
47263
  stakingContract: STAKING_CONTRACT,
47262
47264
  feeManagerContract: null,
47263
47265
  roundsStorageContract: null,
47266
+ appealsContract: null,
47264
47267
  defaultNumberOfInitialValidators: 5,
47265
47268
  defaultConsensusMaxRotations: 3
47266
47269
  });
@@ -47294,6 +47297,70 @@ var ROUNDS_STORAGE_CONTRACT = {
47294
47297
  stateMutability: "view",
47295
47298
  inputs: [{ name: "_txId", type: "bytes32" }],
47296
47299
  outputs: [{ name: "", type: "uint256" }]
47300
+ },
47301
+ {
47302
+ type: "function",
47303
+ name: "getRoundData",
47304
+ stateMutability: "view",
47305
+ inputs: [
47306
+ { name: "_txId", type: "bytes32" },
47307
+ { name: "_round", type: "uint256" }
47308
+ ],
47309
+ outputs: [{
47310
+ name: "",
47311
+ type: "tuple",
47312
+ components: [
47313
+ { name: "round", type: "uint256" },
47314
+ { name: "leaderIndex", type: "uint256" },
47315
+ { name: "votesCommitted", type: "uint256" },
47316
+ { name: "votesRevealed", type: "uint256" },
47317
+ { name: "appealBond", type: "uint256" },
47318
+ { name: "rotationsLeft", type: "uint256" },
47319
+ { name: "result", type: "uint8" },
47320
+ { name: "roundValidators", type: "address[]" },
47321
+ { name: "validatorVotes", type: "uint8[]" },
47322
+ { name: "validatorVotesHash", type: "bytes32[]" },
47323
+ { name: "validatorResultHash", type: "bytes32[]" }
47324
+ ]
47325
+ }]
47326
+ },
47327
+ {
47328
+ type: "function",
47329
+ name: "getLastRoundData",
47330
+ stateMutability: "view",
47331
+ inputs: [{ name: "_txId", type: "bytes32" }],
47332
+ outputs: [
47333
+ { name: "", type: "uint256" },
47334
+ {
47335
+ name: "",
47336
+ type: "tuple",
47337
+ components: [
47338
+ { name: "round", type: "uint256" },
47339
+ { name: "leaderIndex", type: "uint256" },
47340
+ { name: "votesCommitted", type: "uint256" },
47341
+ { name: "votesRevealed", type: "uint256" },
47342
+ { name: "appealBond", type: "uint256" },
47343
+ { name: "rotationsLeft", type: "uint256" },
47344
+ { name: "result", type: "uint8" },
47345
+ { name: "roundValidators", type: "address[]" },
47346
+ { name: "validatorVotes", type: "uint8[]" },
47347
+ { name: "validatorVotesHash", type: "bytes32[]" },
47348
+ { name: "validatorResultHash", type: "bytes32[]" }
47349
+ ]
47350
+ }
47351
+ ]
47352
+ }
47353
+ ]
47354
+ };
47355
+ var APPEALS_CONTRACT = {
47356
+ address: "0xbb8C35AA878D09b9830aFF9e5aAC6492BFbd5471",
47357
+ abi: [
47358
+ {
47359
+ type: "function",
47360
+ name: "canAppeal",
47361
+ stateMutability: "view",
47362
+ inputs: [{ name: "_txId", type: "bytes32" }],
47363
+ outputs: [{ name: "", type: "bool" }]
47297
47364
  }
47298
47365
  ]
47299
47366
  };
@@ -50640,6 +50707,7 @@ var testnetBradbury = defineChain({
50640
50707
  stakingContract: STAKING_CONTRACT2,
50641
50708
  feeManagerContract: FEE_MANAGER_CONTRACT,
50642
50709
  roundsStorageContract: ROUNDS_STORAGE_CONTRACT,
50710
+ appealsContract: APPEALS_CONTRACT,
50643
50711
  defaultNumberOfInitialValidators: 5,
50644
50712
  defaultConsensusMaxRotations: 3
50645
50713
  });
@@ -51410,10 +51478,10 @@ function extractGenCallResult(result) {
51410
51478
  }
51411
51479
  var contractActions = (client, publicClient) => {
51412
51480
  return {
51413
- /** Retrieves the source code of a deployed contract. Localnet only. */
51481
+ /** Retrieves the source code of a deployed contract. Studio only. */
51414
51482
  getContractCode: async (address) => {
51415
- if (client.chain.id !== localnet.id) {
51416
- throw new Error(`getContractCode is only available on localnet (current chain: ${client.chain.name})`);
51483
+ if (!client.chain.isStudio) {
51484
+ throw new Error(`getContractCode is only available on Studio networks (current chain: ${client.chain.name})`);
51417
51485
  }
51418
51486
  const result = await client.request({
51419
51487
  method: "gen_getContractCode",
@@ -51422,10 +51490,10 @@ var contractActions = (client, publicClient) => {
51422
51490
  const codeBytes = b64ToArray(result);
51423
51491
  return new TextDecoder().decode(codeBytes);
51424
51492
  },
51425
- /** Gets the schema (methods and constructor) of a deployed contract. Localnet only. */
51493
+ /** Gets the schema (methods and constructor) of a deployed contract. Studio only. */
51426
51494
  getContractSchema: async (address) => {
51427
- if (client.chain.id !== localnet.id) {
51428
- throw new Error(`getContractSchema is only available on localnet (current chain: ${client.chain.name})`);
51495
+ if (!client.chain.isStudio) {
51496
+ throw new Error(`getContractSchema is only available on Studio networks (current chain: ${client.chain.name})`);
51429
51497
  }
51430
51498
  const schema = await client.request({
51431
51499
  method: "gen_getContractSchema",
@@ -51433,10 +51501,10 @@ var contractActions = (client, publicClient) => {
51433
51501
  });
51434
51502
  return schema;
51435
51503
  },
51436
- /** Generates a schema for contract code without deploying it. Localnet only. */
51504
+ /** Generates a schema for contract code without deploying it. Studio only. */
51437
51505
  getContractSchemaForCode: async (contractCode) => {
51438
- if (client.chain.id !== localnet.id) {
51439
- throw new Error(`getContractSchema is only available on localnet (current chain: ${client.chain.name})`);
51506
+ if (!client.chain.isStudio) {
51507
+ throw new Error(`getContractSchemaForCode is only available on Studio networks (current chain: ${client.chain.name})`);
51440
51508
  }
51441
51509
  const schema = await client.request({
51442
51510
  method: "gen_getContractSchemaForCode",
@@ -51600,6 +51668,54 @@ var contractActions = (client, publicClient) => {
51600
51668
  });
51601
51669
  return minBond;
51602
51670
  },
51671
+ /** Returns the current consensus round number for a transaction. */
51672
+ getRoundNumber: async (args) => {
51673
+ if (!client.chain.roundsStorageContract?.address) {
51674
+ throw new Error("getRoundNumber not supported on this chain (missing roundsStorageContract)");
51675
+ }
51676
+ return publicClient.readContract({
51677
+ address: client.chain.roundsStorageContract.address,
51678
+ abi: client.chain.roundsStorageContract.abi,
51679
+ functionName: "getRoundNumber",
51680
+ args: [args.txId]
51681
+ });
51682
+ },
51683
+ /** Returns detailed data for a specific consensus round. */
51684
+ getRoundData: async (args) => {
51685
+ if (!client.chain.roundsStorageContract?.address) {
51686
+ throw new Error("getRoundData not supported on this chain (missing roundsStorageContract)");
51687
+ }
51688
+ return publicClient.readContract({
51689
+ address: client.chain.roundsStorageContract.address,
51690
+ abi: client.chain.roundsStorageContract.abi,
51691
+ functionName: "getRoundData",
51692
+ args: [args.txId, args.round]
51693
+ });
51694
+ },
51695
+ /** Returns the current round number and its data for a transaction. */
51696
+ getLastRoundData: async (args) => {
51697
+ if (!client.chain.roundsStorageContract?.address) {
51698
+ throw new Error("getLastRoundData not supported on this chain (missing roundsStorageContract)");
51699
+ }
51700
+ return publicClient.readContract({
51701
+ address: client.chain.roundsStorageContract.address,
51702
+ abi: client.chain.roundsStorageContract.abi,
51703
+ functionName: "getLastRoundData",
51704
+ args: [args.txId]
51705
+ });
51706
+ },
51707
+ /** Checks if a transaction can be appealed. */
51708
+ canAppeal: async (args) => {
51709
+ if (!client.chain.appealsContract?.address) {
51710
+ throw new Error("canAppeal not supported on this chain (missing appealsContract)");
51711
+ }
51712
+ return publicClient.readContract({
51713
+ address: client.chain.appealsContract.address,
51714
+ abi: client.chain.appealsContract.abi,
51715
+ functionName: "canAppeal",
51716
+ args: [args.txId]
51717
+ });
51718
+ },
51603
51719
  /** Appeals a consensus transaction to trigger a new round of validation. */
51604
51720
  appealTransaction: async (args) => {
51605
51721
  const { account, txId } = args;
@@ -51626,12 +51742,51 @@ var contractActions = (client, publicClient) => {
51626
51742
  }
51627
51743
  const senderAccount = account || client.account;
51628
51744
  const encodedData = _encodeSubmitAppealData({ client, txId });
51629
- return _sendTransaction({
51745
+ await _sendConsensusCall({
51630
51746
  client,
51631
51747
  publicClient,
51632
51748
  encodedData,
51633
51749
  senderAccount,
51634
- value
51750
+ value,
51751
+ operationName: "Appeal"
51752
+ });
51753
+ return txId;
51754
+ },
51755
+ /** Finalizes a single GenLayer transaction that is ready to be finalized. Returns the EVM transaction hash. */
51756
+ finalizeTransaction: async (args) => {
51757
+ const { account, txId } = args;
51758
+ const senderAccount = account || client.account;
51759
+ const encodedData = encodeFunctionData({
51760
+ abi: client.chain.consensusMainContract?.abi,
51761
+ functionName: "finalizeTransaction",
51762
+ args: [txId]
51763
+ });
51764
+ return _sendConsensusCall({
51765
+ client,
51766
+ publicClient,
51767
+ encodedData,
51768
+ senderAccount,
51769
+ operationName: "Finalize"
51770
+ });
51771
+ },
51772
+ /** Batch-finalizes idle GenLayer transactions (those stuck without progressing). Returns the EVM transaction hash. */
51773
+ finalizeIdlenessTxs: async (args) => {
51774
+ const { account, txIds } = args;
51775
+ if (txIds.length === 0) {
51776
+ throw new Error("finalizeIdlenessTxs requires at least one txId.");
51777
+ }
51778
+ const senderAccount = account || client.account;
51779
+ const encodedData = encodeFunctionData({
51780
+ abi: client.chain.consensusMainContract?.abi,
51781
+ functionName: "finalizeIdlenessTxs",
51782
+ args: [txIds]
51783
+ });
51784
+ return _sendConsensusCall({
51785
+ client,
51786
+ publicClient,
51787
+ encodedData,
51788
+ senderAccount,
51789
+ operationName: "Finalize idleness"
51635
51790
  });
51636
51791
  }
51637
51792
  };
@@ -51745,6 +51900,69 @@ var _encodeSubmitAppealData = ({
51745
51900
  args: [txId]
51746
51901
  });
51747
51902
  };
51903
+ var _sendConsensusCall = async ({
51904
+ client,
51905
+ publicClient,
51906
+ encodedData,
51907
+ senderAccount,
51908
+ value = 0n,
51909
+ operationName = "Consensus call"
51910
+ }) => {
51911
+ if (!client.chain.consensusMainContract?.address) {
51912
+ throw new Error("Consensus main contract not initialized.");
51913
+ }
51914
+ const validatedAccount = validateAccount(senderAccount);
51915
+ const nonce = await client.getCurrentNonce({ address: validatedAccount.address });
51916
+ let estimatedGas;
51917
+ try {
51918
+ estimatedGas = await client.estimateTransactionGas({
51919
+ to: client.chain.consensusMainContract.address,
51920
+ data: encodedData,
51921
+ value
51922
+ });
51923
+ } catch (err) {
51924
+ console.error("Gas estimation failed, using default 200_000:", err);
51925
+ estimatedGas = 200000n;
51926
+ }
51927
+ const gasPriceHex = await client.request({ method: "eth_gasPrice" });
51928
+ if (validatedAccount.type === "local") {
51929
+ if (!validatedAccount.signTransaction) {
51930
+ throw new Error("Local account does not support signTransaction.");
51931
+ }
51932
+ const txRequest = {
51933
+ account: validatedAccount,
51934
+ to: client.chain.consensusMainContract.address,
51935
+ data: encodedData,
51936
+ value,
51937
+ gas: estimatedGas,
51938
+ gasPrice: BigInt(gasPriceHex),
51939
+ nonce,
51940
+ chainId: client.chain.id
51941
+ };
51942
+ const serializedTransaction = await validatedAccount.signTransaction(txRequest);
51943
+ const evmHash2 = await client.sendRawTransaction({ serializedTransaction });
51944
+ const receipt2 = await publicClient.waitForTransactionReceipt({ hash: evmHash2 });
51945
+ if (receipt2.status === "reverted") {
51946
+ throw new Error(`${operationName} reverted: EVM tx ${evmHash2}`);
51947
+ }
51948
+ return evmHash2;
51949
+ }
51950
+ const evmHash = await client.request({
51951
+ method: "eth_sendTransaction",
51952
+ params: [{
51953
+ from: validatedAccount.address,
51954
+ to: client.chain.consensusMainContract.address,
51955
+ data: encodedData,
51956
+ value: value ? `0x${value.toString(16)}` : void 0,
51957
+ gas: `0x${estimatedGas.toString(16)}`
51958
+ }]
51959
+ });
51960
+ const receipt = await publicClient.waitForTransactionReceipt({ hash: evmHash });
51961
+ if (receipt.status === "reverted") {
51962
+ throw new Error(`${operationName} reverted: EVM tx ${evmHash}`);
51963
+ }
51964
+ return evmHash;
51965
+ };
51748
51966
  var isAddTransactionAbiMismatchError = (error) => {
51749
51967
  const seen = /* @__PURE__ */ new WeakSet();
51750
51968
  const serializedError = typeof error === "object" && error !== null ? JSON.stringify(error, (_key, value) => {
@@ -51874,6 +52092,9 @@ var _sendTransaction = async ({
51874
52092
  method: "eth_sendTransaction",
51875
52093
  params: [formattedRequest]
51876
52094
  });
52095
+ if (client.chain.isStudio) {
52096
+ return evmTxHash;
52097
+ }
51877
52098
  const externalReceipt = await publicClient.waitForTransactionReceipt({ hash: evmTxHash });
51878
52099
  if (externalReceipt.status === "reverted") {
51879
52100
  throw new Error(`Transaction reverted: EVM tx ${evmTxHash} to consensus contract ${client.chain.consensusMainContract?.address} was reverted.`);
@@ -52158,7 +52379,7 @@ var receiptActions = (client, publicClient) => ({
52158
52379
  const requestedStatus = transactionsStatusNameToNumber[status];
52159
52380
  if (transactionStatusString === requestedStatus || status === "ACCEPTED" && isDecidedState(transactionStatusString)) {
52160
52381
  let finalTransaction = transaction;
52161
- if (client.chain.id === localnet.id) {
52382
+ if (client.chain.isStudio) {
52162
52383
  finalTransaction = decodeLocalnetTransaction(transaction);
52163
52384
  }
52164
52385
  if (!fullTransaction) {
@@ -53017,6 +53238,21 @@ var PROVIDER_METHODS = /* @__PURE__ */ new Set([
53017
53238
  "personal_sign",
53018
53239
  "eth_signTypedData_v4"
53019
53240
  ]);
53241
+ var assertChainMatch = async (provider, chainConfig) => {
53242
+ if (chainConfig.isStudio) return;
53243
+ const expectedChainIdHex = `0x${chainConfig.id.toString(16)}`;
53244
+ try {
53245
+ const currentChainId = await provider.request({ method: "eth_chainId" });
53246
+ if (currentChainId !== expectedChainIdHex) {
53247
+ const currentId = parseInt(currentChainId, 16);
53248
+ throw new Error(
53249
+ `Wallet is on chain ${currentId} but client is configured for chain ${chainConfig.id} (${chainConfig.name}). Call client.connect("${chainConfig.name}") or switch your wallet to the correct network before sending transactions.`
53250
+ );
53251
+ }
53252
+ } catch (err) {
53253
+ if (err instanceof Error && err.message.startsWith("Wallet is on chain")) throw err;
53254
+ }
53255
+ };
53020
53256
  var getCustomTransportConfig = (config, chainConfig) => {
53021
53257
  const isAddress2 = typeof config.account !== "object";
53022
53258
  return {
@@ -53025,6 +53261,9 @@ var getCustomTransportConfig = (config, chainConfig) => {
53025
53261
  const provider = config.provider || (typeof window !== "undefined" ? window.ethereum : void 0);
53026
53262
  if (provider) {
53027
53263
  try {
53264
+ if (method === "eth_sendTransaction" || method === "eth_signTransaction") {
53265
+ await assertChainMatch(provider, chainConfig);
53266
+ }
53028
53267
  return await provider.request({ method, params });
53029
53268
  } catch (err) {
53030
53269
  console.warn(`Error using provider for method ${method}:`, err);
@@ -56011,6 +56250,41 @@ var TraceAction = class extends BaseAction {
56011
56250
  }
56012
56251
  };
56013
56252
 
56253
+ // src/commands/transactions/finalize.ts
56254
+ var FinalizeAction = class extends BaseAction {
56255
+ constructor() {
56256
+ super();
56257
+ }
56258
+ async finalize({ txId, rpc }) {
56259
+ const client = await this.getClient(rpc);
56260
+ this.startSpinner(`Finalizing transaction ${txId}...`);
56261
+ try {
56262
+ const evmHash = await client.finalizeTransaction({ txId });
56263
+ this.succeedSpinner("Transaction finalized", { txId, evmTransactionHash: evmHash });
56264
+ } catch (error) {
56265
+ this.failSpinner("Error finalizing transaction", error);
56266
+ }
56267
+ }
56268
+ async finalizeBatch({ txIds, rpc }) {
56269
+ if (txIds.length === 0) {
56270
+ this.failSpinner("At least one txId is required.");
56271
+ return;
56272
+ }
56273
+ const client = await this.getClient(rpc);
56274
+ this.startSpinner(`Finalizing ${txIds.length} idle transaction(s)...`);
56275
+ try {
56276
+ const evmHash = await client.finalizeIdlenessTxs({ txIds });
56277
+ this.succeedSpinner("Idle transactions finalized", {
56278
+ count: txIds.length,
56279
+ txIds,
56280
+ evmTransactionHash: evmHash
56281
+ });
56282
+ } catch (error) {
56283
+ this.failSpinner("Error finalizing idle transactions", error);
56284
+ }
56285
+ }
56286
+ };
56287
+
56014
56288
  // src/commands/transactions/index.ts
56015
56289
  function parseIntOption(value, fallback2) {
56016
56290
  const parsed = parseInt(value, 10);
@@ -56034,6 +56308,14 @@ function initializeTransactionsCommands(program2) {
56034
56308
  const traceAction = new TraceAction();
56035
56309
  await traceAction.trace({ txId, ...options });
56036
56310
  });
56311
+ program2.command("finalize <txId>").description("Finalize a transaction that is ready to be finalized (public call)").option("--rpc <rpcUrl>", "RPC URL for the network").action(async (txId, options) => {
56312
+ const finalizeAction = new FinalizeAction();
56313
+ await finalizeAction.finalize({ txId, ...options });
56314
+ });
56315
+ program2.command("finalize-batch <txIds...>").description("Finalize a batch of idle transactions in a single call (public call)").option("--rpc <rpcUrl>", "RPC URL for the network").action(async (txIds, options) => {
56316
+ const finalizeAction = new FinalizeAction();
56317
+ await finalizeAction.finalizeBatch({ txIds, ...options });
56318
+ });
56037
56319
  return program2;
56038
56320
  }
56039
56321
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genlayer",
3
- "version": "0.38.13",
3
+ "version": "0.38.15",
4
4
  "description": "GenLayer Command Line Tool",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -67,7 +67,7 @@
67
67
  "dotenv": "^17.0.0",
68
68
  "ethers": "^6.13.4",
69
69
  "fs-extra": "^11.3.0",
70
- "genlayer-js": "^0.27.9",
70
+ "genlayer-js": "^0.28.5",
71
71
  "inquirer": "^12.0.0",
72
72
  "keytar": "^7.9.0",
73
73
  "node-fetch": "^3.0.0",
@@ -0,0 +1,45 @@
1
+ import {TransactionHash} from "genlayer-js/types";
2
+ import {BaseAction} from "../../lib/actions/BaseAction";
3
+
4
+ export interface FinalizeOptions {
5
+ rpc?: string;
6
+ }
7
+
8
+ export class FinalizeAction extends BaseAction {
9
+ constructor() {
10
+ super();
11
+ }
12
+
13
+ async finalize({txId, rpc}: {txId: TransactionHash; rpc?: string}): Promise<void> {
14
+ const client = await this.getClient(rpc);
15
+
16
+ this.startSpinner(`Finalizing transaction ${txId}...`);
17
+ try {
18
+ const evmHash = await client.finalizeTransaction({txId});
19
+ this.succeedSpinner("Transaction finalized", {txId, evmTransactionHash: evmHash});
20
+ } catch (error) {
21
+ this.failSpinner("Error finalizing transaction", error);
22
+ }
23
+ }
24
+
25
+ async finalizeBatch({txIds, rpc}: {txIds: TransactionHash[]; rpc?: string}): Promise<void> {
26
+ if (txIds.length === 0) {
27
+ this.failSpinner("At least one txId is required.");
28
+ return;
29
+ }
30
+
31
+ const client = await this.getClient(rpc);
32
+
33
+ this.startSpinner(`Finalizing ${txIds.length} idle transaction(s)...`);
34
+ try {
35
+ const evmHash = await client.finalizeIdlenessTxs({txIds});
36
+ this.succeedSpinner("Idle transactions finalized", {
37
+ count: txIds.length,
38
+ txIds,
39
+ evmTransactionHash: evmHash,
40
+ });
41
+ } catch (error) {
42
+ this.failSpinner("Error finalizing idle transactions", error);
43
+ }
44
+ }
45
+ }
@@ -3,6 +3,7 @@ import {TransactionStatus, TransactionHash} from "genlayer-js/types";
3
3
  import {ReceiptAction, ReceiptOptions} from "./receipt";
4
4
  import {AppealAction, AppealOptions, AppealBondOptions} from "./appeal";
5
5
  import {TraceAction, TraceOptions} from "./trace";
6
+ import {FinalizeAction, FinalizeOptions} from "./finalize";
6
7
 
7
8
  function parseIntOption(value: string, fallback: number): number {
8
9
  const parsed = parseInt(value, 10);
@@ -56,5 +57,23 @@ export function initializeTransactionsCommands(program: Command) {
56
57
  await traceAction.trace({txId, ...options});
57
58
  });
58
59
 
60
+ program
61
+ .command("finalize <txId>")
62
+ .description("Finalize a transaction that is ready to be finalized (public call)")
63
+ .option("--rpc <rpcUrl>", "RPC URL for the network")
64
+ .action(async (txId: TransactionHash, options: FinalizeOptions) => {
65
+ const finalizeAction = new FinalizeAction();
66
+ await finalizeAction.finalize({txId, ...options});
67
+ });
68
+
69
+ program
70
+ .command("finalize-batch <txIds...>")
71
+ .description("Finalize a batch of idle transactions in a single call (public call)")
72
+ .option("--rpc <rpcUrl>", "RPC URL for the network")
73
+ .action(async (txIds: TransactionHash[], options: FinalizeOptions) => {
74
+ const finalizeAction = new FinalizeAction();
75
+ await finalizeAction.finalizeBatch({txIds, ...options});
76
+ });
77
+
59
78
  return program;
60
79
  }
@@ -0,0 +1,109 @@
1
+ import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
2
+ import {createClient, createAccount} from "genlayer-js";
3
+ import type {TransactionHash} from "genlayer-js/types";
4
+ import {FinalizeAction} from "../../src/commands/transactions/finalize";
5
+
6
+ vi.mock("genlayer-js", async (importOriginal) => {
7
+ const actual = await importOriginal<typeof import("genlayer-js")>();
8
+ return {
9
+ ...actual,
10
+ createClient: vi.fn(),
11
+ createAccount: vi.fn(),
12
+ };
13
+ });
14
+
15
+ describe("FinalizeAction", () => {
16
+ let finalizeAction: FinalizeAction;
17
+ const mockClient = {
18
+ finalizeTransaction: vi.fn(),
19
+ finalizeIdlenessTxs: vi.fn(),
20
+ };
21
+
22
+ const mockPrivateKey = "mocked_private_key";
23
+ const mockTxId = "0x1234567890123456789012345678901234567890123456789012345678901234" as TransactionHash;
24
+ const mockTxId2 = "0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd" as TransactionHash;
25
+ const mockEvmHash = "0xdeadbeef" as `0x${string}`;
26
+
27
+ beforeEach(() => {
28
+ vi.clearAllMocks();
29
+ vi.mocked(createClient).mockReturnValue(mockClient as any);
30
+ vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
31
+ finalizeAction = new FinalizeAction();
32
+ vi.spyOn(finalizeAction as any, "getAccount").mockResolvedValue({privateKey: mockPrivateKey});
33
+
34
+ vi.spyOn(finalizeAction as any, "startSpinner").mockImplementation(() => {});
35
+ vi.spyOn(finalizeAction as any, "succeedSpinner").mockImplementation(() => {});
36
+ vi.spyOn(finalizeAction as any, "failSpinner").mockImplementation(() => {});
37
+ });
38
+
39
+ afterEach(() => {
40
+ vi.restoreAllMocks();
41
+ });
42
+
43
+ test("finalize calls client.finalizeTransaction and reports the EVM hash", async () => {
44
+ vi.mocked(mockClient.finalizeTransaction).mockResolvedValue(mockEvmHash);
45
+
46
+ await finalizeAction.finalize({txId: mockTxId});
47
+
48
+ expect(mockClient.finalizeTransaction).toHaveBeenCalledWith({txId: mockTxId});
49
+ expect(finalizeAction["succeedSpinner"]).toHaveBeenCalledWith(
50
+ "Transaction finalized",
51
+ {txId: mockTxId, evmTransactionHash: mockEvmHash},
52
+ );
53
+ });
54
+
55
+ test("finalize surfaces underlying errors via failSpinner", async () => {
56
+ vi.mocked(mockClient.finalizeTransaction).mockRejectedValue(new Error("boom"));
57
+
58
+ await finalizeAction.finalize({txId: mockTxId});
59
+
60
+ expect(finalizeAction["failSpinner"]).toHaveBeenCalledWith(
61
+ "Error finalizing transaction",
62
+ expect.any(Error),
63
+ );
64
+ });
65
+
66
+ test("finalize uses custom RPC URL when provided", async () => {
67
+ vi.mocked(mockClient.finalizeTransaction).mockResolvedValue(mockEvmHash);
68
+
69
+ await finalizeAction.finalize({txId: mockTxId, rpc: "https://custom.com"});
70
+
71
+ expect(createClient).toHaveBeenCalledWith(
72
+ expect.objectContaining({endpoint: "https://custom.com"}),
73
+ );
74
+ });
75
+
76
+ test("finalizeBatch calls client.finalizeIdlenessTxs with all ids", async () => {
77
+ vi.mocked(mockClient.finalizeIdlenessTxs).mockResolvedValue(mockEvmHash);
78
+
79
+ await finalizeAction.finalizeBatch({txIds: [mockTxId, mockTxId2]});
80
+
81
+ expect(mockClient.finalizeIdlenessTxs).toHaveBeenCalledWith({
82
+ txIds: [mockTxId, mockTxId2],
83
+ });
84
+ expect(finalizeAction["succeedSpinner"]).toHaveBeenCalledWith(
85
+ "Idle transactions finalized",
86
+ {count: 2, txIds: [mockTxId, mockTxId2], evmTransactionHash: mockEvmHash},
87
+ );
88
+ });
89
+
90
+ test("finalizeBatch rejects an empty list without calling the client", async () => {
91
+ await finalizeAction.finalizeBatch({txIds: []});
92
+
93
+ expect(mockClient.finalizeIdlenessTxs).not.toHaveBeenCalled();
94
+ expect(finalizeAction["failSpinner"]).toHaveBeenCalledWith(
95
+ "At least one txId is required.",
96
+ );
97
+ });
98
+
99
+ test("finalizeBatch surfaces underlying errors via failSpinner", async () => {
100
+ vi.mocked(mockClient.finalizeIdlenessTxs).mockRejectedValue(new Error("revert"));
101
+
102
+ await finalizeAction.finalizeBatch({txIds: [mockTxId]});
103
+
104
+ expect(finalizeAction["failSpinner"]).toHaveBeenCalledWith(
105
+ "Error finalizing idle transactions",
106
+ expect.any(Error),
107
+ );
108
+ });
109
+ });
@@ -0,0 +1,83 @@
1
+ import {Command} from "commander";
2
+ import {FinalizeAction} from "../../src/commands/transactions/finalize";
3
+ import {vi, describe, beforeEach, afterEach, test, expect} from "vitest";
4
+ import {initializeTransactionsCommands} from "../../src/commands/transactions";
5
+
6
+ vi.mock("../../src/commands/transactions/finalize");
7
+
8
+ describe("finalize command", () => {
9
+ let program: Command;
10
+ const mockTxId = "0x1234567890123456789012345678901234567890123456789012345678901234";
11
+
12
+ beforeEach(() => {
13
+ program = new Command();
14
+ initializeTransactionsCommands(program);
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ afterEach(() => {
19
+ vi.restoreAllMocks();
20
+ });
21
+
22
+ test("FinalizeAction.finalize is called with txId", async () => {
23
+ program.parse(["node", "test", "finalize", mockTxId]);
24
+ expect(FinalizeAction).toHaveBeenCalledTimes(1);
25
+ expect(FinalizeAction.prototype.finalize).toHaveBeenCalledWith({txId: mockTxId});
26
+ });
27
+
28
+ test("FinalizeAction.finalize is called with custom RPC URL", async () => {
29
+ program.parse(["node", "test", "finalize", mockTxId, "--rpc", "https://custom.com"]);
30
+ expect(FinalizeAction.prototype.finalize).toHaveBeenCalledWith({
31
+ txId: mockTxId,
32
+ rpc: "https://custom.com",
33
+ });
34
+ });
35
+
36
+ test("throws error for unrecognized options", async () => {
37
+ const finalizeCommand = program.commands.find(cmd => cmd.name() === "finalize");
38
+ finalizeCommand?.exitOverride();
39
+ expect(() =>
40
+ program.parse(["node", "test", "finalize", mockTxId, "--invalid-option"]),
41
+ ).toThrowError("error: unknown option '--invalid-option'");
42
+ });
43
+ });
44
+
45
+ describe("finalize-batch command", () => {
46
+ let program: Command;
47
+ const mockTxId1 = "0x1111111111111111111111111111111111111111111111111111111111111111";
48
+ const mockTxId2 = "0x2222222222222222222222222222222222222222222222222222222222222222";
49
+
50
+ beforeEach(() => {
51
+ program = new Command();
52
+ initializeTransactionsCommands(program);
53
+ vi.clearAllMocks();
54
+ });
55
+
56
+ afterEach(() => {
57
+ vi.restoreAllMocks();
58
+ });
59
+
60
+ test("FinalizeAction.finalizeBatch is called with a single txId", async () => {
61
+ program.parse(["node", "test", "finalize-batch", mockTxId1]);
62
+ expect(FinalizeAction.prototype.finalizeBatch).toHaveBeenCalledWith({
63
+ txIds: [mockTxId1],
64
+ });
65
+ });
66
+
67
+ test("FinalizeAction.finalizeBatch is called with multiple txIds", async () => {
68
+ program.parse(["node", "test", "finalize-batch", mockTxId1, mockTxId2]);
69
+ expect(FinalizeAction.prototype.finalizeBatch).toHaveBeenCalledWith({
70
+ txIds: [mockTxId1, mockTxId2],
71
+ });
72
+ });
73
+
74
+ test("FinalizeAction.finalizeBatch is called with custom RPC", async () => {
75
+ program.parse([
76
+ "node", "test", "finalize-batch", mockTxId1, mockTxId2, "--rpc", "https://custom.com",
77
+ ]);
78
+ expect(FinalizeAction.prototype.finalizeBatch).toHaveBeenCalledWith({
79
+ txIds: [mockTxId1, mockTxId2],
80
+ rpc: "https://custom.com",
81
+ });
82
+ });
83
+ });