nara-sdk 1.0.84 → 1.0.86

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,52 @@ 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.boostRemainingSlots}/${quest.boostRewardCount}`);
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
+ - `boostRewardCount` / `boostWinnerCount` / `boostRewardPerWinner` / `boostRemainingSlots`
220
+ — the single Boost PoMI winner bucket
221
+ - `round` / `question` / `answerHash` / `deadline` / `timeRemaining`
222
+
223
+ Legacy staking (`stake` / `unstake`) remains callable so existing stakers can
224
+ withdraw; it no longer gates mining.
225
+
186
226
  ## Documentation
187
227
 
188
228
  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.86",
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": [
@@ -2062,15 +2067,15 @@
2062
2067
  "type": "u64"
2063
2068
  },
2064
2069
  {
2065
- "name": "stake_reward_count",
2070
+ "name": "boost_reward_count",
2066
2071
  "type": "u32"
2067
2072
  },
2068
2073
  {
2069
- "name": "stake_reward_per_winner",
2074
+ "name": "boost_reward_per_winner",
2070
2075
  "type": "u64"
2071
2076
  },
2072
2077
  {
2073
- "name": "stake_winner_count",
2078
+ "name": "boost_winner_count",
2074
2079
  "type": "u32"
2075
2080
  },
2076
2081
  {
@@ -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": [
@@ -2068,15 +2073,15 @@ export type NaraQuest = {
2068
2073
  "type": "u64"
2069
2074
  },
2070
2075
  {
2071
- "name": "stakeRewardCount",
2076
+ "name": "boostRewardCount",
2072
2077
  "type": "u32"
2073
2078
  },
2074
2079
  {
2075
- "name": "stakeRewardPerWinner",
2080
+ "name": "boostRewardPerWinner",
2076
2081
  "type": "u64"
2077
2082
  },
2078
2083
  {
2079
- "name": "stakeWinnerCount",
2084
+ "name": "boostWinnerCount",
2080
2085
  "type": "u32"
2081
2086
  },
2082
2087
  {
@@ -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,38 @@ 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 */
51
- stakeRewardCount: number;
52
- stakeWinnerCount: number;
53
- stakeRewardPerWinner: number;
54
- stakeRemainingSlots: number;
55
- /** Credit (free-stake) reward bucket — consumed by stakeInfo.freeCredits */
56
- creditRewardCount: number;
57
- creditWinnerCount: number;
58
- creditRewardPerWinner: number;
59
- creditRemainingSlots: number;
50
+ /** Boost PoMI winner bucket (single reward track). */
51
+ boostRewardCount: number;
52
+ boostWinnerCount: number;
53
+ boostRewardPerWinner: number;
54
+ boostRemainingSlots: number;
60
55
  difficulty: number;
61
56
  deadline: number;
62
57
  timeRemaining: number;
63
58
  expired: boolean;
64
- /** High stake requirement for the current round (in NARA, decays over time) */
59
+ /** @deprecated Legacy stake decay field. Staking channel is closed; value is informational only. */
65
60
  stakeHigh: number;
66
- /** Low stake requirement for the current round (in NARA, floor after decay) */
61
+ /** @deprecated Legacy stake decay field. Staking channel is closed; value is informational only. */
67
62
  stakeLow: number;
68
- /** Running average participant stake for the current round (in NARA) */
63
+ /** @deprecated Legacy field. Pool no longer accumulates avg participant stake. */
69
64
  avgParticipantStake: number;
70
65
  /** Unix timestamp when the current question was created */
71
66
  createdAt: number;
72
- /** Current effective stake requirement after parabolic decay (in NARA) */
67
+ /** @deprecated Legacy stake requirement. Boost PoMI gates on boost credits, not stake. */
73
68
  effectiveStakeRequirement: number;
74
69
  }
75
70
 
76
71
  export interface StakeInfo {
77
- /** Current staked amount (in NARA) */
72
+ /** Legacy staked amount (in NARA). Staking channel is closed; use `unstake` to withdraw. */
78
73
  amount: number;
79
- /** Round when the stake was made */
74
+ /** Round when the stake was last touched */
80
75
  stakeRound: number;
81
- /** Free stake credits (admin-assigned, bypass stake requirement) */
82
- freeCredits: number;
76
+ /** Boost PoMI credits — required to submit an answer (consumed -1 per successful reward) */
77
+ boostCredits: number;
78
+ /** On-chain record of the user's pubkey (populated on stake / adjust / answer) */
79
+ userPubkey: string;
83
80
  }
84
81
 
85
82
  export interface ZkProof {
@@ -108,7 +105,12 @@ export interface QuestOptions {
108
105
  circuitWasmPath?: string | Uint8Array;
109
106
  /** File path (Node.js), URL string, or pre-loaded Uint8Array (browser) */
110
107
  zkeyPath?: string | Uint8Array;
111
- /** "auto" = auto top-up stake to stakeRequirement; number = stake exact NARA amount */
108
+ /**
109
+ * @deprecated Boost PoMI no longer gates on stake; credits are the sole admission ticket.
110
+ * If set, a legacy `stake` instruction is still bundled before `submit_answer`:
111
+ * - number: stake the exact NARA amount
112
+ * - "auto": no-op (kept for API compatibility)
113
+ */
112
114
  stake?: "auto" | number;
113
115
  }
114
116
 
@@ -330,10 +332,8 @@ export async function getQuestInfo(
330
332
  stakeHigh, stakeLow, createdAtMs, decayMs, nowMs
331
333
  );
332
334
 
333
- const stakeRewardCount = pool.stakeRewardCount;
334
- const stakeWinnerCount = pool.stakeWinnerCount;
335
- const creditRewardCount = pool.freeRewardCount;
336
- const creditWinnerCount = pool.freeWinnerCount;
335
+ const boostRewardCount = pool.boostRewardCount;
336
+ const boostWinnerCount = pool.boostWinnerCount;
337
337
 
338
338
  return {
339
339
  active,
@@ -341,14 +341,10 @@ export async function getQuestInfo(
341
341
  question: pool.question,
342
342
  answerHash: Array.from(pool.answerHash),
343
343
  totalReward: pool.rewardAmount.toNumber() / LAMPORTS_PER_SOL,
344
- stakeRewardCount,
345
- stakeWinnerCount,
346
- stakeRewardPerWinner: pool.stakeRewardPerWinner.toNumber() / LAMPORTS_PER_SOL,
347
- 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),
344
+ boostRewardCount,
345
+ boostWinnerCount,
346
+ boostRewardPerWinner: pool.boostRewardPerWinner.toNumber() / LAMPORTS_PER_SOL,
347
+ boostRemainingSlots: Math.max(0, boostRewardCount - boostWinnerCount),
352
348
  difficulty: pool.difficulty,
353
349
  deadline,
354
350
  timeRemaining: secsLeft,
@@ -430,6 +426,12 @@ export async function generateProof(
430
426
 
431
427
  /**
432
428
  * Submit a quest answer on-chain (direct submission, requires gas).
429
+ *
430
+ * Boost PoMI: the caller must have at least 1 boost credit; on a successful
431
+ * reward, 1 credit is consumed. Without credits the on-chain program rejects
432
+ * with `NoCredits` (6024); this function pre-checks and throws early with a
433
+ * clearer message.
434
+ *
433
435
  * If `activityLog` is provided, a logActivity instruction from the Agent Registry
434
436
  * is appended to the same transaction.
435
437
  */
@@ -444,37 +446,22 @@ export async function submitAnswer(
444
446
  ): Promise<SubmitAnswerResult> {
445
447
  const program = createProgram(connection, wallet, options?.programId);
446
448
 
447
- // Build optional stake instruction
449
+ // Boost PoMI: fail fast if the user has no credits
450
+ const stakeInfo = await getStakeInfo(connection, wallet.publicKey, options);
451
+ if (!stakeInfo || stakeInfo.boostCredits <= 0) {
452
+ throw new Error(
453
+ "Boost PoMI requires boost credits. Your balance is 0 — acquire credits before submitting an answer."
454
+ );
455
+ }
456
+
457
+ // Legacy optional stake instruction (staking channel is closed but still callable)
448
458
  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
- }
459
+ if (typeof options?.stake === "number" && options.stake > 0) {
460
+ const stakeLamports = new BN(Math.round(options.stake * LAMPORTS_PER_SOL));
461
+ stakeIx = await program.methods
462
+ .stake(stakeLamports)
463
+ .accounts({ user: wallet.publicKey } as any)
464
+ .instruction();
478
465
  }
479
466
 
480
467
  const submitIx = await program.methods
@@ -581,12 +568,19 @@ export async function parseQuestReward(
581
568
  let winner = "";
582
569
  const logs: string[] = txInfo.meta?.logMessages ?? [];
583
570
  for (const log of logs) {
584
- const m = log.match(/reward (\d+) lamports \(winner (\d+\/\d+)\)/);
571
+ // Boost PoMI success: "Boost PoMI reward N lamports (winner W/T, credits remaining: R)"
572
+ // Vault-insufficient notice: "Answer verified but vault insufficient (winner W/T, credit preserved)"
573
+ const m = log.match(/reward (\d+) lamports \(winner (\d+\/\d+)/);
585
574
  if (m) {
586
575
  rewardLamports = parseInt(m[1]!);
587
576
  winner = m[2]!;
588
577
  break;
589
578
  }
579
+ const v = log.match(/vault insufficient \(winner (\d+\/\d+)/);
580
+ if (v) {
581
+ winner = v[1]!;
582
+ // rewardLamports stays 0
583
+ }
590
584
  }
591
585
 
592
586
  return {
@@ -693,7 +687,8 @@ export async function getStakeInfo(
693
687
  return {
694
688
  amount,
695
689
  stakeRound: record.stakeRound.toNumber(),
696
- freeCredits: record.freeCredits,
690
+ boostCredits: record.boostCredits,
691
+ userPubkey: record.userPubkey.toBase58(),
697
692
  };
698
693
  }
699
694
 
@@ -775,15 +770,16 @@ export async function initializeQuest(
775
770
  /**
776
771
  * Set the reward config (authority only).
777
772
  *
778
- * @param freeStakeMultiplier - Multiplier for users answering with free credits
779
- * vs stake-based winners (must be >= 1).
773
+ * @param freeStakeMultiplier - Legacy field (>= 1). Currently unused by Boost PoMI
774
+ * reward calculation; defaults to 1. Kept exposed in
775
+ * case chain re-enables it.
780
776
  */
781
777
  export async function setRewardConfig(
782
778
  connection: Connection,
783
779
  wallet: Keypair,
784
780
  minRewardCount: number,
785
781
  maxRewardCount: number,
786
- freeStakeMultiplier: number,
782
+ freeStakeMultiplier: number = 1,
787
783
  options?: QuestOptions
788
784
  ): Promise<string> {
789
785
  const program = createProgram(connection, wallet, options?.programId);
@@ -908,7 +904,6 @@ export async function getQuestConfig(
908
904
  stakeAuthority: PublicKey;
909
905
  airdropAmount: number;
910
906
  maxAirdropCount: number;
911
- freeStakeMultiplier: number;
912
907
  }> {
913
908
  const kp = Keypair.generate();
914
909
  const program = createProgram(connection, kp, options?.programId);
@@ -933,7 +928,6 @@ export async function getQuestConfig(
933
928
  stakeAuthority: config.stakeAuthority,
934
929
  airdropAmount: Number(config.airdropAmount.toString()),
935
930
  maxAirdropCount: config.maxAirdropCount,
936
- freeStakeMultiplier: config.freeStakeMultiplier,
937
931
  };
938
932
  }
939
933
 
@@ -955,9 +949,10 @@ export async function setStakeAuthority(
955
949
  }
956
950
 
957
951
  /**
958
- * Build an adjustFreeStake instruction without sending it.
952
+ * Build an adjustBoostCredits instruction without sending it.
953
+ * (Wraps the on-chain `adjustFreeStake` instruction — legacy chain-side name.)
959
954
  */
960
- export async function makeAdjustFreeStakeIx(
955
+ export async function makeAdjustBoostCreditsIx(
961
956
  connection: Connection,
962
957
  caller: PublicKey,
963
958
  user: PublicKey,
@@ -973,12 +968,12 @@ export async function makeAdjustFreeStakeIx(
973
968
  }
974
969
 
975
970
  /**
976
- * Adjust free stake credits for a user (stake_authority or authority only).
977
- * @param user - The user whose free credits to adjust
971
+ * Adjust boost credits for a user (stake_authority or authority only).
972
+ * @param user - The user whose boost credits to adjust
978
973
  * @param delta - Amount to adjust (positive to add, negative to remove)
979
974
  * @param reason - Reason for the adjustment (logged on-chain)
980
975
  */
981
- export async function adjustFreeStake(
976
+ export async function adjustBoostCredits(
982
977
  connection: Connection,
983
978
  wallet: Keypair,
984
979
  user: PublicKey,
@@ -986,7 +981,7 @@ export async function adjustFreeStake(
986
981
  reason: string,
987
982
  options?: QuestOptions
988
983
  ): Promise<string> {
989
- const ix = await makeAdjustFreeStakeIx(connection, wallet.publicKey, user, delta, reason, options);
984
+ const ix = await makeAdjustBoostCreditsIx(connection, wallet.publicKey, user, delta, reason, options);
990
985
  return sendTx(connection, wallet, [ix]);
991
986
  }
992
987