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 +60 -19
- package/index.ts +3 -2
- package/package.json +1 -1
- package/src/agent_registry.ts +62 -24
- package/src/idls/nara_quest.json +43 -9
- package/src/idls/nara_quest.ts +43 -9
- package/src/quest.ts +77 -56
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
|
|
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%
|
|
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('
|
|
121
|
-
symbol: '
|
|
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:
|
|
128
|
+
nara: { warpProgram, mode: 'synthetic', mint, tokenProgram },
|
|
125
129
|
});
|
|
126
130
|
```
|
|
127
131
|
|
|
128
132
|
### Fee configuration
|
|
129
133
|
|
|
130
|
-
|
|
131
|
-
|
|
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,
|
|
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 {
|
|
194
|
+
import {
|
|
195
|
+
getQuestInfo,
|
|
196
|
+
getStakeInfo,
|
|
197
|
+
generateProof,
|
|
198
|
+
submitAnswer,
|
|
199
|
+
} from 'nara-sdk';
|
|
180
200
|
|
|
181
201
|
const quest = await getQuestInfo(connection);
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
package/src/agent_registry.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1524
|
-
|
|
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 (
|
|
1550
|
-
const {
|
|
1551
|
-
const
|
|
1552
|
-
connection, wallet.publicKey, authority,
|
|
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(
|
|
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
|
|
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
|
-
|
|
1589
|
-
|
|
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 (
|
|
1614
|
-
const {
|
|
1615
|
-
const
|
|
1616
|
-
connection, wallet.publicKey, authority,
|
|
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(
|
|
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
|
|
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
|
-
|
|
1636
|
-
|
|
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 (
|
|
1662
|
-
const {
|
|
1663
|
-
const
|
|
1664
|
-
connection, wallet.publicKey, authority,
|
|
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(
|
|
1704
|
+
ixs.push(boostCreditsIx);
|
|
1667
1705
|
}
|
|
1668
1706
|
|
|
1669
1707
|
return sendTx(connection, wallet, ixs);
|
package/src/idls/nara_quest.json
CHANGED
|
@@ -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": "
|
|
1887
|
-
"msg": "
|
|
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
|
-
|
|
2032
|
+
16
|
|
2015
2033
|
]
|
|
2016
2034
|
}
|
|
2017
2035
|
}
|
|
@@ -2049,15 +2067,15 @@
|
|
|
2049
2067
|
"type": "u64"
|
|
2050
2068
|
},
|
|
2051
2069
|
{
|
|
2052
|
-
"name": "
|
|
2070
|
+
"name": "stake_reward_count",
|
|
2053
2071
|
"type": "u32"
|
|
2054
2072
|
},
|
|
2055
2073
|
{
|
|
2056
|
-
"name": "
|
|
2074
|
+
"name": "stake_reward_per_winner",
|
|
2057
2075
|
"type": "u64"
|
|
2058
2076
|
},
|
|
2059
2077
|
{
|
|
2060
|
-
"name": "
|
|
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
|
-
|
|
2118
|
+
48
|
|
2089
2119
|
]
|
|
2090
2120
|
}
|
|
2091
2121
|
}
|
|
@@ -2102,15 +2132,19 @@
|
|
|
2102
2132
|
"type": "u64"
|
|
2103
2133
|
},
|
|
2104
2134
|
{
|
|
2105
|
-
"name": "
|
|
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
|
-
|
|
2147
|
+
28
|
|
2114
2148
|
]
|
|
2115
2149
|
}
|
|
2116
2150
|
}
|
package/src/idls/nara_quest.ts
CHANGED
|
@@ -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": "
|
|
1893
|
-
"msg": "
|
|
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
|
-
|
|
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": "
|
|
2076
|
+
"name": "stakeRewardCount",
|
|
2059
2077
|
"type": "u32"
|
|
2060
2078
|
},
|
|
2061
2079
|
{
|
|
2062
|
-
"name": "
|
|
2080
|
+
"name": "stakeRewardPerWinner",
|
|
2063
2081
|
"type": "u64"
|
|
2064
2082
|
},
|
|
2065
2083
|
{
|
|
2066
|
-
"name": "
|
|
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
|
-
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
/** Total reward pool for the round, in NARA */
|
|
49
49
|
totalReward: number;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
/**
|
|
63
|
+
/** @deprecated Legacy stake decay field. Staking channel is closed; value is informational only. */
|
|
58
64
|
stakeHigh: number;
|
|
59
|
-
/**
|
|
65
|
+
/** @deprecated Legacy stake decay field. Staking channel is closed; value is informational only. */
|
|
60
66
|
stakeLow: number;
|
|
61
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
76
|
+
/** Legacy staked amount (in NARA). Staking channel is closed; use `unstake` to withdraw. */
|
|
71
77
|
amount: number;
|
|
72
|
-
/** Round when the stake was
|
|
78
|
+
/** Round when the stake was last touched */
|
|
73
79
|
stakeRound: number;
|
|
74
|
-
/**
|
|
75
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
//
|
|
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
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
955
|
-
* @param user - The user whose
|
|
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
|
|
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
|
|
988
|
+
const ix = await makeAdjustBoostCreditsIx(connection, wallet.publicKey, user, delta, reason, options);
|
|
968
989
|
return sendTx(connection, wallet, [ix]);
|
|
969
990
|
}
|
|
970
991
|
|