genlayer 0.38.14 → 0.38.16
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/.github/workflows/publish.yml +1 -3
- package/.github/workflows/smoke.yml +16 -0
- package/CHANGELOG.md +12 -0
- package/dist/index.js +2556 -2941
- package/package.json +2 -2
- package/src/commands/account/send.ts +13 -11
- package/src/commands/transactions/finalize.ts +45 -0
- package/src/commands/transactions/index.ts +19 -0
- package/tests/actions/finalize.test.ts +109 -0
- package/tests/commands/finalize.test.ts +83 -0
- package/tests/smoke.test.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genlayer",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.16",
|
|
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.
|
|
70
|
+
"genlayer-js": "^1.0.0",
|
|
71
71
|
"inquirer": "^12.0.0",
|
|
72
72
|
"keytar": "^7.9.0",
|
|
73
73
|
"node-fetch": "^3.0.0",
|
|
@@ -31,18 +31,20 @@ export class SendAction extends BaseAction {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
private parseAmount(amount: string): bigint {
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
// Symmetric with genlayer-js `parseStakingAmount`:
|
|
35
|
+
// "Ngen" → N GEN (parseEther)
|
|
36
|
+
// integer → wei (as-is)
|
|
37
|
+
// decimal without suffix → rejected (ambiguous; was previously silently
|
|
38
|
+
// interpreted as GEN for small values and as wei for large — a footgun
|
|
39
|
+
// that made `send 1000` attempt to transfer 1000 GEN, not 1000 wei).
|
|
40
|
+
const trimmed = amount.trim();
|
|
41
|
+
const lower = trimmed.toLowerCase();
|
|
42
|
+
if (lower.endsWith("gen")) {
|
|
43
|
+
return parseEther(lower.slice(0, -3).trim());
|
|
39
44
|
}
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
// Otherwise assume it's in GEN
|
|
45
|
-
return parseEther(amount);
|
|
45
|
+
// Plain integer → wei. BigInt() throws on decimals, which is the intended
|
|
46
|
+
// failure mode for ambiguous input like "1.5".
|
|
47
|
+
return BigInt(trimmed);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
async execute(options: SendOptions): Promise<void> {
|
|
@@ -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
|
+
});
|
package/tests/smoke.test.ts
CHANGED
|
@@ -7,7 +7,10 @@ import type {Address, GenLayerChain} from "genlayer-js/types";
|
|
|
7
7
|
|
|
8
8
|
const CLI = path.resolve(__dirname, "../dist/index.js");
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Testnet validator-list fetches ALL validators + per-validator detail in
|
|
11
|
+
// batches; on bradbury/asimov that routinely passes 30s. 90s gives headroom
|
|
12
|
+
// without hiding real hangs.
|
|
13
|
+
const TIMEOUT = 90_000;
|
|
11
14
|
|
|
12
15
|
const testnets: {name: string; chain: GenLayerChain}[] = [
|
|
13
16
|
{name: "Asimov", chain: testnetAsimov},
|