naracli 1.0.17 → 1.0.22
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/README.md +101 -114
- package/bin/nara-cli.ts +0 -20
- package/dist/nara-cli.mjs +49930 -2222
- package/index.ts +10 -58
- package/package.json +7 -6
- package/src/cli/commands/quest.ts +8 -7
- package/src/cli/commands/skills.ts +491 -0
- package/src/cli/commands/skillsInstall.ts +793 -0
- package/src/cli/commands/wallet.ts +13 -114
- package/src/cli/commands/zkid.ts +410 -0
- package/src/cli/index.ts +215 -9
- package/src/cli/prompts/searchMultiselect.ts +297 -0
- package/src/cli/types.ts +0 -138
- package/src/cli/utils/transaction.ts +1 -1
- package/src/cli/utils/validation.ts +0 -40
- package/src/cli/utils/wallet.ts +3 -1
- package/src/tests/helpers.ts +78 -0
- package/src/tests/skills.e2e.test.ts +126 -0
- package/src/tests/skills.test.ts +192 -0
- package/src/tests/test_skill.md +18 -0
- package/src/tests/zkid.e2e.test.ts +128 -0
- package/src/tests/zkid.test.ts +153 -0
- package/src/types/snarkjs.d.ts +4 -1
- package/dist/quest/nara_quest.json +0 -534
- package/dist/zk/answer_proof.wasm +0 -0
- package/dist/zk/answer_proof_final.zkey +0 -0
- package/src/cli/commands/config.ts +0 -125
- package/src/cli/commands/migrate.ts +0 -270
- package/src/cli/commands/pool.ts +0 -364
- package/src/cli/commands/swap.ts +0 -349
- package/src/cli/quest/nara_quest.json +0 -534
- package/src/cli/quest/nara_quest_types.ts +0 -540
- package/src/cli/zk/answer_proof.wasm +0 -0
- package/src/cli/zk/answer_proof_final.zkey +0 -0
- package/src/client.ts +0 -96
- package/src/config.ts +0 -132
- package/src/constants.ts +0 -35
- package/src/migrate.ts +0 -222
- package/src/pool.ts +0 -259
- package/src/quest.ts +0 -387
- package/src/swap.ts +0 -608
|
@@ -66,46 +66,6 @@ export function validateNonNegativeNumber(
|
|
|
66
66
|
return num;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/**
|
|
70
|
-
* Validate swap mode string
|
|
71
|
-
* @param mode Mode string
|
|
72
|
-
* @returns Validated mode string
|
|
73
|
-
* @throws Error if invalid
|
|
74
|
-
*/
|
|
75
|
-
export function validateSwapMode(mode: string): string {
|
|
76
|
-
const validModes = ["exact-in", "partial-fill", "exact-out"];
|
|
77
|
-
const normalized = mode.toLowerCase();
|
|
78
|
-
|
|
79
|
-
if (!validModes.includes(normalized)) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`Invalid swap mode: ${mode}. Must be one of: ${validModes.join(", ")}`
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return normalized;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Validate direction string
|
|
90
|
-
* @param direction Direction string
|
|
91
|
-
* @returns Validated direction string
|
|
92
|
-
* @throws Error if invalid
|
|
93
|
-
*/
|
|
94
|
-
export function validateDirection(direction: string): string {
|
|
95
|
-
const validDirections = ["buy", "sell"];
|
|
96
|
-
const normalized = direction.toLowerCase();
|
|
97
|
-
|
|
98
|
-
if (!validDirections.includes(normalized)) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Invalid direction: ${direction}. Must be one of: ${validDirections.join(
|
|
101
|
-
", "
|
|
102
|
-
)}`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return normalized;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
69
|
/**
|
|
110
70
|
* Validate required option
|
|
111
71
|
* @param value Option value
|
package/src/cli/utils/wallet.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
import { Keypair } from "@solana/web3.js";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
-
import { DEFAULT_RPC_URL
|
|
8
|
+
import { DEFAULT_RPC_URL } from "nara-sdk";
|
|
9
|
+
|
|
10
|
+
const _DEFAULT_WALLET_PATH = process.env.WALLET_PATH || "~/.config/nara/id.json";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Resolve wallet path (expand ~ to home directory)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers - run the CLI as a subprocess and capture output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const CLI = join(__dirname, "../../bin/nara-cli.ts");
|
|
12
|
+
const ROOT = join(__dirname, "../../");
|
|
13
|
+
const ENV_FILE = join(ROOT, ".env");
|
|
14
|
+
|
|
15
|
+
/** Parse .env file into an object (simple key=value, skips comments) */
|
|
16
|
+
function loadEnvFile(path: string): Record<string, string> {
|
|
17
|
+
if (!existsSync(path)) return {};
|
|
18
|
+
const env: Record<string, string> = {};
|
|
19
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
22
|
+
const idx = trimmed.indexOf("=");
|
|
23
|
+
if (idx < 0) continue;
|
|
24
|
+
const key = trimmed.slice(0, idx).trim();
|
|
25
|
+
const val = trimmed.slice(idx + 1).trim();
|
|
26
|
+
if (key) env[key] = val;
|
|
27
|
+
}
|
|
28
|
+
return env;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ENV = loadEnvFile(ENV_FILE);
|
|
32
|
+
|
|
33
|
+
export interface CliResult {
|
|
34
|
+
stdout: string;
|
|
35
|
+
stderr: string;
|
|
36
|
+
exitCode: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run the CLI with given args, stream stdout/stderr to parent in real-time
|
|
41
|
+
* while also capturing them for assertions.
|
|
42
|
+
*/
|
|
43
|
+
export function runCli(args: string[], extraEnv: Record<string, string> = {}): Promise<CliResult> {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const child = spawn("npx", ["tsx", CLI, ...args], {
|
|
46
|
+
env: { ...process.env, ...ENV, ...extraEnv },
|
|
47
|
+
timeout: 120_000,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
let stdout = "";
|
|
51
|
+
let stderr = "";
|
|
52
|
+
|
|
53
|
+
child.stdout.setEncoding("utf8");
|
|
54
|
+
child.stderr.setEncoding("utf8");
|
|
55
|
+
|
|
56
|
+
child.stdout.on("data", (chunk: string) => {
|
|
57
|
+
stdout += chunk;
|
|
58
|
+
process.stdout.write(chunk);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
child.stderr.on("data", (chunk: string) => {
|
|
62
|
+
stderr += chunk;
|
|
63
|
+
process.stderr.write(chunk);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
child.on("close", (code) => {
|
|
67
|
+
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether a wallet is configured (for write-command tests) */
|
|
73
|
+
export const hasWallet = !!(ENV.PRIVATE_KEY || process.env.PRIVATE_KEY);
|
|
74
|
+
|
|
75
|
+
/** Generate a unique test resource name using a timestamp */
|
|
76
|
+
export function uniqueName(prefix: string): string {
|
|
77
|
+
return `${prefix}-${Date.now().toString(36)}`;
|
|
78
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end tests for `skills` commands (hits real testnet)
|
|
3
|
+
*
|
|
4
|
+
* Requires PRIVATE_KEY in .env and sufficient NARA balance.
|
|
5
|
+
* Run: npm run test:e2e
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, before, after } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { writeFileSync, unlinkSync, mkdtempSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { runCli, hasWallet, uniqueName } from "./helpers.js";
|
|
14
|
+
|
|
15
|
+
if (!hasWallet) {
|
|
16
|
+
console.log("Skipping skills e2e tests: PRIVATE_KEY not set");
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Shared state across tests ────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const SKILL_NAME = uniqueName("e2e-skill");
|
|
23
|
+
const AUTHOR = "e2e-test-author";
|
|
24
|
+
let tmpDir: string;
|
|
25
|
+
let contentFile: string;
|
|
26
|
+
|
|
27
|
+
// ─── Setup ────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
before(() => {
|
|
30
|
+
tmpDir = mkdtempSync(join(tmpdir(), "naracli-test-"));
|
|
31
|
+
contentFile = join(tmpDir, "skill-content.md");
|
|
32
|
+
writeFileSync(contentFile, `# ${SKILL_NAME}\n\nThis is e2e test content.\n`);
|
|
33
|
+
console.log(`\nUsing skill name: ${SKILL_NAME}`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ─── Cleanup ──────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
after(async () => {
|
|
39
|
+
try { unlinkSync(contentFile); } catch {}
|
|
40
|
+
// Delete on-chain skill (reclaim rent)
|
|
41
|
+
const { exitCode, stderr } = await runCli(["skills", "delete", SKILL_NAME]);
|
|
42
|
+
if (exitCode !== 0) {
|
|
43
|
+
console.warn(` Warning: failed to delete skill "${SKILL_NAME}": ${stderr}`);
|
|
44
|
+
} else {
|
|
45
|
+
console.log(` Cleaned up skill "${SKILL_NAME}"`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ─── Tests ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe(`skills e2e (name=${SKILL_NAME})`, () => {
|
|
52
|
+
it("register skill on-chain", async () => {
|
|
53
|
+
const { stdout, stderr, exitCode } = await runCli([
|
|
54
|
+
"skills", "register", SKILL_NAME, AUTHOR,
|
|
55
|
+
]);
|
|
56
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
57
|
+
assert.ok(stdout.includes("registered") || stdout.includes("Transaction"), `stdout: ${stdout}`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("get skill returns correct name and author", async () => {
|
|
61
|
+
const { stdout, exitCode, stderr } = await runCli([
|
|
62
|
+
"skills", "get", "--json", SKILL_NAME,
|
|
63
|
+
]);
|
|
64
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
65
|
+
const json = JSON.parse(stdout);
|
|
66
|
+
assert.equal(json.name, SKILL_NAME);
|
|
67
|
+
assert.equal(json.author, AUTHOR);
|
|
68
|
+
assert.equal(json.version, 0);
|
|
69
|
+
assert.equal(json.description, null);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("set-description updates description", async () => {
|
|
73
|
+
const desc = "An e2e test skill";
|
|
74
|
+
const { exitCode, stderr } = await runCli([
|
|
75
|
+
"skills", "set-description", SKILL_NAME, desc,
|
|
76
|
+
]);
|
|
77
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
78
|
+
|
|
79
|
+
const { stdout } = await runCli(["skills", "get", "--json", SKILL_NAME]);
|
|
80
|
+
const json = JSON.parse(stdout);
|
|
81
|
+
assert.equal(json.description, desc);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("set-metadata updates metadata", async () => {
|
|
85
|
+
const meta = JSON.stringify({ env: "e2e", version: "1.0.0" });
|
|
86
|
+
const { exitCode, stderr } = await runCli([
|
|
87
|
+
"skills", "set-metadata", SKILL_NAME, meta,
|
|
88
|
+
]);
|
|
89
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
90
|
+
|
|
91
|
+
const { stdout } = await runCli(["skills", "get", "--json", SKILL_NAME]);
|
|
92
|
+
const json = JSON.parse(stdout);
|
|
93
|
+
assert.equal(json.metadata, meta);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("upload content and read back matches original", async () => {
|
|
97
|
+
const { exitCode, stderr } = await runCli([
|
|
98
|
+
"skills", "upload", SKILL_NAME, contentFile,
|
|
99
|
+
]);
|
|
100
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
101
|
+
|
|
102
|
+
// Read back via skills content
|
|
103
|
+
const { stdout: contentOut, exitCode: cExit } = await runCli([
|
|
104
|
+
"skills", "content", SKILL_NAME,
|
|
105
|
+
]);
|
|
106
|
+
assert.equal(cExit, 0);
|
|
107
|
+
assert.ok(contentOut.includes(SKILL_NAME), `content mismatch: ${contentOut}`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("skills content --json includes size field", async () => {
|
|
111
|
+
const { stdout, exitCode } = await runCli([
|
|
112
|
+
"skills", "content", "--json", SKILL_NAME,
|
|
113
|
+
]);
|
|
114
|
+
assert.equal(exitCode, 0);
|
|
115
|
+
const json = JSON.parse(stdout);
|
|
116
|
+
assert.ok(typeof json.size === "number" && json.size > 0);
|
|
117
|
+
assert.ok(typeof json.content === "string" && json.content.length > 0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("get skill shows updated version after upload", async () => {
|
|
121
|
+
const { stdout, exitCode } = await runCli(["skills", "get", "--json", SKILL_NAME]);
|
|
122
|
+
assert.equal(exitCode, 0);
|
|
123
|
+
const json = JSON.parse(stdout);
|
|
124
|
+
assert.ok(json.version >= 1, `expected version >= 1, got ${json.version}`);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `skills` CLI commands
|
|
3
|
+
*
|
|
4
|
+
* Run: npm test
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, after } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { runCli, hasWallet, uniqueName } from "./helpers.js";
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const TEST_SKILL_FILE = join(__dirname, "test_skill.md");
|
|
15
|
+
|
|
16
|
+
// ─── Help output ──────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe("skills --help", () => {
|
|
19
|
+
it("shows all subcommands", async () => {
|
|
20
|
+
const { stdout, exitCode } = await runCli(["skills", "--help"]);
|
|
21
|
+
assert.equal(exitCode, 0);
|
|
22
|
+
for (const cmd of ["register", "get", "content", "set-description", "set-metadata", "upload", "transfer", "close-buffer", "delete"]) {
|
|
23
|
+
assert.ok(stdout.includes(cmd), `missing command: ${cmd}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("skills register --help shows <name> and <author>", async () => {
|
|
28
|
+
const { stdout, exitCode } = await runCli(["skills", "register", "--help"]);
|
|
29
|
+
assert.equal(exitCode, 0);
|
|
30
|
+
assert.ok(stdout.includes("<name>"));
|
|
31
|
+
assert.ok(stdout.includes("<author>"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("skills upload --help shows <file>", async () => {
|
|
35
|
+
const { stdout, exitCode } = await runCli(["skills", "upload", "--help"]);
|
|
36
|
+
assert.equal(exitCode, 0);
|
|
37
|
+
assert.ok(stdout.includes("<file>"));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("skills content --help shows --hex option", async () => {
|
|
41
|
+
const { stdout, exitCode } = await runCli(["skills", "content", "--help"]);
|
|
42
|
+
assert.equal(exitCode, 0);
|
|
43
|
+
assert.ok(stdout.includes("--hex"));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── Argument validation (no chain needed) ────────────────────────
|
|
48
|
+
|
|
49
|
+
describe("skills argument errors", () => {
|
|
50
|
+
it("skills get with no name exits non-zero", async () => {
|
|
51
|
+
const { exitCode } = await runCli(["skills", "get"]);
|
|
52
|
+
assert.notEqual(exitCode, 0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("skills register with missing author exits non-zero", async () => {
|
|
56
|
+
const { exitCode } = await runCli(["skills", "register", "myskill"]);
|
|
57
|
+
assert.notEqual(exitCode, 0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("skills set-metadata rejects invalid JSON", async () => {
|
|
61
|
+
if (!hasWallet) return;
|
|
62
|
+
const { stderr, exitCode } = await runCli(["skills", "set-metadata", "anyskill", "not-valid-json"]);
|
|
63
|
+
assert.equal(exitCode, 1);
|
|
64
|
+
assert.ok(stderr.includes("Invalid JSON"), `stderr: ${stderr}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("skills transfer rejects invalid public key", async () => {
|
|
68
|
+
if (!hasWallet) return;
|
|
69
|
+
const { stderr, exitCode } = await runCli(["skills", "transfer", "anyskill", "not-a-pubkey"]);
|
|
70
|
+
assert.equal(exitCode, 1);
|
|
71
|
+
assert.ok(stderr.includes("Invalid public key"), `stderr: ${stderr}`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("skills upload with non-existent file fails", async () => {
|
|
75
|
+
if (!hasWallet) return;
|
|
76
|
+
const { exitCode, stderr } = await runCli(["skills", "upload", "anyskill", "/tmp/__no_such_file__.bin"]);
|
|
77
|
+
assert.equal(exitCode, 1);
|
|
78
|
+
assert.ok(stderr.length > 0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─── Read-only chain queries ──────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
describe("skills read-only queries", () => {
|
|
85
|
+
it("skills get non-existent skill exits with error", async () => {
|
|
86
|
+
const { exitCode, stderr } = await runCli(["skills", "get", "definitely-does-not-exist-xyz123"]);
|
|
87
|
+
assert.equal(exitCode, 1);
|
|
88
|
+
assert.ok(stderr.length > 0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ─── Success cases (requires wallet) ─────────────────────────────
|
|
93
|
+
|
|
94
|
+
describe("skills success cases", () => {
|
|
95
|
+
const SKILL_NAME = uniqueName("unit-skill");
|
|
96
|
+
const AUTHOR = "unit-test-author";
|
|
97
|
+
|
|
98
|
+
after(async () => {
|
|
99
|
+
if (!hasWallet) return;
|
|
100
|
+
await runCli(["skills", "delete", SKILL_NAME]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("register a new skill on-chain", async () => {
|
|
104
|
+
if (!hasWallet) return;
|
|
105
|
+
const { stdout, stderr, exitCode } = await runCli([
|
|
106
|
+
"skills", "register", SKILL_NAME, AUTHOR,
|
|
107
|
+
]);
|
|
108
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
109
|
+
assert.ok(stdout.includes("Transaction"), `stdout: ${stdout}`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("get --json returns correct name, author and initial state", async () => {
|
|
113
|
+
if (!hasWallet) return;
|
|
114
|
+
const { stdout, exitCode, stderr } = await runCli([
|
|
115
|
+
"skills", "get", "--json", SKILL_NAME,
|
|
116
|
+
]);
|
|
117
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
118
|
+
const json = JSON.parse(stdout);
|
|
119
|
+
assert.equal(json.name, SKILL_NAME);
|
|
120
|
+
assert.equal(json.author, AUTHOR);
|
|
121
|
+
assert.equal(json.version, 0);
|
|
122
|
+
assert.equal(json.description, null);
|
|
123
|
+
assert.equal(json.metadata, null);
|
|
124
|
+
assert.ok(typeof json.authority === "string" && json.authority.length > 0);
|
|
125
|
+
assert.ok(typeof json.createdAt === "string");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("set-description updates description on-chain", async () => {
|
|
129
|
+
if (!hasWallet) return;
|
|
130
|
+
const desc = "A unit-test skill description";
|
|
131
|
+
const { exitCode, stderr } = await runCli([
|
|
132
|
+
"skills", "set-description", SKILL_NAME, desc,
|
|
133
|
+
]);
|
|
134
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
135
|
+
|
|
136
|
+
const { stdout } = await runCli(["skills", "get", "--json", SKILL_NAME]);
|
|
137
|
+
assert.equal(JSON.parse(stdout).description, desc);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("set-metadata updates metadata on-chain", async () => {
|
|
141
|
+
if (!hasWallet) return;
|
|
142
|
+
const meta = JSON.stringify({ env: "unit-test", ok: true });
|
|
143
|
+
const { exitCode, stderr } = await runCli([
|
|
144
|
+
"skills", "set-metadata", SKILL_NAME, meta,
|
|
145
|
+
]);
|
|
146
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
147
|
+
|
|
148
|
+
const { stdout } = await runCli(["skills", "get", "--json", SKILL_NAME]);
|
|
149
|
+
assert.equal(JSON.parse(stdout).metadata, meta);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("upload test_skill.md uploads content on-chain", async () => {
|
|
153
|
+
if (!hasWallet) return;
|
|
154
|
+
const { stdout, stderr, exitCode } = await runCli([
|
|
155
|
+
"skills", "upload", SKILL_NAME, TEST_SKILL_FILE,
|
|
156
|
+
]);
|
|
157
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
158
|
+
assert.ok(stdout.includes("Transaction") || stdout.includes("Finalize"), `stdout: ${stdout}`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("get --json shows version=1 and non-null updatedAt after upload", async () => {
|
|
162
|
+
if (!hasWallet) return;
|
|
163
|
+
const { stdout, exitCode, stderr } = await runCli([
|
|
164
|
+
"skills", "get", "--json", SKILL_NAME,
|
|
165
|
+
]);
|
|
166
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
167
|
+
const json = JSON.parse(stdout);
|
|
168
|
+
assert.equal(json.version, 1);
|
|
169
|
+
assert.ok(json.updatedAt !== null, "updatedAt should be set after content upload");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("content --json reads back uploaded file", async () => {
|
|
173
|
+
if (!hasWallet) return;
|
|
174
|
+
const { stdout, exitCode, stderr } = await runCli([
|
|
175
|
+
"skills", "content", "--json", SKILL_NAME,
|
|
176
|
+
]);
|
|
177
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
178
|
+
const json = JSON.parse(stdout);
|
|
179
|
+
assert.ok(typeof json.size === "number" && json.size > 0);
|
|
180
|
+
assert.ok(typeof json.content === "string" && json.content.length > 0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("delete removes the skill from chain", async () => {
|
|
184
|
+
if (!hasWallet) return;
|
|
185
|
+
const { exitCode, stderr } = await runCli(["skills", "delete", SKILL_NAME]);
|
|
186
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
187
|
+
|
|
188
|
+
// Verify it's gone
|
|
189
|
+
const { exitCode: getExit } = await runCli(["skills", "get", SKILL_NAME]);
|
|
190
|
+
assert.equal(getExit, 1);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
2
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
3
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
4
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
5
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
6
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
7
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
8
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
9
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
10
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
11
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
12
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
13
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
14
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
15
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
16
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
17
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
18
|
+
TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL TEST SKILL
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end tests for `zkid` commands (hits real testnet)
|
|
3
|
+
*
|
|
4
|
+
* Requires PRIVATE_KEY in .env and sufficient NARA balance.
|
|
5
|
+
* Run: npm run test:e2e
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { runCli, hasWallet, uniqueName } from "./helpers.js";
|
|
11
|
+
|
|
12
|
+
if (!hasWallet) {
|
|
13
|
+
console.log("Skipping zkid e2e tests: PRIVATE_KEY not set");
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Shared state ─────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const ZKID_NAME = uniqueName("e2e-zkid");
|
|
20
|
+
console.log(`\nUsing ZK ID name: ${ZKID_NAME}`);
|
|
21
|
+
|
|
22
|
+
// ─── Tests ────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe(`zkid e2e (name=${ZKID_NAME})`, () => {
|
|
25
|
+
// ── id-commitment (pure crypto, no chain) ──────────────────────
|
|
26
|
+
|
|
27
|
+
it("id-commitment outputs a deterministic 64-char hex", async () => {
|
|
28
|
+
const r1 = await runCli(["zkid", "id-commitment", "--json", ZKID_NAME]);
|
|
29
|
+
const r2 = await runCli(["zkid", "id-commitment", "--json", ZKID_NAME]);
|
|
30
|
+
assert.equal(r1.exitCode, 0, `stderr: ${r1.stderr}`);
|
|
31
|
+
assert.equal(r2.exitCode, 0);
|
|
32
|
+
const c1 = JSON.parse(r1.stdout);
|
|
33
|
+
const c2 = JSON.parse(r2.stdout);
|
|
34
|
+
assert.match(c1.idCommitment, /^[0-9a-f]{64}$/);
|
|
35
|
+
assert.equal(c1.idCommitment, c2.idCommitment, "commitment not deterministic");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ── create ─────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
it("create registers a new ZK ID on-chain", async () => {
|
|
41
|
+
const { stdout, stderr, exitCode } = await runCli([
|
|
42
|
+
"zkid", "create", ZKID_NAME,
|
|
43
|
+
]);
|
|
44
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
45
|
+
assert.ok(stdout.includes("registered") || stdout.includes("Transaction"), `stdout: ${stdout}`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("create again exits 0 with warning (already exists)", async () => {
|
|
49
|
+
const { stdout, exitCode } = await runCli(["zkid", "create", ZKID_NAME]);
|
|
50
|
+
assert.equal(exitCode, 0);
|
|
51
|
+
assert.ok(stdout.includes("already exists"), `stdout: ${stdout}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ── info ───────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
it("info returns correct fields for existing ZK ID", async () => {
|
|
57
|
+
const { stdout, exitCode, stderr } = await runCli([
|
|
58
|
+
"zkid", "info", "--json", ZKID_NAME,
|
|
59
|
+
]);
|
|
60
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
61
|
+
const json = JSON.parse(stdout);
|
|
62
|
+
assert.equal(json.name, ZKID_NAME);
|
|
63
|
+
assert.equal(json.depositCount, 0);
|
|
64
|
+
assert.equal(json.commitmentStartIndex, 0);
|
|
65
|
+
assert.match(json.idCommitment, /^[0-9a-f]{64}$/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("id-commitment matches the commitment stored on-chain", async () => {
|
|
69
|
+
const commitResult = await runCli(["zkid", "id-commitment", "--json", ZKID_NAME]);
|
|
70
|
+
const infoResult = await runCli(["zkid", "info", "--json", ZKID_NAME]);
|
|
71
|
+
assert.equal(commitResult.exitCode, 0);
|
|
72
|
+
assert.equal(infoResult.exitCode, 0);
|
|
73
|
+
const localCommitment = JSON.parse(commitResult.stdout).idCommitment;
|
|
74
|
+
const chainCommitment = JSON.parse(infoResult.stdout).idCommitment;
|
|
75
|
+
assert.equal(localCommitment, chainCommitment, "local commitment != on-chain commitment");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── deposit ────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
it("deposit 1 NARA into the ZK ID", async () => {
|
|
81
|
+
const { stdout, stderr, exitCode } = await runCli([
|
|
82
|
+
"zkid", "deposit", ZKID_NAME, "1",
|
|
83
|
+
]);
|
|
84
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
85
|
+
assert.ok(stdout.includes("Transaction"), `stdout: ${stdout}`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("info shows depositCount=1 after deposit", async () => {
|
|
89
|
+
const { stdout, exitCode } = await runCli(["zkid", "info", "--json", ZKID_NAME]);
|
|
90
|
+
assert.equal(exitCode, 0);
|
|
91
|
+
const json = JSON.parse(stdout);
|
|
92
|
+
assert.equal(json.depositCount, 1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── scan ───────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
it("scan finds 1 claimable deposit", async () => {
|
|
98
|
+
const { stdout, exitCode, stderr } = await runCli([
|
|
99
|
+
"zkid", "scan", "--json", ZKID_NAME,
|
|
100
|
+
]);
|
|
101
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
102
|
+
const json = JSON.parse(stdout);
|
|
103
|
+
assert.equal(json.count, 1);
|
|
104
|
+
assert.equal(json.deposits[0].nara, 1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── withdraw ───────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
it("withdraw sends funds to auto-generated recipient", async () => {
|
|
110
|
+
const { stdout, stderr, exitCode } = await runCli([
|
|
111
|
+
"zkid", "withdraw", "--json", ZKID_NAME,
|
|
112
|
+
]);
|
|
113
|
+
assert.equal(exitCode, 0, `stderr: ${stderr}`);
|
|
114
|
+
const json = JSON.parse(stdout);
|
|
115
|
+
assert.ok(json.recipient, "no recipient in output");
|
|
116
|
+
assert.ok(json.signature, "no signature in output");
|
|
117
|
+
assert.equal(json.nara, 1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("scan shows 0 claimable deposits after withdrawal", async () => {
|
|
121
|
+
const { stdout, exitCode } = await runCli([
|
|
122
|
+
"zkid", "scan", "--json", ZKID_NAME,
|
|
123
|
+
]);
|
|
124
|
+
assert.equal(exitCode, 0);
|
|
125
|
+
const json = JSON.parse(stdout);
|
|
126
|
+
assert.equal(json.count, 0, `expected 0, got ${json.count}`);
|
|
127
|
+
});
|
|
128
|
+
});
|