naracli 1.0.37 → 1.0.44

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naracli",
3
- "version": "1.0.37",
3
+ "version": "1.0.44",
4
4
  "description": "CLI for the Nara chain (Solana-compatible)",
5
5
  "module": "index.ts",
6
6
  "main": "index.ts",
@@ -56,7 +56,8 @@
56
56
  "bs58": "^6.0.0",
57
57
  "commander": "^12.1.0",
58
58
  "ed25519-hd-key": "^1.3.0",
59
- "nara-sdk": "^1.0.37",
59
+ "nara-sdk": "^1.0.44",
60
60
  "picocolors": "^1.1.1"
61
- }
61
+ },
62
+ "packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
62
63
  }
@@ -15,6 +15,7 @@ import {
15
15
  import type { GlobalOptions } from "../types";
16
16
  import {
17
17
  registerAgent,
18
+ registerAgentWithReferral,
18
19
  getAgentInfo,
19
20
  getAgentMemory,
20
21
  setBio,
@@ -24,6 +25,7 @@ import {
24
25
  transferAgentAuthority,
25
26
  deleteAgent,
26
27
  logActivity,
28
+ logActivityWithReferral,
27
29
  setReferral as setReferralOnChain,
28
30
  } from "nara-sdk";
29
31
  import { readFileSync } from "node:fs";
@@ -39,7 +41,9 @@ async function handleAgentRegister(agentId: string, options: GlobalOptions & { r
39
41
  const wallet = await loadWallet(options.wallet);
40
42
 
41
43
  if (!options.json) printInfo(`Registering agent "${agentId}"...`);
42
- const result = await registerAgent(connection, wallet, agentId, undefined, options.referral);
44
+ const result = options.referral
45
+ ? await registerAgentWithReferral(connection, wallet, agentId, options.referral)
46
+ : await registerAgent(connection, wallet, agentId);
43
47
  if (!options.json) printSuccess(`Agent "${agentId}" registered!`);
44
48
  addAgentId(agentId, rpcUrl);
45
49
 
@@ -255,7 +259,9 @@ async function handleAgentLog(
255
259
  const referral = options.referral;
256
260
 
257
261
  if (!options.json) printInfo(`Logging activity for "${agentId}"...`);
258
- const signature = await logActivity(connection, wallet, agentId, model, activity, log, undefined, referral);
262
+ const signature = referral
263
+ ? await logActivityWithReferral(connection, wallet, agentId, model, activity, log, referral)
264
+ : await logActivity(connection, wallet, agentId, model, activity, log);
259
265
  if (!options.json) printSuccess("Activity logged!");
260
266
 
261
267
  if (options.json) {
@@ -20,7 +20,11 @@ import {
20
20
  submitAnswer,
21
21
  submitAnswerViaRelay,
22
22
  parseQuestReward,
23
+ stake as questStake,
24
+ unstake as questUnstake,
25
+ getStakeInfo,
23
26
  type ActivityLog,
27
+ type StakeInfo,
24
28
  } from "nara-sdk";
25
29
  import { loadNetworkConfig } from "../utils/agent-config";
26
30
 
@@ -34,9 +38,12 @@ const QUEST_ERRORS: Record<number, string> = {
34
38
  6003: "invalidProof",
35
39
  6004: "invalidDeadline",
36
40
  6005: "insufficientReward",
37
- 6006: "insufficientPoolBalance",
38
- 6007: "questionTooLong",
39
- 6008: "alreadyAnswered",
41
+ 6006: "questionTooLong",
42
+ 6007: "alreadyAnswered",
43
+ 6008: "invalidMinRewardCount",
44
+ 6009: "invalidMaxRewardCount",
45
+ 6010: "unstakeNotReady",
46
+ 6011: "insufficientStakeBalance",
40
47
  };
41
48
 
42
49
  function anchorErrorCode(err: any): string {
@@ -85,9 +92,8 @@ async function handleQuestGet(options: GlobalOptions) {
85
92
  return;
86
93
  }
87
94
 
88
- const data = {
95
+ const data: Record<string, any> = {
89
96
  round: quest.round,
90
- questionId: quest.questionId,
91
97
  question: quest.question,
92
98
  difficulty: quest.difficulty,
93
99
  rewardPerWinner: `${quest.rewardPerWinner} NARA`,
@@ -97,6 +103,8 @@ async function handleQuestGet(options: GlobalOptions) {
97
103
  deadline: new Date(quest.deadline * 1000).toLocaleString(),
98
104
  timeRemaining: formatTimeRemaining(quest.timeRemaining),
99
105
  expired: quest.expired,
106
+ stakeRequirement: `${quest.stakeRequirement} NARA`,
107
+ minWinnerStake: `${quest.minWinnerStake} NARA`,
100
108
  };
101
109
 
102
110
  if (options.json) {
@@ -111,6 +119,10 @@ async function handleQuestGet(options: GlobalOptions) {
111
119
  console.log(
112
120
  ` Reward slots: ${quest.winnerCount}/${quest.rewardCount} (${quest.remainingSlots} remaining)`
113
121
  );
122
+ if (quest.stakeRequirement > 0) {
123
+ console.log(` Stake requirement: ${quest.stakeRequirement} NARA`);
124
+ console.log(` Min winner stake: ${quest.minWinnerStake} NARA`);
125
+ }
114
126
  console.log(` Deadline: ${new Date(quest.deadline * 1000).toLocaleString()}`);
115
127
  if (quest.timeRemaining > 0) {
116
128
  console.log(` Time remaining: ${formatTimeRemaining(quest.timeRemaining)}`);
@@ -124,7 +136,7 @@ async function handleQuestGet(options: GlobalOptions) {
124
136
  // ─── Command: quest answer ───────────────────────────────────────
125
137
  async function handleQuestAnswer(
126
138
  answer: string,
127
- options: GlobalOptions & { relay?: string; agent?: string; model?: string; referral?: string }
139
+ options: GlobalOptions & { relay?: string; agent?: string; model?: string; referral?: string; stake?: string }
128
140
  ) {
129
141
  const rpcUrl = getRpcUrl(options.rpcUrl);
130
142
  const connection = new Connection(rpcUrl, "confirmed");
@@ -210,7 +222,8 @@ async function handleQuestAnswer(
210
222
  if (configAgentId) {
211
223
  activityLog = { agentId: configAgentId, activity: "PoMI", model, log: "", referralAgentId: referral };
212
224
  }
213
- const result = await submitAnswer(connection, wallet, proof.solana, agent, model, undefined, activityLog);
225
+ const stakeOpt = options.stake === "auto" ? "auto" : options.stake ? parseFloat(options.stake) : undefined;
226
+ const result = await submitAnswer(connection, wallet, proof.solana, agent, model, stakeOpt !== undefined ? { stake: stakeOpt } : undefined, activityLog);
214
227
  printSuccess("Answer submitted!");
215
228
  console.log(` Transaction: ${result.signature}`);
216
229
  await handleReward(connection, result.signature, options);
@@ -220,6 +233,78 @@ async function handleQuestAnswer(
220
233
  }
221
234
  }
222
235
 
236
+ // ─── Command: quest stake ────────────────────────────────────────
237
+ async function handleQuestStake(amount: string, options: GlobalOptions) {
238
+ const rpcUrl = getRpcUrl(options.rpcUrl);
239
+ const connection = new Connection(rpcUrl, "confirmed");
240
+ const wallet = await loadWallet(options.wallet);
241
+
242
+ const n = parseFloat(amount);
243
+ if (isNaN(n) || n <= 0) {
244
+ printError("Amount must be a positive number");
245
+ process.exit(1);
246
+ }
247
+
248
+ if (!options.json) printInfo(`Staking ${n} NARA...`);
249
+ const signature = await questStake(connection, wallet, n);
250
+ if (!options.json) printSuccess(`Staked ${n} NARA!`);
251
+
252
+ if (options.json) {
253
+ formatOutput({ amount: n, signature }, true);
254
+ } else {
255
+ console.log(` Transaction: ${signature}`);
256
+ }
257
+ }
258
+
259
+ // ─── Command: quest unstake ─────────────────────────────────────
260
+ async function handleQuestUnstake(amount: string, options: GlobalOptions) {
261
+ const rpcUrl = getRpcUrl(options.rpcUrl);
262
+ const connection = new Connection(rpcUrl, "confirmed");
263
+ const wallet = await loadWallet(options.wallet);
264
+
265
+ const n = parseFloat(amount);
266
+ if (isNaN(n) || n <= 0) {
267
+ printError("Amount must be a positive number");
268
+ process.exit(1);
269
+ }
270
+
271
+ if (!options.json) printInfo(`Unstaking ${n} NARA...`);
272
+ const signature = await questUnstake(connection, wallet, n);
273
+ if (!options.json) printSuccess(`Unstaked ${n} NARA!`);
274
+
275
+ if (options.json) {
276
+ formatOutput({ amount: n, signature }, true);
277
+ } else {
278
+ console.log(` Transaction: ${signature}`);
279
+ }
280
+ }
281
+
282
+ // ─── Command: quest stake-info ──────────────────────────────────
283
+ async function handleQuestStakeInfo(options: GlobalOptions) {
284
+ const rpcUrl = getRpcUrl(options.rpcUrl);
285
+ const connection = new Connection(rpcUrl, "confirmed");
286
+ const wallet = await loadWallet(options.wallet);
287
+
288
+ const stakeInfo = await getStakeInfo(connection, wallet.publicKey);
289
+ if (!stakeInfo) {
290
+ if (options.json) {
291
+ formatOutput({ staked: false, amount: 0 }, true);
292
+ } else {
293
+ printInfo("No stake record found");
294
+ }
295
+ return;
296
+ }
297
+
298
+ if (options.json) {
299
+ formatOutput({ staked: true, amount: stakeInfo.amount, stakeRound: stakeInfo.stakeRound }, true);
300
+ } else {
301
+ console.log("");
302
+ console.log(` Staked: ${stakeInfo.amount} NARA`);
303
+ console.log(` Stake round: ${stakeInfo.stakeRound}`);
304
+ console.log("");
305
+ }
306
+ }
307
+
223
308
  // ─── Parse reward from transaction ───────────────────────────────
224
309
  async function handleReward(
225
310
  connection: Connection,
@@ -280,6 +365,12 @@ function handleSubmitError(err: any) {
280
365
  case "poolNotActive":
281
366
  printError("No active quest at the moment");
282
367
  break;
368
+ case "unstakeNotReady":
369
+ printError("Cannot unstake until round advances or deadline passes");
370
+ break;
371
+ case "insufficientStakeBalance":
372
+ printError("Unstake amount exceeds staked balance");
373
+ break;
283
374
  default:
284
375
  printError(`Failed to submit answer: ${err.message ?? String(err)}`);
285
376
  if (err.logs) {
@@ -318,11 +409,55 @@ export function registerQuestCommands(program: Command): void {
318
409
  .option("--agent <name>", "Agent identifier (default: naracli)")
319
410
  .option("--model <name>", "Model identifier")
320
411
  .option("--referral <agent-id>", "Referral agent ID")
412
+ .option("--stake [amount]", 'Stake NARA before answering ("auto" to top-up to requirement, or a number)')
321
413
  .action(async (answer: string, opts: any, cmd: Command) => {
322
414
  try {
323
415
  const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
324
416
  const relayUrl = opts.relay === true ? DEFAULT_QUEST_RELAY_URL : opts.relay;
325
- await handleQuestAnswer(answer, { ...globalOpts, relay: relayUrl, agent: opts.agent, model: opts.model, referral: opts.referral });
417
+ const stakeVal = opts.stake === true ? "auto" : opts.stake;
418
+ await handleQuestAnswer(answer, { ...globalOpts, relay: relayUrl, agent: opts.agent, model: opts.model, referral: opts.referral, stake: stakeVal });
419
+ } catch (error: any) {
420
+ printError(error.message);
421
+ process.exit(1);
422
+ }
423
+ });
424
+
425
+ // quest stake
426
+ quest
427
+ .command("stake <amount>")
428
+ .description("Stake NARA to participate in quests")
429
+ .action(async (amount: string, _opts: any, cmd: Command) => {
430
+ try {
431
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
432
+ await handleQuestStake(amount, globalOpts);
433
+ } catch (error: any) {
434
+ printError(error.message);
435
+ process.exit(1);
436
+ }
437
+ });
438
+
439
+ // quest unstake
440
+ quest
441
+ .command("unstake <amount>")
442
+ .description("Unstake NARA (available after round advances or deadline passes)")
443
+ .action(async (amount: string, _opts: any, cmd: Command) => {
444
+ try {
445
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
446
+ await handleQuestUnstake(amount, globalOpts);
447
+ } catch (error: any) {
448
+ printError(error.message);
449
+ process.exit(1);
450
+ }
451
+ });
452
+
453
+ // quest stake-info
454
+ quest
455
+ .command("stake-info")
456
+ .description("Get your current quest stake info")
457
+ .action(async (_opts: any, cmd: Command) => {
458
+ try {
459
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
460
+ await handleQuestStakeInfo(globalOpts);
326
461
  } catch (error: any) {
327
462
  printError(error.message);
328
463
  process.exit(1);
@@ -7,6 +7,7 @@ import { join, dirname } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { existsSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
+ import type { Connection } from "@solana/web3.js";
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
13
  const CLI = join(__dirname, "../../bin/nara-cli.ts");
@@ -57,3 +58,22 @@ export const hasWallet = existsSync(join(homedir(), ".config", "nara", "id.json"
57
58
  export function uniqueName(prefix: string): string {
58
59
  return `${prefix}-${Date.now().toString(36)}`;
59
60
  }
61
+
62
+ /**
63
+ * Poll for transaction confirmation (avoids WebSocket-based confirmTransaction
64
+ * which fails with TLS errors on some networks).
65
+ */
66
+ export async function pollConfirmation(
67
+ connection: Connection,
68
+ signature: string,
69
+ maxRetries = 30,
70
+ intervalMs = 1000
71
+ ): Promise<void> {
72
+ for (let i = 0; i < maxRetries; i++) {
73
+ const { value } = await connection.getSignatureStatuses([signature]);
74
+ const status = value[0]?.confirmationStatus;
75
+ if (status === "confirmed" || status === "finalized") return;
76
+ await new Promise((r) => setTimeout(r, intervalMs));
77
+ }
78
+ throw new Error(`Transaction ${signature} not confirmed after ${maxRetries}s`);
79
+ }
@@ -22,7 +22,7 @@ import { fileURLToPath } from "node:url";
22
22
  import { Connection, Keypair, LAMPORTS_PER_SOL, SystemProgram, Transaction } from "@solana/web3.js";
23
23
  import bs58 from "bs58";
24
24
  import { DEFAULT_AGENT_REGISTRY_PROGRAM_ID, getQuestInfo, getAgentRecord } from "nara-sdk";
25
- import { runCli, hasWallet } from "./helpers.js";
25
+ import { runCli, hasWallet, pollConfirmation } from "./helpers.js";
26
26
 
27
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
28
28
 
@@ -80,7 +80,7 @@ describe("quest referral (on-chain)", { skip: !hasWallet ? "no PRIVATE_KEY" : un
80
80
  transferTx.recentBlockhash = (await connection.getLatestBlockhash("confirmed")).blockhash;
81
81
  transferTx.sign(mainWallet);
82
82
  const sig = await connection.sendRawTransaction(transferTx.serialize());
83
- await connection.confirmTransaction(sig, "confirmed");
83
+ await pollConfirmation(connection, sig);
84
84
  console.log(` Transfer tx: ${sig}`);
85
85
  });
86
86
 
@@ -106,9 +106,11 @@ describe("quest referral (on-chain)", { skip: !hasWallet ? "no PRIVATE_KEY" : un
106
106
  console.log(" Referral agent registered");
107
107
  });
108
108
 
109
- it("registers main agent", async () => {
110
- console.log(` Registering main agent "${mainAgentId}"...`);
111
- const { stdout, stderr, exitCode } = await runCli(["agent", "register", mainAgentId]);
109
+ it("registers main agent with referral", async () => {
110
+ console.log(` Registering main agent "${mainAgentId}" with referral "${referralAgentId}"...`);
111
+ const { stdout, stderr, exitCode } = await runCli([
112
+ "agent", "register", mainAgentId, "--referral", referralAgentId,
113
+ ]);
112
114
  const output = stdout + stderr;
113
115
  if (output.includes("already") || output.includes("in use")) {
114
116
  console.log(" Agent already exists, continuing");
@@ -116,7 +118,7 @@ describe("quest referral (on-chain)", { skip: !hasWallet ? "no PRIVATE_KEY" : un
116
118
  }
117
119
  assert.equal(exitCode, 0, `Failed: ${stderr}`);
118
120
  assert.ok(output.includes("registered") || output.includes("Transaction"), "should confirm registration");
119
- console.log(" Main agent registered");
121
+ console.log(" Main agent registered with referral");
120
122
  });
121
123
 
122
124
  it("answers quest with --referral", async () => {
@@ -162,7 +164,7 @@ describe("quest referral (on-chain)", { skip: !hasWallet ? "no PRIVATE_KEY" : un
162
164
  // Extract transaction signature
163
165
  const txMatch = output.match(/Transaction:\s+(\S+)/);
164
166
  assert.ok(txMatch, "should show transaction signature");
165
- const txSig = txMatch![1];
167
+ const txSig = txMatch![1]!;
166
168
  console.log(` Transaction: ${txSig}`);
167
169
 
168
170
  // Verify transaction succeeded on-chain
@@ -204,25 +206,16 @@ describe("quest referral (on-chain)", { skip: !hasWallet ? "no PRIVATE_KEY" : un
204
206
  console.log(" Answer submitted with referral successfully");
205
207
  });
206
208
 
207
- it("verifies on-chain agent points", async () => {
209
+ it("verifies on-chain agent records", async () => {
208
210
  try {
209
211
  const mainRecord = await getAgentRecord(connection, mainAgentId);
210
- console.log(` Main agent points: ${mainRecord.points}`);
212
+ console.log(` Main agent: ${mainRecord.agentId}, referral: ${mainRecord.referralId ?? "(none)"}`);
211
213
 
212
214
  const referralRecord = await getAgentRecord(connection, referralAgentId);
213
- console.log(` Referral agent points: ${referralRecord.points}`);
214
-
215
- if (mainRecord.points > 0) {
216
- console.log(" OK: Main agent earned points");
217
- } else {
218
- console.log(" WARN: Main agent has 0 points (quest may not have been answered in this run)");
219
- }
220
-
221
- if (referralRecord.points > 0) {
222
- console.log(" OK: Referral agent earned referral points");
223
- } else {
224
- console.log(" WARN: Referral agent has 0 points");
225
- }
215
+ console.log(` Referral agent: ${referralRecord.agentId}`);
216
+
217
+ // Points are now minted as SPL tokens (Token-2022), not stored on AgentRecord
218
+ console.log(" OK: Both agent records exist on-chain");
226
219
  } catch (err: any) {
227
220
  console.log(` (skipped: ${err.message})`);
228
221
  }
@@ -94,7 +94,7 @@ describe("quest proof generation", () => {
94
94
 
95
95
  // Generate proof with a random pubkey (we're just testing proof generation)
96
96
  const testKeypair = Keypair.generate();
97
- const proof = await generateProof(match.answer, quest.answerHash, testKeypair.publicKey);
97
+ const proof = await generateProof(match.answer, quest.answerHash, testKeypair.publicKey, quest.round);
98
98
 
99
99
  assert.ok(proof.solana.proofA.length > 0, "proofA should not be empty");
100
100
  assert.ok(proof.solana.proofB.length > 0, "proofB should not be empty");