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 +59 -19
- package/index.ts +3 -2
- package/package.json +1 -1
- package/src/agent_registry.ts +62 -24
- package/src/idls/nara_quest.json +16 -7
- package/src/idls/nara_quest.ts +16 -7
- package/src/quest.ts +70 -75
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,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,
|
|
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.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
|
-
|
|
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
|
@@ -1887,8 +1887,8 @@
|
|
|
1887
1887
|
},
|
|
1888
1888
|
{
|
|
1889
1889
|
"code": 6017,
|
|
1890
|
-
"name": "
|
|
1891
|
-
"msg": "
|
|
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": "
|
|
2070
|
+
"name": "boost_reward_count",
|
|
2066
2071
|
"type": "u32"
|
|
2067
2072
|
},
|
|
2068
2073
|
{
|
|
2069
|
-
"name": "
|
|
2074
|
+
"name": "boost_reward_per_winner",
|
|
2070
2075
|
"type": "u64"
|
|
2071
2076
|
},
|
|
2072
2077
|
{
|
|
2073
|
-
"name": "
|
|
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": "
|
|
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
|
-
|
|
2147
|
+
28
|
|
2139
2148
|
]
|
|
2140
2149
|
}
|
|
2141
2150
|
}
|
package/src/idls/nara_quest.ts
CHANGED
|
@@ -1893,8 +1893,8 @@ export type NaraQuest = {
|
|
|
1893
1893
|
},
|
|
1894
1894
|
{
|
|
1895
1895
|
"code": 6017,
|
|
1896
|
-
"name": "
|
|
1897
|
-
"msg": "
|
|
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": "
|
|
2076
|
+
"name": "boostRewardCount",
|
|
2072
2077
|
"type": "u32"
|
|
2073
2078
|
},
|
|
2074
2079
|
{
|
|
2075
|
-
"name": "
|
|
2080
|
+
"name": "boostRewardPerWinner",
|
|
2076
2081
|
"type": "u64"
|
|
2077
2082
|
},
|
|
2078
2083
|
{
|
|
2079
|
-
"name": "
|
|
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": "
|
|
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
|
-
|
|
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
|
|
48
|
+
/** Total reward pool for the round, in NARA */
|
|
49
49
|
totalReward: number;
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
/**
|
|
59
|
+
/** @deprecated Legacy stake decay field. Staking channel is closed; value is informational only. */
|
|
65
60
|
stakeHigh: number;
|
|
66
|
-
/**
|
|
61
|
+
/** @deprecated Legacy stake decay field. Staking channel is closed; value is informational only. */
|
|
67
62
|
stakeLow: number;
|
|
68
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
72
|
+
/** Legacy staked amount (in NARA). Staking channel is closed; use `unstake` to withdraw. */
|
|
78
73
|
amount: number;
|
|
79
|
-
/** Round when the stake was
|
|
74
|
+
/** Round when the stake was last touched */
|
|
80
75
|
stakeRound: number;
|
|
81
|
-
/**
|
|
82
|
-
|
|
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
|
-
/**
|
|
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
|
|
334
|
-
const
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
//
|
|
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
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 -
|
|
779
|
-
*
|
|
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
|
|
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
|
|
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
|
|
977
|
-
* @param user - The user whose
|
|
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
|
|
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
|
|
984
|
+
const ix = await makeAdjustBoostCreditsIx(connection, wallet.publicKey, user, delta, reason, options);
|
|
990
985
|
return sendTx(connection, wallet, [ix]);
|
|
991
986
|
}
|
|
992
987
|
|