nara-sdk 1.0.83 → 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.83",
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);
@@ -761,6 +761,10 @@
761
761
  {
762
762
  "name": "max_reward_count",
763
763
  "type": "u32"
764
+ },
765
+ {
766
+ "name": "free_stake_multiplier",
767
+ "type": "u32"
764
768
  }
765
769
  ]
766
770
  },
@@ -1883,8 +1887,8 @@
1883
1887
  },
1884
1888
  {
1885
1889
  "code": 6017,
1886
- "name": "FreeCreditsOverflow",
1887
- "msg": "Free credits overflow"
1890
+ "name": "BoostCreditsOverflow",
1891
+ "msg": "Boost credits overflow"
1888
1892
  },
1889
1893
  {
1890
1894
  "code": 6018,
@@ -1910,6 +1914,16 @@
1910
1914
  "code": 6022,
1911
1915
  "name": "InsufficientAirdrop",
1912
1916
  "msg": "Airdrop fund has insufficient balance"
1917
+ },
1918
+ {
1919
+ "code": 6023,
1920
+ "name": "InvalidMultiplier",
1921
+ "msg": "Multiplier must be >= 1"
1922
+ },
1923
+ {
1924
+ "code": 6024,
1925
+ "name": "NoCredits",
1926
+ "msg": "Boost PoMI requires free credits"
1913
1927
  }
1914
1928
  ],
1915
1929
  "types": [
@@ -2006,12 +2020,16 @@
2006
2020
  "name": "max_airdrop_count",
2007
2021
  "type": "u32"
2008
2022
  },
2023
+ {
2024
+ "name": "free_stake_multiplier",
2025
+ "type": "u32"
2026
+ },
2009
2027
  {
2010
2028
  "name": "_padding",
2011
2029
  "type": {
2012
2030
  "array": [
2013
2031
  "u8",
2014
- 20
2032
+ 16
2015
2033
  ]
2016
2034
  }
2017
2035
  }
@@ -2049,15 +2067,15 @@
2049
2067
  "type": "u64"
2050
2068
  },
2051
2069
  {
2052
- "name": "reward_count",
2070
+ "name": "stake_reward_count",
2053
2071
  "type": "u32"
2054
2072
  },
2055
2073
  {
2056
- "name": "reward_per_winner",
2074
+ "name": "stake_reward_per_winner",
2057
2075
  "type": "u64"
2058
2076
  },
2059
2077
  {
2060
- "name": "winner_count",
2078
+ "name": "stake_winner_count",
2061
2079
  "type": "u32"
2062
2080
  },
2063
2081
  {
@@ -2080,12 +2098,24 @@
2080
2098
  "name": "avg_participant_stake",
2081
2099
  "type": "u64"
2082
2100
  },
2101
+ {
2102
+ "name": "free_reward_count",
2103
+ "type": "u32"
2104
+ },
2105
+ {
2106
+ "name": "free_reward_per_winner",
2107
+ "type": "u64"
2108
+ },
2109
+ {
2110
+ "name": "free_winner_count",
2111
+ "type": "u32"
2112
+ },
2083
2113
  {
2084
2114
  "name": "_padding",
2085
2115
  "type": {
2086
2116
  "array": [
2087
2117
  "u8",
2088
- 64
2118
+ 48
2089
2119
  ]
2090
2120
  }
2091
2121
  }
@@ -2102,15 +2132,19 @@
2102
2132
  "type": "u64"
2103
2133
  },
2104
2134
  {
2105
- "name": "free_credits",
2135
+ "name": "boost_credits",
2106
2136
  "type": "u32"
2107
2137
  },
2138
+ {
2139
+ "name": "user_pubkey",
2140
+ "type": "pubkey"
2141
+ },
2108
2142
  {
2109
2143
  "name": "_padding",
2110
2144
  "type": {
2111
2145
  "array": [
2112
2146
  "u8",
2113
- 60
2147
+ 28
2114
2148
  ]
2115
2149
  }
2116
2150
  }
@@ -767,6 +767,10 @@ export type NaraQuest = {
767
767
  {
768
768
  "name": "maxRewardCount",
769
769
  "type": "u32"
770
+ },
771
+ {
772
+ "name": "freeStakeMultiplier",
773
+ "type": "u32"
770
774
  }
771
775
  ]
772
776
  },
@@ -1889,8 +1893,8 @@ export type NaraQuest = {
1889
1893
  },
1890
1894
  {
1891
1895
  "code": 6017,
1892
- "name": "freeCreditsOverflow",
1893
- "msg": "Free credits overflow"
1896
+ "name": "boostCreditsOverflow",
1897
+ "msg": "Boost credits overflow"
1894
1898
  },
1895
1899
  {
1896
1900
  "code": 6018,
@@ -1916,6 +1920,16 @@ export type NaraQuest = {
1916
1920
  "code": 6022,
1917
1921
  "name": "insufficientAirdrop",
1918
1922
  "msg": "Airdrop fund has insufficient balance"
1923
+ },
1924
+ {
1925
+ "code": 6023,
1926
+ "name": "invalidMultiplier",
1927
+ "msg": "Multiplier must be >= 1"
1928
+ },
1929
+ {
1930
+ "code": 6024,
1931
+ "name": "noCredits",
1932
+ "msg": "Boost PoMI requires free credits"
1919
1933
  }
1920
1934
  ],
1921
1935
  "types": [
@@ -2012,12 +2026,16 @@ export type NaraQuest = {
2012
2026
  "name": "maxAirdropCount",
2013
2027
  "type": "u32"
2014
2028
  },
2029
+ {
2030
+ "name": "freeStakeMultiplier",
2031
+ "type": "u32"
2032
+ },
2015
2033
  {
2016
2034
  "name": "padding",
2017
2035
  "type": {
2018
2036
  "array": [
2019
2037
  "u8",
2020
- 20
2038
+ 16
2021
2039
  ]
2022
2040
  }
2023
2041
  }
@@ -2055,15 +2073,15 @@ export type NaraQuest = {
2055
2073
  "type": "u64"
2056
2074
  },
2057
2075
  {
2058
- "name": "rewardCount",
2076
+ "name": "stakeRewardCount",
2059
2077
  "type": "u32"
2060
2078
  },
2061
2079
  {
2062
- "name": "rewardPerWinner",
2080
+ "name": "stakeRewardPerWinner",
2063
2081
  "type": "u64"
2064
2082
  },
2065
2083
  {
2066
- "name": "winnerCount",
2084
+ "name": "stakeWinnerCount",
2067
2085
  "type": "u32"
2068
2086
  },
2069
2087
  {
@@ -2086,12 +2104,24 @@ export type NaraQuest = {
2086
2104
  "name": "avgParticipantStake",
2087
2105
  "type": "u64"
2088
2106
  },
2107
+ {
2108
+ "name": "freeRewardCount",
2109
+ "type": "u32"
2110
+ },
2111
+ {
2112
+ "name": "freeRewardPerWinner",
2113
+ "type": "u64"
2114
+ },
2115
+ {
2116
+ "name": "freeWinnerCount",
2117
+ "type": "u32"
2118
+ },
2089
2119
  {
2090
2120
  "name": "padding",
2091
2121
  "type": {
2092
2122
  "array": [
2093
2123
  "u8",
2094
- 64
2124
+ 48
2095
2125
  ]
2096
2126
  }
2097
2127
  }
@@ -2108,15 +2138,19 @@ export type NaraQuest = {
2108
2138
  "type": "u64"
2109
2139
  },
2110
2140
  {
2111
- "name": "freeCredits",
2141
+ "name": "boostCredits",
2112
2142
  "type": "u32"
2113
2143
  },
2144
+ {
2145
+ "name": "userPubkey",
2146
+ "type": "pubkey"
2147
+ },
2114
2148
  {
2115
2149
  "name": "padding",
2116
2150
  "type": {
2117
2151
  "array": [
2118
2152
  "u8",
2119
- 60
2153
+ 28
2120
2154
  ]
2121
2155
  }
2122
2156
  }
package/src/quest.ts CHANGED
@@ -45,34 +45,42 @@ export interface QuestInfo {
45
45
  round: string;
46
46
  question: string;
47
47
  answerHash: number[];
48
- rewardPerWinner: number;
48
+ /** Total reward pool for the round, in NARA */
49
49
  totalReward: number;
50
- rewardCount: number;
51
- winnerCount: number;
52
- remainingSlots: number;
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
+ */
55
+ stakeRewardCount: number;
56
+ stakeWinnerCount: number;
57
+ stakeRewardPerWinner: number;
58
+ stakeRemainingSlots: number;
53
59
  difficulty: number;
54
60
  deadline: number;
55
61
  timeRemaining: number;
56
62
  expired: boolean;
57
- /** 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. */
58
64
  stakeHigh: number;
59
- /** 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. */
60
66
  stakeLow: number;
61
- /** Running average participant stake for the current round (in NARA) */
67
+ /** @deprecated Legacy field. Pool no longer accumulates avg participant stake. */
62
68
  avgParticipantStake: number;
63
69
  /** Unix timestamp when the current question was created */
64
70
  createdAt: number;
65
- /** Current effective stake requirement after parabolic decay (in NARA) */
71
+ /** @deprecated Legacy stake requirement. Boost PoMI gates on boost credits, not stake. */
66
72
  effectiveStakeRequirement: number;
67
73
  }
68
74
 
69
75
  export interface StakeInfo {
70
- /** Current staked amount (in NARA) */
76
+ /** Legacy staked amount (in NARA). Staking channel is closed; use `unstake` to withdraw. */
71
77
  amount: number;
72
- /** Round when the stake was made */
78
+ /** Round when the stake was last touched */
73
79
  stakeRound: number;
74
- /** Free stake credits (admin-assigned, bypass stake requirement) */
75
- 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;
76
84
  }
77
85
 
78
86
  export interface ZkProof {
@@ -101,7 +109,12 @@ export interface QuestOptions {
101
109
  circuitWasmPath?: string | Uint8Array;
102
110
  /** File path (Node.js), URL string, or pre-loaded Uint8Array (browser) */
103
111
  zkeyPath?: string | Uint8Array;
104
- /** "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
+ */
105
118
  stake?: "auto" | number;
106
119
  }
107
120
 
@@ -323,16 +336,19 @@ export async function getQuestInfo(
323
336
  stakeHigh, stakeLow, createdAtMs, decayMs, nowMs
324
337
  );
325
338
 
339
+ const stakeRewardCount = pool.stakeRewardCount;
340
+ const stakeWinnerCount = pool.stakeWinnerCount;
341
+
326
342
  return {
327
343
  active,
328
344
  round: pool.round.toString(),
329
345
  question: pool.question,
330
346
  answerHash: Array.from(pool.answerHash),
331
- rewardPerWinner: pool.rewardPerWinner.toNumber() / LAMPORTS_PER_SOL,
332
347
  totalReward: pool.rewardAmount.toNumber() / LAMPORTS_PER_SOL,
333
- rewardCount: pool.rewardCount,
334
- winnerCount: pool.winnerCount,
335
- remainingSlots: Math.max(0, pool.rewardCount - pool.winnerCount),
348
+ stakeRewardCount,
349
+ stakeWinnerCount,
350
+ stakeRewardPerWinner: pool.stakeRewardPerWinner.toNumber() / LAMPORTS_PER_SOL,
351
+ stakeRemainingSlots: Math.max(0, stakeRewardCount - stakeWinnerCount),
336
352
  difficulty: pool.difficulty,
337
353
  deadline,
338
354
  timeRemaining: secsLeft,
@@ -414,6 +430,12 @@ export async function generateProof(
414
430
 
415
431
  /**
416
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
+ *
417
439
  * If `activityLog` is provided, a logActivity instruction from the Agent Registry
418
440
  * is appended to the same transaction.
419
441
  */
@@ -428,37 +450,22 @@ export async function submitAnswer(
428
450
  ): Promise<SubmitAnswerResult> {
429
451
  const program = createProgram(connection, wallet, options?.programId);
430
452
 
431
- // 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)
432
462
  let stakeIx: any = null;
433
- if (options?.stake !== undefined) {
434
- let stakeLamports: BN;
435
- if (options.stake === "auto") {
436
- const quest = await getQuestInfo(connection, wallet, options);
437
- const stakeInfo = await getStakeInfo(connection, wallet.publicKey, options);
438
- const freeCredits = stakeInfo?.freeCredits ?? 0;
439
-
440
- if (freeCredits > 0) {
441
- // 本轮使用免质押额度,不需要补质押
442
- stakeLamports = new BN(0);
443
- } else {
444
- const required = quest.effectiveStakeRequirement;
445
- const current = stakeInfo?.amount ?? 0;
446
- const deficit = required - current;
447
- if (deficit > 0) {
448
- stakeLamports = new BN(Math.round(deficit * LAMPORTS_PER_SOL));
449
- } else {
450
- stakeLamports = new BN(0);
451
- }
452
- }
453
- } else {
454
- stakeLamports = new BN(Math.round(options.stake * LAMPORTS_PER_SOL));
455
- }
456
- if (!stakeLamports.isZero()) {
457
- stakeIx = await program.methods
458
- .stake(stakeLamports)
459
- .accounts({ user: wallet.publicKey } as any)
460
- .instruction();
461
- }
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();
462
469
  }
463
470
 
464
471
  const submitIx = await program.methods
@@ -565,12 +572,19 @@ export async function parseQuestReward(
565
572
  let winner = "";
566
573
  const logs: string[] = txInfo.meta?.logMessages ?? [];
567
574
  for (const log of logs) {
568
- 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+)/);
569
578
  if (m) {
570
579
  rewardLamports = parseInt(m[1]!);
571
580
  winner = m[2]!;
572
581
  break;
573
582
  }
583
+ const v = log.match(/vault insufficient \(winner (\d+\/\d+)/);
584
+ if (v) {
585
+ winner = v[1]!;
586
+ // rewardLamports stays 0
587
+ }
574
588
  }
575
589
 
576
590
  return {
@@ -677,7 +691,8 @@ export async function getStakeInfo(
677
691
  return {
678
692
  amount,
679
693
  stakeRound: record.stakeRound.toNumber(),
680
- freeCredits: record.freeCredits,
694
+ boostCredits: record.boostCredits,
695
+ userPubkey: record.userPubkey.toBase58(),
681
696
  };
682
697
  }
683
698
 
@@ -758,17 +773,22 @@ export async function initializeQuest(
758
773
 
759
774
  /**
760
775
  * Set the reward config (authority only).
776
+ *
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.
761
780
  */
762
781
  export async function setRewardConfig(
763
782
  connection: Connection,
764
783
  wallet: Keypair,
765
784
  minRewardCount: number,
766
785
  maxRewardCount: number,
786
+ freeStakeMultiplier: number = 1,
767
787
  options?: QuestOptions
768
788
  ): Promise<string> {
769
789
  const program = createProgram(connection, wallet, options?.programId);
770
790
  const ix = await program.methods
771
- .setRewardConfig(minRewardCount, maxRewardCount)
791
+ .setRewardConfig(minRewardCount, maxRewardCount, freeStakeMultiplier)
772
792
  .accounts({ authority: wallet.publicKey } as any)
773
793
  .instruction();
774
794
  return sendTx(connection, wallet, [ix]);
@@ -933,9 +953,10 @@ export async function setStakeAuthority(
933
953
  }
934
954
 
935
955
  /**
936
- * 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.)
937
958
  */
938
- export async function makeAdjustFreeStakeIx(
959
+ export async function makeAdjustBoostCreditsIx(
939
960
  connection: Connection,
940
961
  caller: PublicKey,
941
962
  user: PublicKey,
@@ -951,12 +972,12 @@ export async function makeAdjustFreeStakeIx(
951
972
  }
952
973
 
953
974
  /**
954
- * Adjust free stake credits for a user (stake_authority or authority only).
955
- * @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
956
977
  * @param delta - Amount to adjust (positive to add, negative to remove)
957
978
  * @param reason - Reason for the adjustment (logged on-chain)
958
979
  */
959
- export async function adjustFreeStake(
980
+ export async function adjustBoostCredits(
960
981
  connection: Connection,
961
982
  wallet: Keypair,
962
983
  user: PublicKey,
@@ -964,7 +985,7 @@ export async function adjustFreeStake(
964
985
  reason: string,
965
986
  options?: QuestOptions
966
987
  ): Promise<string> {
967
- const ix = await makeAdjustFreeStakeIx(connection, wallet.publicKey, user, delta, reason, options);
988
+ const ix = await makeAdjustBoostCreditsIx(connection, wallet.publicKey, user, delta, reason, options);
968
989
  return sendTx(connection, wallet, [ix]);
969
990
  }
970
991