nara-sdk 1.0.84 → 1.0.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +13 -4
- package/src/idls/nara_quest.ts +13 -4
- package/src/quest.ts +64 -65
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
|
@@ -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": [
|
|
@@ -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": [
|
|
@@ -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,42 @@ 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
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Boost PoMI winner bucket (single reward track).
|
|
52
|
+
* Named `stake*` for chain-field parity — the underlying pool fields are still
|
|
53
|
+
* `stake_reward_count` / `stake_reward_per_winner` / `stake_winner_count`.
|
|
54
|
+
*/
|
|
51
55
|
stakeRewardCount: number;
|
|
52
56
|
stakeWinnerCount: number;
|
|
53
57
|
stakeRewardPerWinner: number;
|
|
54
58
|
stakeRemainingSlots: number;
|
|
55
|
-
/** Credit (free-stake) reward bucket — consumed by stakeInfo.freeCredits */
|
|
56
|
-
creditRewardCount: number;
|
|
57
|
-
creditWinnerCount: number;
|
|
58
|
-
creditRewardPerWinner: number;
|
|
59
|
-
creditRemainingSlots: number;
|
|
60
59
|
difficulty: number;
|
|
61
60
|
deadline: number;
|
|
62
61
|
timeRemaining: number;
|
|
63
62
|
expired: boolean;
|
|
64
|
-
/**
|
|
63
|
+
/** @deprecated Legacy stake decay field. Staking channel is closed; value is informational only. */
|
|
65
64
|
stakeHigh: number;
|
|
66
|
-
/**
|
|
65
|
+
/** @deprecated Legacy stake decay field. Staking channel is closed; value is informational only. */
|
|
67
66
|
stakeLow: number;
|
|
68
|
-
/**
|
|
67
|
+
/** @deprecated Legacy field. Pool no longer accumulates avg participant stake. */
|
|
69
68
|
avgParticipantStake: number;
|
|
70
69
|
/** Unix timestamp when the current question was created */
|
|
71
70
|
createdAt: number;
|
|
72
|
-
/**
|
|
71
|
+
/** @deprecated Legacy stake requirement. Boost PoMI gates on boost credits, not stake. */
|
|
73
72
|
effectiveStakeRequirement: number;
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
export interface StakeInfo {
|
|
77
|
-
/**
|
|
76
|
+
/** Legacy staked amount (in NARA). Staking channel is closed; use `unstake` to withdraw. */
|
|
78
77
|
amount: number;
|
|
79
|
-
/** Round when the stake was
|
|
78
|
+
/** Round when the stake was last touched */
|
|
80
79
|
stakeRound: number;
|
|
81
|
-
/**
|
|
82
|
-
|
|
80
|
+
/** Boost PoMI credits — required to submit an answer (consumed -1 per successful reward) */
|
|
81
|
+
boostCredits: number;
|
|
82
|
+
/** On-chain record of the user's pubkey (populated on stake / adjust / answer) */
|
|
83
|
+
userPubkey: string;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
export interface ZkProof {
|
|
@@ -108,7 +109,12 @@ export interface QuestOptions {
|
|
|
108
109
|
circuitWasmPath?: string | Uint8Array;
|
|
109
110
|
/** File path (Node.js), URL string, or pre-loaded Uint8Array (browser) */
|
|
110
111
|
zkeyPath?: string | Uint8Array;
|
|
111
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* @deprecated Boost PoMI no longer gates on stake; credits are the sole admission ticket.
|
|
114
|
+
* If set, a legacy `stake` instruction is still bundled before `submit_answer`:
|
|
115
|
+
* - number: stake the exact NARA amount
|
|
116
|
+
* - "auto": no-op (kept for API compatibility)
|
|
117
|
+
*/
|
|
112
118
|
stake?: "auto" | number;
|
|
113
119
|
}
|
|
114
120
|
|
|
@@ -332,8 +338,6 @@ export async function getQuestInfo(
|
|
|
332
338
|
|
|
333
339
|
const stakeRewardCount = pool.stakeRewardCount;
|
|
334
340
|
const stakeWinnerCount = pool.stakeWinnerCount;
|
|
335
|
-
const creditRewardCount = pool.freeRewardCount;
|
|
336
|
-
const creditWinnerCount = pool.freeWinnerCount;
|
|
337
341
|
|
|
338
342
|
return {
|
|
339
343
|
active,
|
|
@@ -345,10 +349,6 @@ export async function getQuestInfo(
|
|
|
345
349
|
stakeWinnerCount,
|
|
346
350
|
stakeRewardPerWinner: pool.stakeRewardPerWinner.toNumber() / LAMPORTS_PER_SOL,
|
|
347
351
|
stakeRemainingSlots: Math.max(0, stakeRewardCount - stakeWinnerCount),
|
|
348
|
-
creditRewardCount,
|
|
349
|
-
creditWinnerCount,
|
|
350
|
-
creditRewardPerWinner: pool.freeRewardPerWinner.toNumber() / LAMPORTS_PER_SOL,
|
|
351
|
-
creditRemainingSlots: Math.max(0, creditRewardCount - creditWinnerCount),
|
|
352
352
|
difficulty: pool.difficulty,
|
|
353
353
|
deadline,
|
|
354
354
|
timeRemaining: secsLeft,
|
|
@@ -430,6 +430,12 @@ export async function generateProof(
|
|
|
430
430
|
|
|
431
431
|
/**
|
|
432
432
|
* Submit a quest answer on-chain (direct submission, requires gas).
|
|
433
|
+
*
|
|
434
|
+
* Boost PoMI: the caller must have at least 1 boost credit; on a successful
|
|
435
|
+
* reward, 1 credit is consumed. Without credits the on-chain program rejects
|
|
436
|
+
* with `NoCredits` (6024); this function pre-checks and throws early with a
|
|
437
|
+
* clearer message.
|
|
438
|
+
*
|
|
433
439
|
* If `activityLog` is provided, a logActivity instruction from the Agent Registry
|
|
434
440
|
* is appended to the same transaction.
|
|
435
441
|
*/
|
|
@@ -444,37 +450,22 @@ export async function submitAnswer(
|
|
|
444
450
|
): Promise<SubmitAnswerResult> {
|
|
445
451
|
const program = createProgram(connection, wallet, options?.programId);
|
|
446
452
|
|
|
447
|
-
//
|
|
453
|
+
// Boost PoMI: fail fast if the user has no credits
|
|
454
|
+
const stakeInfo = await getStakeInfo(connection, wallet.publicKey, options);
|
|
455
|
+
if (!stakeInfo || stakeInfo.boostCredits <= 0) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
"Boost PoMI requires boost credits. Your balance is 0 — acquire credits before submitting an answer."
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Legacy optional stake instruction (staking channel is closed but still callable)
|
|
448
462
|
let stakeIx: any = null;
|
|
449
|
-
if (options?.stake
|
|
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
|
-
}
|
|
463
|
+
if (typeof options?.stake === "number" && options.stake > 0) {
|
|
464
|
+
const stakeLamports = new BN(Math.round(options.stake * LAMPORTS_PER_SOL));
|
|
465
|
+
stakeIx = await program.methods
|
|
466
|
+
.stake(stakeLamports)
|
|
467
|
+
.accounts({ user: wallet.publicKey } as any)
|
|
468
|
+
.instruction();
|
|
478
469
|
}
|
|
479
470
|
|
|
480
471
|
const submitIx = await program.methods
|
|
@@ -581,12 +572,19 @@ export async function parseQuestReward(
|
|
|
581
572
|
let winner = "";
|
|
582
573
|
const logs: string[] = txInfo.meta?.logMessages ?? [];
|
|
583
574
|
for (const log of logs) {
|
|
584
|
-
|
|
575
|
+
// Boost PoMI success: "Boost PoMI reward N lamports (winner W/T, credits remaining: R)"
|
|
576
|
+
// Vault-insufficient notice: "Answer verified but vault insufficient (winner W/T, credit preserved)"
|
|
577
|
+
const m = log.match(/reward (\d+) lamports \(winner (\d+\/\d+)/);
|
|
585
578
|
if (m) {
|
|
586
579
|
rewardLamports = parseInt(m[1]!);
|
|
587
580
|
winner = m[2]!;
|
|
588
581
|
break;
|
|
589
582
|
}
|
|
583
|
+
const v = log.match(/vault insufficient \(winner (\d+\/\d+)/);
|
|
584
|
+
if (v) {
|
|
585
|
+
winner = v[1]!;
|
|
586
|
+
// rewardLamports stays 0
|
|
587
|
+
}
|
|
590
588
|
}
|
|
591
589
|
|
|
592
590
|
return {
|
|
@@ -693,7 +691,8 @@ export async function getStakeInfo(
|
|
|
693
691
|
return {
|
|
694
692
|
amount,
|
|
695
693
|
stakeRound: record.stakeRound.toNumber(),
|
|
696
|
-
|
|
694
|
+
boostCredits: record.boostCredits,
|
|
695
|
+
userPubkey: record.userPubkey.toBase58(),
|
|
697
696
|
};
|
|
698
697
|
}
|
|
699
698
|
|
|
@@ -775,15 +774,16 @@ export async function initializeQuest(
|
|
|
775
774
|
/**
|
|
776
775
|
* Set the reward config (authority only).
|
|
777
776
|
*
|
|
778
|
-
* @param freeStakeMultiplier -
|
|
779
|
-
*
|
|
777
|
+
* @param freeStakeMultiplier - Legacy field (>= 1). Currently unused by Boost PoMI
|
|
778
|
+
* reward calculation; defaults to 1. Kept exposed in
|
|
779
|
+
* case chain re-enables it.
|
|
780
780
|
*/
|
|
781
781
|
export async function setRewardConfig(
|
|
782
782
|
connection: Connection,
|
|
783
783
|
wallet: Keypair,
|
|
784
784
|
minRewardCount: number,
|
|
785
785
|
maxRewardCount: number,
|
|
786
|
-
freeStakeMultiplier: number,
|
|
786
|
+
freeStakeMultiplier: number = 1,
|
|
787
787
|
options?: QuestOptions
|
|
788
788
|
): Promise<string> {
|
|
789
789
|
const program = createProgram(connection, wallet, options?.programId);
|
|
@@ -908,7 +908,6 @@ export async function getQuestConfig(
|
|
|
908
908
|
stakeAuthority: PublicKey;
|
|
909
909
|
airdropAmount: number;
|
|
910
910
|
maxAirdropCount: number;
|
|
911
|
-
freeStakeMultiplier: number;
|
|
912
911
|
}> {
|
|
913
912
|
const kp = Keypair.generate();
|
|
914
913
|
const program = createProgram(connection, kp, options?.programId);
|
|
@@ -933,7 +932,6 @@ export async function getQuestConfig(
|
|
|
933
932
|
stakeAuthority: config.stakeAuthority,
|
|
934
933
|
airdropAmount: Number(config.airdropAmount.toString()),
|
|
935
934
|
maxAirdropCount: config.maxAirdropCount,
|
|
936
|
-
freeStakeMultiplier: config.freeStakeMultiplier,
|
|
937
935
|
};
|
|
938
936
|
}
|
|
939
937
|
|
|
@@ -955,9 +953,10 @@ export async function setStakeAuthority(
|
|
|
955
953
|
}
|
|
956
954
|
|
|
957
955
|
/**
|
|
958
|
-
* Build an
|
|
956
|
+
* Build an adjustBoostCredits instruction without sending it.
|
|
957
|
+
* (Wraps the on-chain `adjustFreeStake` instruction — legacy chain-side name.)
|
|
959
958
|
*/
|
|
960
|
-
export async function
|
|
959
|
+
export async function makeAdjustBoostCreditsIx(
|
|
961
960
|
connection: Connection,
|
|
962
961
|
caller: PublicKey,
|
|
963
962
|
user: PublicKey,
|
|
@@ -973,12 +972,12 @@ export async function makeAdjustFreeStakeIx(
|
|
|
973
972
|
}
|
|
974
973
|
|
|
975
974
|
/**
|
|
976
|
-
* Adjust
|
|
977
|
-
* @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
|
|
978
977
|
* @param delta - Amount to adjust (positive to add, negative to remove)
|
|
979
978
|
* @param reason - Reason for the adjustment (logged on-chain)
|
|
980
979
|
*/
|
|
981
|
-
export async function
|
|
980
|
+
export async function adjustBoostCredits(
|
|
982
981
|
connection: Connection,
|
|
983
982
|
wallet: Keypair,
|
|
984
983
|
user: PublicKey,
|
|
@@ -986,7 +985,7 @@ export async function adjustFreeStake(
|
|
|
986
985
|
reason: string,
|
|
987
986
|
options?: QuestOptions
|
|
988
987
|
): Promise<string> {
|
|
989
|
-
const ix = await
|
|
988
|
+
const ix = await makeAdjustBoostCreditsIx(connection, wallet.publicKey, user, delta, reason, options);
|
|
990
989
|
return sendTx(connection, wallet, [ix]);
|
|
991
990
|
}
|
|
992
991
|
|