nara-sdk 1.0.84 → 1.0.85

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 CHANGED
@@ -22,7 +22,7 @@ npm install nara-sdk
22
22
  ## Features
23
23
 
24
24
  - **Agent Registry** — Register agents, bind Twitter, submit tweets, verify, referral system
25
- - **Quest (PoMI)** — Proof of Machine Intelligence ZK quest system, stake, answer-to-earn
25
+ - **Quest (Boost PoMI)** — Proof of Machine Intelligence ZK quest system — credit-gated, answer-to-earn
26
26
  - **Skills Hub** — On-chain skill registry for AI agents, upload/query skill content
27
27
  - **ZK ID** — Zero-knowledge anonymous identity, deposit, withdraw, ownership proofs
28
28
  - **Cross-chain Bridge** — Nara ↔ Solana bridge via Hyperlane warp routes (USDC, SOL), with in-tx fee extraction and validator signature tracking
@@ -38,7 +38,7 @@ const connection = new Connection('https://mainnet-api.nara.build');
38
38
 
39
39
  ## Cross-chain Bridge
40
40
 
41
- Bridge tokens between Solana and Nara with built-in 0.5% fee extraction.
41
+ Bridge tokens between Solana and Nara with built-in fee extraction (0.5% or per-token floor, whichever is higher).
42
42
 
43
43
  ### One-step bridge
44
44
 
@@ -106,29 +106,36 @@ console.log(status.deliverySignature); // destination tx
106
106
 
107
107
  ### Supported tokens
108
108
 
109
- | Token | Solana side | Nara side | Decimals |
110
- |---|---|---|---|
111
- | USDC | collateral (lock) | synthetic (mint, Token-2022) | 6 |
112
- | USDT | collateral (lock) | synthetic (mint, Token-2022) | 6 |
113
- | SOL | native (lamports) | synthetic (mint, Token-2022) | 9 |
109
+ | Token | Solana side | Nara side | Decimals | Min bridge | Min fee |
110
+ |---|---|---|---|---|---|
111
+ | USDC | collateral (lock) | synthetic (mint, Token-2022) | 6 | 5 USDC | 0.5 USDC |
112
+ | USDT | collateral (lock) | synthetic (mint, Token-2022) | 6 | 5 USDT | 0.5 USDT |
113
+ | SOL | native (lamports) | synthetic (mint, Token-2022) | 9 | 0.1 SOL | 0.01 SOL |
114
+
115
+ Requests below the per-token `minAmount` are rejected client-side.
114
116
 
115
117
  Add new tokens at runtime:
116
118
 
117
119
  ```ts
118
120
  import { registerBridgeToken } from 'nara-sdk';
119
121
 
120
- registerBridgeToken('USDT', {
121
- symbol: 'USDT',
122
+ registerBridgeToken('XYZ', {
123
+ symbol: 'XYZ',
122
124
  decimals: 6,
125
+ minAmount: 5_000_000n, // 5.0
126
+ minFee: 500_000n, // 0.5 (fee floor)
123
127
  solana: { warpProgram, mode: 'collateral', mint, tokenProgram },
124
- nara: { warpProgram, mode: 'synthetic', mint, tokenProgram },
128
+ nara: { warpProgram, mode: 'synthetic', mint, tokenProgram },
125
129
  });
126
130
  ```
127
131
 
128
132
  ### Fee configuration
129
133
 
130
- Default fee: **0.5%** (50 bps), deducted from the bridged amount on the source chain.
131
- Fee recipients are chain-specific (one per source chain).
134
+ Fee formula: **`fee = max(amount × 0.5%, token.minFee)`** deducted from the
135
+ bridged amount on the source chain in the same transaction. Below the
136
+ crossover (100 USDC / 2 SOL) the floor dominates; above, the percentage wins.
137
+
138
+ Fee recipients are chain-specific (one per source chain):
132
139
 
133
140
  ```ts
134
141
  import { setBridgeFeeRecipient, getBridgeFeeRecipient } from 'nara-sdk';
@@ -143,8 +150,8 @@ const recipient = getBridgeFeeRecipient('solana'); // PublicKey
143
150
  // Or per-call
144
151
  await bridgeTransfer(conn, wallet, {
145
152
  ...params,
146
- feeBps: 100, // 1%
147
- feeRecipient: customPubkey, // override
153
+ feeBps: 100, // 1% (overrides default 50 bps)
154
+ feeRecipient: customPubkey, // override recipient
148
155
  skipFee: true, // or skip entirely
149
156
  });
150
157
  ```
@@ -170,19 +177,53 @@ await verifyTwitter(connection, verifierWallet, agentId);
170
177
 
171
178
  // Tweet submission & approval
172
179
  await submitTweet(connection, wallet, agentId, tweetId, tweetUrl);
173
- await approveTweet(connection, verifierWallet, agentId, tweetId, freeCredits);
180
+ await approveTweet(connection, verifierWallet, agentId, tweetId, boostCreditsDelta);
174
181
  ```
175
182
 
176
- ## Quest (PoMI)
183
+ ## Quest (Boost PoMI)
184
+
185
+ Boost PoMI is a single-track reward system. The staking channel is closed —
186
+ **boost credits** are the sole admission ticket for submitting an answer:
187
+
188
+ - 1 credit is consumed per successful reward
189
+ - `submitAnswer` throws if `stakeInfo.boostCredits === 0`
190
+ - Credits are granted by the `stake_authority` (see `adjustBoostCredits`) or
191
+ as part of agent-registry flows (`approveTweet`, `verifyTwitter`, etc.)
177
192
 
178
193
  ```ts
179
- import { getQuestInfo, generateProof, submitAnswer } from 'nara-sdk';
194
+ import {
195
+ getQuestInfo,
196
+ getStakeInfo,
197
+ generateProof,
198
+ submitAnswer,
199
+ } from 'nara-sdk';
180
200
 
181
201
  const quest = await getQuestInfo(connection);
182
- const proof = await generateProof(quest.question, answer);
183
- const sig = await submitAnswer(connection, wallet, proof);
202
+ console.log(`Boost slots: ${quest.stakeRemainingSlots}/${quest.stakeRewardCount}`);
203
+
204
+ const stakeInfo = await getStakeInfo(connection, wallet.publicKey);
205
+ console.log(`Your boost credits: ${stakeInfo?.boostCredits ?? 0}`);
206
+
207
+ const proof = await generateProof(
208
+ answer,
209
+ quest.answerHash,
210
+ wallet.publicKey,
211
+ quest.round,
212
+ );
213
+
214
+ const { signature } = await submitAnswer(connection, wallet, proof.solana);
184
215
  ```
185
216
 
217
+ Relevant `QuestInfo` fields:
218
+
219
+ - `stakeRewardCount` / `stakeWinnerCount` / `stakeRewardPerWinner` / `stakeRemainingSlots`
220
+ — the single Boost PoMI winner bucket (field names retain the `stake*` prefix
221
+ for chain parity)
222
+ - `round` / `question` / `answerHash` / `deadline` / `timeRemaining`
223
+
224
+ Legacy staking (`stake` / `unstake`) remains callable so existing stakers can
225
+ withdraw; it no longer gates mining.
226
+
186
227
  ## Documentation
187
228
 
188
229
  Full API reference at [nara.build/docs](https://nara.build/docs).
package/index.ts CHANGED
@@ -49,8 +49,8 @@ export {
49
49
  setQuestInterval,
50
50
  setRewardPerShare,
51
51
  setStakeAuthority,
52
- makeAdjustFreeStakeIx,
53
- adjustFreeStake,
52
+ makeAdjustBoostCreditsIx,
53
+ adjustBoostCredits,
54
54
  claimAirdrop,
55
55
  setAirdropConfig,
56
56
  getQuestConfig,
@@ -120,6 +120,7 @@ export {
120
120
  registerAgentWithReferral,
121
121
  getAgentRecord,
122
122
  getAgentInfo,
123
+ listAgentsByAuthority,
123
124
  getAgentMemory,
124
125
  getConfig as getAgentRegistryConfig,
125
126
  setBio,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nara-sdk",
3
- "version": "1.0.84",
3
+ "version": "1.0.85",
4
4
  "description": "SDK for the Nara chain (Solana-compatible)",
5
5
  "module": "index.ts",
6
6
  "main": "index.ts",
@@ -14,6 +14,7 @@ import * as anchor from "@coral-xyz/anchor";
14
14
  import { Program, AnchorProvider, Wallet } from "@coral-xyz/anchor";
15
15
  import BN from "bn.js";
16
16
  import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
17
+ import bs58 from "bs58";
17
18
  import type { NaraAgentRegistry } from "./idls/nara_agent_registry";
18
19
  import { DEFAULT_AGENT_REGISTRY_PROGRAM_ID } from "./constants";
19
20
  import { sendTx } from "./tx";
@@ -342,6 +343,43 @@ export async function getAgentRecord(
342
343
  return parseAgentRecordData(accountInfo.data);
343
344
  }
344
345
 
346
+ /** AgentState account discriminator (from IDL). */
347
+ const AGENT_STATE_DISCRIMINATOR = Buffer.from([254, 187, 98, 119, 228, 48, 47, 49]);
348
+
349
+ /**
350
+ * List all agent IDs owned by a given authority.
351
+ * Scans the program via `getProgramAccounts` with memcmp filters on the
352
+ * account discriminator and the `authority` field (offset 8).
353
+ * Uses `dataSlice` to return only the `agent_id` portion — cheap on bandwidth.
354
+ */
355
+ export async function listAgentsByAuthority(
356
+ connection: Connection,
357
+ authority: PublicKey,
358
+ options?: AgentRegistryOptions
359
+ ): Promise<string[]> {
360
+ const pid = new PublicKey(options?.programId ?? DEFAULT_AGENT_REGISTRY_PROGRAM_ID);
361
+
362
+ // AgentState layout (after 8-byte discriminator):
363
+ // 32 authority | 32 pending_buffer | 32 memory |
364
+ // 8 created_at | 8 updated_at |
365
+ // 4 version | 4 agent_id_len | 32 agent_id | ...
366
+ // Absolute offsets: authority=8, agent_id_len=124, agent_id=128
367
+ const accounts = await connection.getProgramAccounts(pid, {
368
+ commitment: "confirmed",
369
+ filters: [
370
+ { memcmp: { offset: 0, bytes: bs58.encode(AGENT_STATE_DISCRIMINATOR) } },
371
+ { memcmp: { offset: 8, bytes: authority.toBase58() } },
372
+ ],
373
+ dataSlice: { offset: 124, length: 36 },
374
+ });
375
+
376
+ return accounts.map(({ account }) => {
377
+ const data = Buffer.from(account.data);
378
+ const agentIdLen = data.readUInt32LE(0);
379
+ return data.subarray(4, 4 + agentIdLen).toString("utf-8");
380
+ });
381
+ }
382
+
345
383
  /**
346
384
  * Fetch an agent's record, bio, and metadata in one call.
347
385
  */
@@ -1512,7 +1550,7 @@ export async function unbindTwitter(
1512
1550
  /**
1513
1551
  * Verify an agent's twitter (verifier-only).
1514
1552
  * Awards verification reward and points to the agent owner.
1515
- * @param freeStakeDelta - If provided, also adjusts free stake credits for the agent owner in the same tx.
1553
+ * @param boostCreditsDelta - If provided, also adjusts boost credits for the agent owner in the same tx.
1516
1554
  */
1517
1555
  export async function verifyTwitter(
1518
1556
  connection: Connection,
@@ -1520,8 +1558,8 @@ export async function verifyTwitter(
1520
1558
  agentId: string,
1521
1559
  username: string,
1522
1560
  options?: AgentRegistryOptions,
1523
- freeStakeDelta?: number,
1524
- freeStakeReason?: string
1561
+ boostCreditsDelta?: number,
1562
+ boostCreditsReason?: string
1525
1563
  ): Promise<string> {
1526
1564
  const program = createProgram(connection, wallet, options?.programId);
1527
1565
  const agentPda = getAgentPda(program.programId, agentId);
@@ -1546,12 +1584,12 @@ export async function verifyTwitter(
1546
1584
  .instruction();
1547
1585
 
1548
1586
  const ixs = [ix];
1549
- if (freeStakeDelta !== undefined && freeStakeDelta !== 0) {
1550
- const { makeAdjustFreeStakeIx } = await import("./quest");
1551
- const freeStakeIx = await makeAdjustFreeStakeIx(
1552
- connection, wallet.publicKey, authority, freeStakeDelta, freeStakeReason ?? ""
1587
+ if (boostCreditsDelta !== undefined && boostCreditsDelta !== 0) {
1588
+ const { makeAdjustBoostCreditsIx } = await import("./quest");
1589
+ const boostCreditsIx = await makeAdjustBoostCreditsIx(
1590
+ connection, wallet.publicKey, authority, boostCreditsDelta, boostCreditsReason ?? ""
1553
1591
  );
1554
- ixs.push(freeStakeIx);
1592
+ ixs.push(boostCreditsIx);
1555
1593
  }
1556
1594
 
1557
1595
  return sendTx(connection, wallet, ixs);
@@ -1577,7 +1615,7 @@ export async function rejectTwitter(
1577
1615
  /**
1578
1616
  * Approve a previously rejected twitter verification (verifier-only).
1579
1617
  * Awards verification reward and points to the agent owner.
1580
- * @param freeStakeDelta - If provided, also adjusts free stake credits for the agent owner in the same tx.
1618
+ * @param boostCreditsDelta - If provided, also adjusts boost credits for the agent owner in the same tx.
1581
1619
  */
1582
1620
  export async function approveRejectedTwitter(
1583
1621
  connection: Connection,
@@ -1585,8 +1623,8 @@ export async function approveRejectedTwitter(
1585
1623
  agentId: string,
1586
1624
  username: string,
1587
1625
  options?: AgentRegistryOptions,
1588
- freeStakeDelta?: number,
1589
- freeStakeReason?: string
1626
+ boostCreditsDelta?: number,
1627
+ boostCreditsReason?: string
1590
1628
  ): Promise<string> {
1591
1629
  const program = createProgram(connection, wallet, options?.programId);
1592
1630
  const agentPda = getAgentPda(program.programId, agentId);
@@ -1610,12 +1648,12 @@ export async function approveRejectedTwitter(
1610
1648
  .instruction();
1611
1649
 
1612
1650
  const ixs = [ix];
1613
- if (freeStakeDelta !== undefined && freeStakeDelta !== 0) {
1614
- const { makeAdjustFreeStakeIx } = await import("./quest");
1615
- const freeStakeIx = await makeAdjustFreeStakeIx(
1616
- connection, wallet.publicKey, authority, freeStakeDelta, freeStakeReason ?? ""
1651
+ if (boostCreditsDelta !== undefined && boostCreditsDelta !== 0) {
1652
+ const { makeAdjustBoostCreditsIx } = await import("./quest");
1653
+ const boostCreditsIx = await makeAdjustBoostCreditsIx(
1654
+ connection, wallet.publicKey, authority, boostCreditsDelta, boostCreditsReason ?? ""
1617
1655
  );
1618
- ixs.push(freeStakeIx);
1656
+ ixs.push(boostCreditsIx);
1619
1657
  }
1620
1658
 
1621
1659
  return sendTx(connection, wallet, ixs);
@@ -1624,7 +1662,7 @@ export async function approveRejectedTwitter(
1624
1662
  /**
1625
1663
  * Approve a tweet verification (verifier-only).
1626
1664
  * Awards tweet verify reward and points to the agent owner.
1627
- * @param freeStakeDelta - If provided, also adjusts free stake credits for the agent owner in the same tx.
1665
+ * @param boostCreditsDelta - If provided, also adjusts boost credits for the agent owner in the same tx.
1628
1666
  */
1629
1667
  export async function approveTweet(
1630
1668
  connection: Connection,
@@ -1632,8 +1670,8 @@ export async function approveTweet(
1632
1670
  agentId: string,
1633
1671
  tweetId: bigint,
1634
1672
  options?: AgentRegistryOptions,
1635
- freeStakeDelta?: number,
1636
- freeStakeReason?: string
1673
+ boostCreditsDelta?: number,
1674
+ boostCreditsReason?: string
1637
1675
  ): Promise<string> {
1638
1676
  const program = createProgram(connection, wallet, options?.programId);
1639
1677
  const agentPda = getAgentPda(program.programId, agentId);
@@ -1658,12 +1696,12 @@ export async function approveTweet(
1658
1696
  .instruction();
1659
1697
 
1660
1698
  const ixs = [ix];
1661
- if (freeStakeDelta !== undefined && freeStakeDelta !== 0) {
1662
- const { makeAdjustFreeStakeIx } = await import("./quest");
1663
- const freeStakeIx = await makeAdjustFreeStakeIx(
1664
- connection, wallet.publicKey, authority, freeStakeDelta, freeStakeReason ?? ""
1699
+ if (boostCreditsDelta !== undefined && boostCreditsDelta !== 0) {
1700
+ const { makeAdjustBoostCreditsIx } = await import("./quest");
1701
+ const boostCreditsIx = await makeAdjustBoostCreditsIx(
1702
+ connection, wallet.publicKey, authority, boostCreditsDelta, boostCreditsReason ?? ""
1665
1703
  );
1666
- ixs.push(freeStakeIx);
1704
+ ixs.push(boostCreditsIx);
1667
1705
  }
1668
1706
 
1669
1707
  return sendTx(connection, wallet, ixs);
@@ -1887,8 +1887,8 @@
1887
1887
  },
1888
1888
  {
1889
1889
  "code": 6017,
1890
- "name": "FreeCreditsOverflow",
1891
- "msg": "Free credits overflow"
1890
+ "name": "BoostCreditsOverflow",
1891
+ "msg": "Boost credits overflow"
1892
1892
  },
1893
1893
  {
1894
1894
  "code": 6018,
@@ -1919,6 +1919,11 @@
1919
1919
  "code": 6023,
1920
1920
  "name": "InvalidMultiplier",
1921
1921
  "msg": "Multiplier must be >= 1"
1922
+ },
1923
+ {
1924
+ "code": 6024,
1925
+ "name": "NoCredits",
1926
+ "msg": "Boost PoMI requires free credits"
1922
1927
  }
1923
1928
  ],
1924
1929
  "types": [
@@ -2127,15 +2132,19 @@
2127
2132
  "type": "u64"
2128
2133
  },
2129
2134
  {
2130
- "name": "free_credits",
2135
+ "name": "boost_credits",
2131
2136
  "type": "u32"
2132
2137
  },
2138
+ {
2139
+ "name": "user_pubkey",
2140
+ "type": "pubkey"
2141
+ },
2133
2142
  {
2134
2143
  "name": "_padding",
2135
2144
  "type": {
2136
2145
  "array": [
2137
2146
  "u8",
2138
- 60
2147
+ 28
2139
2148
  ]
2140
2149
  }
2141
2150
  }
@@ -1893,8 +1893,8 @@ export type NaraQuest = {
1893
1893
  },
1894
1894
  {
1895
1895
  "code": 6017,
1896
- "name": "freeCreditsOverflow",
1897
- "msg": "Free credits overflow"
1896
+ "name": "boostCreditsOverflow",
1897
+ "msg": "Boost credits overflow"
1898
1898
  },
1899
1899
  {
1900
1900
  "code": 6018,
@@ -1925,6 +1925,11 @@ export type NaraQuest = {
1925
1925
  "code": 6023,
1926
1926
  "name": "invalidMultiplier",
1927
1927
  "msg": "Multiplier must be >= 1"
1928
+ },
1929
+ {
1930
+ "code": 6024,
1931
+ "name": "noCredits",
1932
+ "msg": "Boost PoMI requires free credits"
1928
1933
  }
1929
1934
  ],
1930
1935
  "types": [
@@ -2133,15 +2138,19 @@ export type NaraQuest = {
2133
2138
  "type": "u64"
2134
2139
  },
2135
2140
  {
2136
- "name": "freeCredits",
2141
+ "name": "boostCredits",
2137
2142
  "type": "u32"
2138
2143
  },
2144
+ {
2145
+ "name": "userPubkey",
2146
+ "type": "pubkey"
2147
+ },
2139
2148
  {
2140
2149
  "name": "padding",
2141
2150
  "type": {
2142
2151
  "array": [
2143
2152
  "u8",
2144
- 60
2153
+ 28
2145
2154
  ]
2146
2155
  }
2147
2156
  }
package/src/quest.ts CHANGED
@@ -45,41 +45,42 @@ export interface QuestInfo {
45
45
  round: string;
46
46
  question: string;
47
47
  answerHash: number[];
48
- /** Total reward pool for the round (stake + free), in NARA */
48
+ /** Total reward pool for the round, in NARA */
49
49
  totalReward: number;
50
- /** Stake-gated reward bucket */
50
+ /**
51
+ * Boost PoMI winner bucket (single reward track).
52
+ * Named `stake*` for chain-field parity — the underlying pool fields are still
53
+ * `stake_reward_count` / `stake_reward_per_winner` / `stake_winner_count`.
54
+ */
51
55
  stakeRewardCount: number;
52
56
  stakeWinnerCount: number;
53
57
  stakeRewardPerWinner: number;
54
58
  stakeRemainingSlots: number;
55
- /** Credit (free-stake) reward bucket — consumed by stakeInfo.freeCredits */
56
- creditRewardCount: number;
57
- creditWinnerCount: number;
58
- creditRewardPerWinner: number;
59
- creditRemainingSlots: number;
60
59
  difficulty: number;
61
60
  deadline: number;
62
61
  timeRemaining: number;
63
62
  expired: boolean;
64
- /** High stake requirement for the current round (in NARA, decays over time) */
63
+ /** @deprecated Legacy stake decay field. Staking channel is closed; value is informational only. */
65
64
  stakeHigh: number;
66
- /** Low stake requirement for the current round (in NARA, floor after decay) */
65
+ /** @deprecated Legacy stake decay field. Staking channel is closed; value is informational only. */
67
66
  stakeLow: number;
68
- /** Running average participant stake for the current round (in NARA) */
67
+ /** @deprecated Legacy field. Pool no longer accumulates avg participant stake. */
69
68
  avgParticipantStake: number;
70
69
  /** Unix timestamp when the current question was created */
71
70
  createdAt: number;
72
- /** Current effective stake requirement after parabolic decay (in NARA) */
71
+ /** @deprecated Legacy stake requirement. Boost PoMI gates on boost credits, not stake. */
73
72
  effectiveStakeRequirement: number;
74
73
  }
75
74
 
76
75
  export interface StakeInfo {
77
- /** Current staked amount (in NARA) */
76
+ /** Legacy staked amount (in NARA). Staking channel is closed; use `unstake` to withdraw. */
78
77
  amount: number;
79
- /** Round when the stake was made */
78
+ /** Round when the stake was last touched */
80
79
  stakeRound: number;
81
- /** Free stake credits (admin-assigned, bypass stake requirement) */
82
- freeCredits: number;
80
+ /** Boost PoMI credits — required to submit an answer (consumed -1 per successful reward) */
81
+ boostCredits: number;
82
+ /** On-chain record of the user's pubkey (populated on stake / adjust / answer) */
83
+ userPubkey: string;
83
84
  }
84
85
 
85
86
  export interface ZkProof {
@@ -108,7 +109,12 @@ export interface QuestOptions {
108
109
  circuitWasmPath?: string | Uint8Array;
109
110
  /** File path (Node.js), URL string, or pre-loaded Uint8Array (browser) */
110
111
  zkeyPath?: string | Uint8Array;
111
- /** "auto" = auto top-up stake to stakeRequirement; number = stake exact NARA amount */
112
+ /**
113
+ * @deprecated Boost PoMI no longer gates on stake; credits are the sole admission ticket.
114
+ * If set, a legacy `stake` instruction is still bundled before `submit_answer`:
115
+ * - number: stake the exact NARA amount
116
+ * - "auto": no-op (kept for API compatibility)
117
+ */
112
118
  stake?: "auto" | number;
113
119
  }
114
120
 
@@ -332,8 +338,6 @@ export async function getQuestInfo(
332
338
 
333
339
  const stakeRewardCount = pool.stakeRewardCount;
334
340
  const stakeWinnerCount = pool.stakeWinnerCount;
335
- const creditRewardCount = pool.freeRewardCount;
336
- const creditWinnerCount = pool.freeWinnerCount;
337
341
 
338
342
  return {
339
343
  active,
@@ -345,10 +349,6 @@ export async function getQuestInfo(
345
349
  stakeWinnerCount,
346
350
  stakeRewardPerWinner: pool.stakeRewardPerWinner.toNumber() / LAMPORTS_PER_SOL,
347
351
  stakeRemainingSlots: Math.max(0, stakeRewardCount - stakeWinnerCount),
348
- creditRewardCount,
349
- creditWinnerCount,
350
- creditRewardPerWinner: pool.freeRewardPerWinner.toNumber() / LAMPORTS_PER_SOL,
351
- creditRemainingSlots: Math.max(0, creditRewardCount - creditWinnerCount),
352
352
  difficulty: pool.difficulty,
353
353
  deadline,
354
354
  timeRemaining: secsLeft,
@@ -430,6 +430,12 @@ export async function generateProof(
430
430
 
431
431
  /**
432
432
  * Submit a quest answer on-chain (direct submission, requires gas).
433
+ *
434
+ * Boost PoMI: the caller must have at least 1 boost credit; on a successful
435
+ * reward, 1 credit is consumed. Without credits the on-chain program rejects
436
+ * with `NoCredits` (6024); this function pre-checks and throws early with a
437
+ * clearer message.
438
+ *
433
439
  * If `activityLog` is provided, a logActivity instruction from the Agent Registry
434
440
  * is appended to the same transaction.
435
441
  */
@@ -444,37 +450,22 @@ export async function submitAnswer(
444
450
  ): Promise<SubmitAnswerResult> {
445
451
  const program = createProgram(connection, wallet, options?.programId);
446
452
 
447
- // Build optional stake instruction
453
+ // Boost PoMI: fail fast if the user has no credits
454
+ const stakeInfo = await getStakeInfo(connection, wallet.publicKey, options);
455
+ if (!stakeInfo || stakeInfo.boostCredits <= 0) {
456
+ throw new Error(
457
+ "Boost PoMI requires boost credits. Your balance is 0 — acquire credits before submitting an answer."
458
+ );
459
+ }
460
+
461
+ // Legacy optional stake instruction (staking channel is closed but still callable)
448
462
  let stakeIx: any = null;
449
- if (options?.stake !== undefined) {
450
- let stakeLamports: BN;
451
- if (options.stake === "auto") {
452
- const quest = await getQuestInfo(connection, wallet, options);
453
- const stakeInfo = await getStakeInfo(connection, wallet.publicKey, options);
454
- const freeCredits = stakeInfo?.freeCredits ?? 0;
455
-
456
- if (freeCredits > 0) {
457
- // 本轮使用免质押额度,不需要补质押
458
- stakeLamports = new BN(0);
459
- } else {
460
- const required = quest.effectiveStakeRequirement;
461
- const current = stakeInfo?.amount ?? 0;
462
- const deficit = required - current;
463
- if (deficit > 0) {
464
- stakeLamports = new BN(Math.round(deficit * LAMPORTS_PER_SOL));
465
- } else {
466
- stakeLamports = new BN(0);
467
- }
468
- }
469
- } else {
470
- stakeLamports = new BN(Math.round(options.stake * LAMPORTS_PER_SOL));
471
- }
472
- if (!stakeLamports.isZero()) {
473
- stakeIx = await program.methods
474
- .stake(stakeLamports)
475
- .accounts({ user: wallet.publicKey } as any)
476
- .instruction();
477
- }
463
+ if (typeof options?.stake === "number" && options.stake > 0) {
464
+ const stakeLamports = new BN(Math.round(options.stake * LAMPORTS_PER_SOL));
465
+ stakeIx = await program.methods
466
+ .stake(stakeLamports)
467
+ .accounts({ user: wallet.publicKey } as any)
468
+ .instruction();
478
469
  }
479
470
 
480
471
  const submitIx = await program.methods
@@ -581,12 +572,19 @@ export async function parseQuestReward(
581
572
  let winner = "";
582
573
  const logs: string[] = txInfo.meta?.logMessages ?? [];
583
574
  for (const log of logs) {
584
- const m = log.match(/reward (\d+) lamports \(winner (\d+\/\d+)\)/);
575
+ // Boost PoMI success: "Boost PoMI reward N lamports (winner W/T, credits remaining: R)"
576
+ // Vault-insufficient notice: "Answer verified but vault insufficient (winner W/T, credit preserved)"
577
+ const m = log.match(/reward (\d+) lamports \(winner (\d+\/\d+)/);
585
578
  if (m) {
586
579
  rewardLamports = parseInt(m[1]!);
587
580
  winner = m[2]!;
588
581
  break;
589
582
  }
583
+ const v = log.match(/vault insufficient \(winner (\d+\/\d+)/);
584
+ if (v) {
585
+ winner = v[1]!;
586
+ // rewardLamports stays 0
587
+ }
590
588
  }
591
589
 
592
590
  return {
@@ -693,7 +691,8 @@ export async function getStakeInfo(
693
691
  return {
694
692
  amount,
695
693
  stakeRound: record.stakeRound.toNumber(),
696
- freeCredits: record.freeCredits,
694
+ boostCredits: record.boostCredits,
695
+ userPubkey: record.userPubkey.toBase58(),
697
696
  };
698
697
  }
699
698
 
@@ -775,15 +774,16 @@ export async function initializeQuest(
775
774
  /**
776
775
  * Set the reward config (authority only).
777
776
  *
778
- * @param freeStakeMultiplier - Multiplier for users answering with free credits
779
- * vs stake-based winners (must be >= 1).
777
+ * @param freeStakeMultiplier - Legacy field (>= 1). Currently unused by Boost PoMI
778
+ * reward calculation; defaults to 1. Kept exposed in
779
+ * case chain re-enables it.
780
780
  */
781
781
  export async function setRewardConfig(
782
782
  connection: Connection,
783
783
  wallet: Keypair,
784
784
  minRewardCount: number,
785
785
  maxRewardCount: number,
786
- freeStakeMultiplier: number,
786
+ freeStakeMultiplier: number = 1,
787
787
  options?: QuestOptions
788
788
  ): Promise<string> {
789
789
  const program = createProgram(connection, wallet, options?.programId);
@@ -908,7 +908,6 @@ export async function getQuestConfig(
908
908
  stakeAuthority: PublicKey;
909
909
  airdropAmount: number;
910
910
  maxAirdropCount: number;
911
- freeStakeMultiplier: number;
912
911
  }> {
913
912
  const kp = Keypair.generate();
914
913
  const program = createProgram(connection, kp, options?.programId);
@@ -933,7 +932,6 @@ export async function getQuestConfig(
933
932
  stakeAuthority: config.stakeAuthority,
934
933
  airdropAmount: Number(config.airdropAmount.toString()),
935
934
  maxAirdropCount: config.maxAirdropCount,
936
- freeStakeMultiplier: config.freeStakeMultiplier,
937
935
  };
938
936
  }
939
937
 
@@ -955,9 +953,10 @@ export async function setStakeAuthority(
955
953
  }
956
954
 
957
955
  /**
958
- * Build an adjustFreeStake instruction without sending it.
956
+ * Build an adjustBoostCredits instruction without sending it.
957
+ * (Wraps the on-chain `adjustFreeStake` instruction — legacy chain-side name.)
959
958
  */
960
- export async function makeAdjustFreeStakeIx(
959
+ export async function makeAdjustBoostCreditsIx(
961
960
  connection: Connection,
962
961
  caller: PublicKey,
963
962
  user: PublicKey,
@@ -973,12 +972,12 @@ export async function makeAdjustFreeStakeIx(
973
972
  }
974
973
 
975
974
  /**
976
- * Adjust free stake credits for a user (stake_authority or authority only).
977
- * @param user - The user whose free credits to adjust
975
+ * Adjust boost credits for a user (stake_authority or authority only).
976
+ * @param user - The user whose boost credits to adjust
978
977
  * @param delta - Amount to adjust (positive to add, negative to remove)
979
978
  * @param reason - Reason for the adjustment (logged on-chain)
980
979
  */
981
- export async function adjustFreeStake(
980
+ export async function adjustBoostCredits(
982
981
  connection: Connection,
983
982
  wallet: Keypair,
984
983
  user: PublicKey,
@@ -986,7 +985,7 @@ export async function adjustFreeStake(
986
985
  reason: string,
987
986
  options?: QuestOptions
988
987
  ): Promise<string> {
989
- const ix = await makeAdjustFreeStakeIx(connection, wallet.publicKey, user, delta, reason, options);
988
+ const ix = await makeAdjustBoostCreditsIx(connection, wallet.publicKey, user, delta, reason, options);
990
989
  return sendTx(connection, wallet, [ix]);
991
990
  }
992
991