naracli 1.0.46 → 1.0.47

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.
@@ -32114,7 +32114,7 @@ Message: ${transactionMessage}.
32114
32114
  * @internal
32115
32115
  */
32116
32116
  static checkProgramId(programId) {
32117
- if (!programId.equals(ComputeBudgetProgram.programId)) {
32117
+ if (!programId.equals(ComputeBudgetProgram2.programId)) {
32118
32118
  throw new Error("invalid instruction; programId is not ComputeBudgetProgram");
32119
32119
  }
32120
32120
  }
@@ -32137,7 +32137,7 @@ Message: ${transactionMessage}.
32137
32137
  layout: BufferLayout__namespace.struct([BufferLayout__namespace.u8("instruction"), u642("microLamports")])
32138
32138
  }
32139
32139
  });
32140
- var ComputeBudgetProgram = class {
32140
+ var ComputeBudgetProgram2 = class {
32141
32141
  /**
32142
32142
  * @internal
32143
32143
  */
@@ -32188,7 +32188,7 @@ Message: ${transactionMessage}.
32188
32188
  });
32189
32189
  }
32190
32190
  };
32191
- ComputeBudgetProgram.programId = new PublicKey30("ComputeBudget111111111111111111111111111111");
32191
+ ComputeBudgetProgram2.programId = new PublicKey30("ComputeBudget111111111111111111111111111111");
32192
32192
  var PRIVATE_KEY_BYTES$1 = 64;
32193
32193
  var PUBLIC_KEY_BYTES$1 = 32;
32194
32194
  var SIGNATURE_BYTES = 64;
@@ -33688,7 +33688,7 @@ Message: ${transactionMessage}.
33688
33688
  exports2.BpfLoader = BpfLoader;
33689
33689
  exports2.COMPUTE_BUDGET_INSTRUCTION_LAYOUTS = COMPUTE_BUDGET_INSTRUCTION_LAYOUTS;
33690
33690
  exports2.ComputeBudgetInstruction = ComputeBudgetInstruction;
33691
- exports2.ComputeBudgetProgram = ComputeBudgetProgram;
33691
+ exports2.ComputeBudgetProgram = ComputeBudgetProgram2;
33692
33692
  exports2.Connection = Connection13;
33693
33693
  exports2.Ed25519Program = Ed25519Program;
33694
33694
  exports2.Enum = Enum;
@@ -60397,7 +60397,35 @@ async function loadAlt(connection) {
60397
60397
  _cachedAltAddress = addr;
60398
60398
  return _cachedAlt;
60399
60399
  }
60400
+ async function getRecentPriorityFee(connection) {
60401
+ const fees = await connection.getRecentPrioritizationFees();
60402
+ if (!fees.length) return 0;
60403
+ const nonZero = fees.filter((f) => f.prioritizationFee > 0);
60404
+ if (!nonZero.length) return 0;
60405
+ const avg = nonZero.reduce((s, f) => s + f.prioritizationFee, 0) / nonZero.length;
60406
+ return Math.ceil(avg);
60407
+ }
60400
60408
  async function sendTx(connection, payer, instructions, signers, opts) {
60409
+ const budgetIxs = [];
60410
+ if (opts?.computeUnitLimit) {
60411
+ budgetIxs.push(
60412
+ import_web388.ComputeBudgetProgram.setComputeUnitLimit({ units: opts.computeUnitLimit })
60413
+ );
60414
+ }
60415
+ if (opts?.computeUnitPrice !== void 0) {
60416
+ let price;
60417
+ if (opts.computeUnitPrice === "auto") {
60418
+ price = await getRecentPriorityFee(connection);
60419
+ } else {
60420
+ price = opts.computeUnitPrice;
60421
+ }
60422
+ if (price > 0) {
60423
+ budgetIxs.push(
60424
+ import_web388.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: price })
60425
+ );
60426
+ }
60427
+ }
60428
+ const allInstructions = [...budgetIxs, ...instructions];
60401
60429
  const alt = await loadAlt(connection);
60402
60430
  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed");
60403
60431
  let signature;
@@ -60405,7 +60433,7 @@ async function sendTx(connection, payer, instructions, signers, opts) {
60405
60433
  const message = new import_web388.TransactionMessage({
60406
60434
  payerKey: payer.publicKey,
60407
60435
  recentBlockhash: blockhash,
60408
- instructions
60436
+ instructions: allInstructions
60409
60437
  }).compileToV0Message([alt]);
60410
60438
  const tx = new import_web388.VersionedTransaction(message);
60411
60439
  const allSigners = [payer, ...signers ?? []];
@@ -60425,7 +60453,7 @@ async function sendTx(connection, payer, instructions, signers, opts) {
60425
60453
  tx.recentBlockhash = blockhash;
60426
60454
  tx.lastValidBlockHeight = lastValidBlockHeight;
60427
60455
  tx.feePayer = payer.publicKey;
60428
- for (const ix of instructions) tx.add(ix);
60456
+ for (const ix of allInstructions) tx.add(ix);
60429
60457
  const allSigners = [payer, ...signers ?? []];
60430
60458
  const seen = /* @__PURE__ */ new Set();
60431
60459
  const uniqueSigners = allSigners.filter((s) => {
@@ -60439,16 +60467,31 @@ async function sendTx(connection, payer, instructions, signers, opts) {
60439
60467
  skipPreflight: opts?.skipPreflight ?? false
60440
60468
  });
60441
60469
  }
60442
- const confirmation = await connection.confirmTransaction(
60443
- { signature, blockhash, lastValidBlockHeight },
60444
- "confirmed"
60445
- );
60446
- if (confirmation.value.err) {
60447
- throw new Error(
60448
- `Transaction ${signature} failed: ${JSON.stringify(confirmation.value.err)}`
60449
- );
60470
+ const startTime = Date.now();
60471
+ const TIMEOUT_MS = 2e4;
60472
+ const POLL_INTERVAL_MS = 1e3;
60473
+ while (Date.now() - startTime < TIMEOUT_MS) {
60474
+ const currentBlockHeight = await connection.getBlockHeight("confirmed");
60475
+ if (currentBlockHeight > lastValidBlockHeight) {
60476
+ throw new Error(
60477
+ `Transaction ${signature} expired: block height exceeded`
60478
+ );
60479
+ }
60480
+ const statusResult = await connection.getSignatureStatuses([signature]);
60481
+ const status = statusResult?.value?.[0];
60482
+ if (status) {
60483
+ if (status.err) {
60484
+ throw new Error(
60485
+ `Transaction ${signature} failed: ${JSON.stringify(status.err)}`
60486
+ );
60487
+ }
60488
+ if (status.confirmationStatus === "confirmed" || status.confirmationStatus === "finalized") {
60489
+ return signature;
60490
+ }
60491
+ }
60492
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
60450
60493
  }
60451
- return signature;
60494
+ throw new Error(`Transaction ${signature} confirmation timeout`);
60452
60495
  }
60453
60496
  var import_web388, _cachedAlt, _cachedAltAddress, _overrideAltAddress;
60454
60497
  var init_tx = __esm({
@@ -124721,16 +124764,16 @@ var nara_quest_default = {
124721
124764
  args: []
124722
124765
  },
124723
124766
  {
124724
- name: "set_max_reward_count",
124767
+ name: "set_reward_config",
124725
124768
  discriminator: [
124726
- 247,
124727
- 62,
124728
- 67,
124729
- 243,
124730
- 249,
124731
- 243,
124732
- 102,
124733
- 62
124769
+ 163,
124770
+ 34,
124771
+ 211,
124772
+ 14,
124773
+ 25,
124774
+ 118,
124775
+ 181,
124776
+ 233
124734
124777
  ],
124735
124778
  accounts: [
124736
124779
  {
@@ -124764,6 +124807,10 @@ var nara_quest_default = {
124764
124807
  }
124765
124808
  ],
124766
124809
  args: [
124810
+ {
124811
+ name: "min_reward_count",
124812
+ type: "u32"
124813
+ },
124767
124814
  {
124768
124815
  name: "max_reward_count",
124769
124816
  type: "u32"
@@ -124771,16 +124818,16 @@ var nara_quest_default = {
124771
124818
  ]
124772
124819
  },
124773
124820
  {
124774
- name: "set_min_reward_count",
124821
+ name: "set_stake_config",
124775
124822
  discriminator: [
124776
- 108,
124777
- 213,
124778
- 24,
124779
- 47,
124780
- 93,
124781
- 149,
124782
- 58,
124783
- 4
124823
+ 84,
124824
+ 37,
124825
+ 76,
124826
+ 39,
124827
+ 236,
124828
+ 111,
124829
+ 214,
124830
+ 191
124784
124831
  ],
124785
124832
  accounts: [
124786
124833
  {
@@ -124815,8 +124862,16 @@ var nara_quest_default = {
124815
124862
  ],
124816
124863
  args: [
124817
124864
  {
124818
- name: "min_reward_count",
124819
- type: "u32"
124865
+ name: "bps_high",
124866
+ type: "u64"
124867
+ },
124868
+ {
124869
+ name: "bps_low",
124870
+ type: "u64"
124871
+ },
124872
+ {
124873
+ name: "decay_ms",
124874
+ type: "i64"
124820
124875
  }
124821
124876
  ]
124822
124877
  },
@@ -125015,6 +125070,30 @@ var nara_quest_default = {
125015
125070
  48
125016
125071
  ],
125017
125072
  accounts: [
125073
+ {
125074
+ name: "game_config",
125075
+ pda: {
125076
+ seeds: [
125077
+ {
125078
+ kind: "const",
125079
+ value: [
125080
+ 113,
125081
+ 117,
125082
+ 101,
125083
+ 115,
125084
+ 116,
125085
+ 95,
125086
+ 99,
125087
+ 111,
125088
+ 110,
125089
+ 102,
125090
+ 105,
125091
+ 103
125092
+ ]
125093
+ }
125094
+ ]
125095
+ }
125096
+ },
125018
125097
  {
125019
125098
  name: "pool",
125020
125099
  writable: true,
@@ -125709,12 +125788,12 @@ var nara_quest_default = {
125709
125788
  {
125710
125789
  code: 6008,
125711
125790
  name: "InvalidMinRewardCount",
125712
- msg: "min_reward_count must be > 0 and <= max_reward_count"
125791
+ msg: "Invalid reward config: need 0 < min <= max"
125713
125792
  },
125714
125793
  {
125715
125794
  code: 6009,
125716
- name: "InvalidMaxRewardCount",
125717
- msg: "max_reward_count must be >= min_reward_count"
125795
+ name: "InvalidStakeConfig",
125796
+ msg: "Stake config values must be > 0"
125718
125797
  },
125719
125798
  {
125720
125799
  code: 6010,
@@ -125725,6 +125804,11 @@ var nara_quest_default = {
125725
125804
  code: 6011,
125726
125805
  name: "InsufficientStakeBalance",
125727
125806
  msg: "Unstake amount exceeds staked balance"
125807
+ },
125808
+ {
125809
+ code: 6012,
125810
+ name: "InsufficientStake",
125811
+ msg: "Stake does not meet dynamic requirement"
125728
125812
  }
125729
125813
  ],
125730
125814
  types: [
@@ -125777,6 +125861,18 @@ var nara_quest_default = {
125777
125861
  name: "max_reward_count",
125778
125862
  type: "u32"
125779
125863
  },
125864
+ {
125865
+ name: "stake_bps_high",
125866
+ type: "u64"
125867
+ },
125868
+ {
125869
+ name: "stake_bps_low",
125870
+ type: "u64"
125871
+ },
125872
+ {
125873
+ name: "decay_ms",
125874
+ type: "i64"
125875
+ },
125780
125876
  {
125781
125877
  name: "_padding",
125782
125878
  type: {
@@ -125836,11 +125932,19 @@ var nara_quest_default = {
125836
125932
  type: "u32"
125837
125933
  },
125838
125934
  {
125839
- name: "stake_requirement",
125935
+ name: "created_at",
125936
+ type: "i64"
125937
+ },
125938
+ {
125939
+ name: "stake_high",
125840
125940
  type: "u64"
125841
125941
  },
125842
125942
  {
125843
- name: "min_winner_stake",
125943
+ name: "stake_low",
125944
+ type: "u64"
125945
+ },
125946
+ {
125947
+ name: "avg_participant_stake",
125844
125948
  type: "u64"
125845
125949
  },
125846
125950
  {
@@ -125906,14 +126010,7 @@ var BN254_FIELD = 21888242871839275222246405745257275088696311157297823662689037
125906
126010
  async function resolveDefaultZkPaths() {
125907
126011
  const { fileURLToPath } = await import("url");
125908
126012
  const { dirname: dirname2, join: join6 } = await import("path");
125909
- let dir;
125910
- if (import_meta.url) {
125911
- dir = dirname2(fileURLToPath(import_meta.url));
125912
- } else {
125913
- const { createRequire } = await import("module");
125914
- const req = createRequire(process.cwd() + "/");
125915
- dir = dirname2(req.resolve("nara-sdk/package.json")) + "/src";
125916
- }
126013
+ const dir = dirname2(fileURLToPath(import_meta.url));
125917
126014
  return {
125918
126015
  wasm: join6(dir, "zk", "answer_proof.wasm"),
125919
126016
  zkey: join6(dir, "zk", "answer_proof_final.zkey")
@@ -126031,6 +126128,14 @@ var WSOL_MINT = new import_web390.PublicKey("So111111111111111111111111111111111
126031
126128
  function getStakeTokenAccount(stakeRecordPda) {
126032
126129
  return getAssociatedTokenAddressSync(WSOL_MINT, stakeRecordPda, true);
126033
126130
  }
126131
+ function computeEffectiveStake(stakeHigh, stakeLow, createdAt, decayMs, nowMs) {
126132
+ if (decayMs <= 0) return stakeLow;
126133
+ const elapsedMs = nowMs - createdAt;
126134
+ if (elapsedMs >= decayMs) return stakeLow;
126135
+ const range = stakeHigh - stakeLow;
126136
+ const ratio = elapsedMs / decayMs;
126137
+ return stakeHigh - range * ratio * ratio;
126138
+ }
126034
126139
  async function getQuestInfo(connection, wallet, options) {
126035
126140
  const kp = wallet ?? import_web390.Keypair.generate();
126036
126141
  const program3 = createProgram2(connection, kp, options?.programId);
@@ -126040,6 +126145,25 @@ async function getQuestInfo(connection, wallet, options) {
126040
126145
  const deadline = pool.deadline.toNumber();
126041
126146
  const secsLeft = deadline - now;
126042
126147
  const active = pool.question.length > 0 && secsLeft > 0;
126148
+ const stakeHigh = Number(pool.stakeHigh.toString()) / import_web390.LAMPORTS_PER_SOL;
126149
+ const stakeLow = Number(pool.stakeLow.toString()) / import_web390.LAMPORTS_PER_SOL;
126150
+ const createdAt = pool.createdAt.toNumber();
126151
+ const programId = new import_web390.PublicKey(options?.programId ?? DEFAULT_QUEST_PROGRAM_ID);
126152
+ const [configPda] = import_web390.PublicKey.findProgramAddressSync(
126153
+ [new TextEncoder().encode("quest_config")],
126154
+ programId
126155
+ );
126156
+ const config = await program3.account.gameConfig.fetch(configPda);
126157
+ const decayMs = Number(config.decayMs.toString());
126158
+ const nowMs = Date.now();
126159
+ const createdAtMs = createdAt * 1e3;
126160
+ const effectiveStakeRequirement = computeEffectiveStake(
126161
+ stakeHigh,
126162
+ stakeLow,
126163
+ createdAtMs,
126164
+ decayMs,
126165
+ nowMs
126166
+ );
126043
126167
  return {
126044
126168
  active,
126045
126169
  round: pool.round.toString(),
@@ -126054,8 +126178,11 @@ async function getQuestInfo(connection, wallet, options) {
126054
126178
  deadline,
126055
126179
  timeRemaining: secsLeft,
126056
126180
  expired: secsLeft <= 0,
126057
- stakeRequirement: Number(pool.stakeRequirement.toString()) / import_web390.LAMPORTS_PER_SOL,
126058
- minWinnerStake: Number(pool.minWinnerStake.toString()) / import_web390.LAMPORTS_PER_SOL
126181
+ stakeHigh,
126182
+ stakeLow,
126183
+ avgParticipantStake: Number(pool.avgParticipantStake.toString()) / import_web390.LAMPORTS_PER_SOL,
126184
+ createdAt,
126185
+ effectiveStakeRequirement
126059
126186
  };
126060
126187
  }
126061
126188
  async function hasAnswered(connection, wallet, options) {
@@ -126105,7 +126232,7 @@ async function submitAnswer(connection, wallet, proof, agent = "", model = "", o
126105
126232
  if (options.stake === "auto") {
126106
126233
  const quest = await getQuestInfo(connection, wallet, options);
126107
126234
  const stakeInfo = await getStakeInfo(connection, wallet.publicKey, options);
126108
- const required = quest.stakeRequirement;
126235
+ const required = quest.effectiveStakeRequirement;
126109
126236
  const current = stakeInfo?.amount ?? 0;
126110
126237
  const deficit = required - current;
126111
126238
  if (deficit > 0) {
@@ -130103,8 +130230,10 @@ async function handleQuestGet(options) {
130103
130230
  deadline: new Date(quest.deadline * 1e3).toLocaleString(),
130104
130231
  timeRemaining: formatTimeRemaining(quest.timeRemaining),
130105
130232
  expired: quest.expired,
130106
- stakeRequirement: `${quest.stakeRequirement} NARA`,
130107
- minWinnerStake: `${quest.minWinnerStake} NARA`
130233
+ stakeRequirement: `${quest.effectiveStakeRequirement.toFixed(4)} NARA`,
130234
+ stakeHigh: `${quest.stakeHigh} NARA`,
130235
+ stakeLow: `${quest.stakeLow} NARA`,
130236
+ avgParticipantStake: `${quest.avgParticipantStake} NARA`
130108
130237
  };
130109
130238
  if (options.json) {
130110
130239
  formatOutput(data, true);
@@ -130118,9 +130247,8 @@ async function handleQuestGet(options) {
130118
130247
  console.log(
130119
130248
  ` Reward slots: ${quest.winnerCount}/${quest.rewardCount} (${quest.remainingSlots} remaining)`
130120
130249
  );
130121
- if (quest.stakeRequirement > 0) {
130122
- console.log(` Stake requirement: ${quest.stakeRequirement} NARA`);
130123
- console.log(` Min winner stake: ${quest.minWinnerStake} NARA`);
130250
+ if (quest.effectiveStakeRequirement > 0) {
130251
+ console.log(` Stake requirement: ${quest.effectiveStakeRequirement.toFixed(4)} NARA (decays ${quest.stakeHigh} \u2192 ${quest.stakeLow})`);
130124
130252
  }
130125
130253
  console.log(` Deadline: ${new Date(quest.deadline * 1e3).toLocaleString()}`);
130126
130254
  if (quest.timeRemaining > 0) {
@@ -133245,7 +133373,7 @@ function registerCommands(program3) {
133245
133373
  }
133246
133374
 
133247
133375
  // bin/nara-cli.ts
133248
- var version2 = true ? "1.0.46" : "dev";
133376
+ var version2 = true ? "1.0.47" : "dev";
133249
133377
  var program2 = new Command();
133250
133378
  program2.name("naracli").description("CLI for the Nara chain (Solana-compatible)").version(version2);
133251
133379
  program2.option("-r, --rpc-url <url>", "RPC endpoint URL").option("-w, --wallet <path>", "Path to wallet keypair JSON file").option("-j, --json", "Output in JSON format");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naracli",
3
- "version": "1.0.46",
3
+ "version": "1.0.47",
4
4
  "description": "CLI for the Nara chain (Solana-compatible)",
5
5
  "module": "index.ts",
6
6
  "main": "index.ts",
@@ -20,6 +20,7 @@
20
20
  "test:skills": "node --require ./bin/env-loader.cjs --import tsx --test --experimental-test-isolation=none --test-reporter spec src/tests/skills.test.ts",
21
21
  "test:zkid": "node --require ./bin/env-loader.cjs --import tsx --test --experimental-test-isolation=none --test-reporter spec src/tests/zkid.test.ts",
22
22
  "test:agent": "node --require ./bin/env-loader.cjs --import tsx --test --experimental-test-isolation=none --test-reporter spec src/tests/agent-registry.test.ts",
23
+ "test:quest-relay": "node --require ./bin/env-loader.cjs --import tsx --test --experimental-test-isolation=none --test-reporter spec src/tests/quest-relay.test.ts",
23
24
  "test:quest-referral": "node --require ./bin/env-loader.cjs --import tsx --test --experimental-test-isolation=none --test-reporter spec src/tests/quest-referral.test.ts",
24
25
  "prepublishOnly": "npm run build"
25
26
  },
@@ -56,7 +57,7 @@
56
57
  "bs58": "^6.0.0",
57
58
  "commander": "^12.1.0",
58
59
  "ed25519-hd-key": "^1.3.0",
59
- "nara-sdk": "^1.0.44",
60
+ "nara-sdk": "^1.0.48",
60
61
  "picocolors": "^1.1.1"
61
62
  },
62
63
  "packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
@@ -117,8 +117,10 @@ async function handleQuestGet(options: GlobalOptions) {
117
117
  deadline: new Date(quest.deadline * 1000).toLocaleString(),
118
118
  timeRemaining: formatTimeRemaining(quest.timeRemaining),
119
119
  expired: quest.expired,
120
- stakeRequirement: `${quest.stakeRequirement} NARA`,
121
- minWinnerStake: `${quest.minWinnerStake} NARA`,
120
+ stakeRequirement: `${quest.effectiveStakeRequirement.toFixed(4)} NARA`,
121
+ stakeHigh: `${quest.stakeHigh} NARA`,
122
+ stakeLow: `${quest.stakeLow} NARA`,
123
+ avgParticipantStake: `${quest.avgParticipantStake} NARA`,
122
124
  };
123
125
 
124
126
  if (options.json) {
@@ -133,9 +135,8 @@ async function handleQuestGet(options: GlobalOptions) {
133
135
  console.log(
134
136
  ` Reward slots: ${quest.winnerCount}/${quest.rewardCount} (${quest.remainingSlots} remaining)`
135
137
  );
136
- if (quest.stakeRequirement > 0) {
137
- console.log(` Stake requirement: ${quest.stakeRequirement} NARA`);
138
- console.log(` Min winner stake: ${quest.minWinnerStake} NARA`);
138
+ if (quest.effectiveStakeRequirement > 0) {
139
+ console.log(` Stake requirement: ${quest.effectiveStakeRequirement.toFixed(4)} NARA (decays ${quest.stakeHigh} → ${quest.stakeLow})`);
139
140
  }
140
141
  console.log(` Deadline: ${new Date(quest.deadline * 1000).toLocaleString()}`);
141
142
  if (quest.timeRemaining > 0) {
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Tests for quest relay (gasless) submission
3
+ *
4
+ * Requires PRIVATE_KEY in .env and an active quest.
5
+ *
6
+ * Run: npm run test:quest-relay
7
+ */
8
+
9
+ import { describe, it } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { readFileSync } from "node:fs";
12
+ import { join, dirname } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { Connection } from "@solana/web3.js";
15
+ import { runCli, hasWallet } from "./helpers.js";
16
+ import { getQuestInfo } from "nara-sdk";
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+
20
+ interface TestQuestion {
21
+ text: string;
22
+ answer: string;
23
+ }
24
+
25
+ function loadTestQuestions(): TestQuestion[] {
26
+ const filePath = join(__dirname, "../../.assets/test-questions.json");
27
+ return JSON.parse(readFileSync(filePath, "utf-8"));
28
+ }
29
+
30
+ // ─── Relay quest answer ──────────────────────────────────────────
31
+
32
+ describe("quest answer (relay)", { skip: !hasWallet ? "no wallet" : undefined }, () => {
33
+ it("submits answer via relay and outputs tx", async () => {
34
+ const rpcUrl = process.env.RPC_URL || "https://mainnet-api.nara.build/";
35
+ const relayUrl = process.env.QUEST_RELAY_URL || "https://quest-api.nara.build/";
36
+ console.log(` Relay: ${relayUrl}`);
37
+ const connection = new Connection(rpcUrl, "confirmed");
38
+
39
+ const quest = await getQuestInfo(connection);
40
+ if (!quest.active || quest.expired) {
41
+ console.log(" (skipped: no active quest)");
42
+ return;
43
+ }
44
+
45
+ const questions = loadTestQuestions();
46
+ const match = questions.find((q) => q.text === quest.question);
47
+ if (!match) {
48
+ console.log(` (skipped: question not found in test-questions.json)`);
49
+ console.log(` Question: ${quest.question}`);
50
+ return;
51
+ }
52
+
53
+ console.log(` Question: ${quest.question}`);
54
+ console.log(` Answer: ${match.answer}`);
55
+
56
+ const { stdout, stderr, exitCode } = await runCli([
57
+ "quest", "answer", match.answer,
58
+ "--relay",
59
+ "--agent", "test",
60
+ "--model", "test",
61
+ ]);
62
+
63
+ const output = stdout + stderr;
64
+
65
+ if (output.includes("already answered") || output.includes("Already answered")) {
66
+ console.log(" Already answered this round");
67
+ return;
68
+ }
69
+ if (output.includes("expired")) {
70
+ console.log(" Quest expired during test");
71
+ return;
72
+ }
73
+
74
+ assert.equal(exitCode, 0, `CLI failed: ${stderr}`);
75
+ assert.ok(output.includes("Transaction:") || output.includes("submitted"), "should confirm submission");
76
+
77
+ // Extract and print tx signature
78
+ const txMatch = output.match(/Transaction:\s+(\S+)/);
79
+ if (txMatch) {
80
+ console.log(` TX: ${txMatch[1]}`);
81
+ }
82
+
83
+ // Print reward info
84
+ if (output.includes("Reward received")) {
85
+ const rewardMatch = output.match(/Reward received:\s+(.+)/);
86
+ console.log(` Reward: ${rewardMatch ? rewardMatch[1] : "yes"}`);
87
+ } else if (output.includes("no reward") || output.includes("all reward slots")) {
88
+ console.log(" Reward: none (all slots claimed)");
89
+ }
90
+ });
91
+ });
@@ -168,4 +168,4 @@ describe("quest answer (on-chain)", { skip: !hasWallet ? "no wallet" : undefined
168
168
  console.log(` Output: ${stdout.trim()}`);
169
169
  }
170
170
  });
171
- });
171
+ });