nara-sdk 1.0.75 → 1.0.77
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/index.ts +14 -0
- package/package.json +4 -1
- package/src/agent_registry.ts +10 -20
- package/src/bridge.ts +40 -10
- package/src/quest.ts +49 -23
- package/src/skills.ts +19 -25
- package/src/tx_parser.ts +750 -0
- package/src/zkid.ts +28 -9
package/index.ts
CHANGED
|
@@ -176,6 +176,11 @@ export {
|
|
|
176
176
|
SOLANA_MAILBOX,
|
|
177
177
|
NARA_MAILBOX,
|
|
178
178
|
SPL_NOOP,
|
|
179
|
+
SOLANA_USDC_MINT,
|
|
180
|
+
NARA_USDC_MINT,
|
|
181
|
+
SOLANA_USDT_MINT,
|
|
182
|
+
NARA_USDT_MINT,
|
|
183
|
+
NARA_SOL_MINT,
|
|
179
184
|
BRIDGE_TOKENS,
|
|
180
185
|
// Token registry
|
|
181
186
|
registerBridgeToken,
|
|
@@ -214,6 +219,15 @@ export {
|
|
|
214
219
|
type MessageSignatureStatus,
|
|
215
220
|
} from "./src/bridge";
|
|
216
221
|
|
|
222
|
+
// Export transaction parser
|
|
223
|
+
export {
|
|
224
|
+
parseTxFromHash,
|
|
225
|
+
parseTxResponse,
|
|
226
|
+
formatParsedTx,
|
|
227
|
+
type ParsedInstruction,
|
|
228
|
+
type ParsedTransaction,
|
|
229
|
+
} from "./src/tx_parser";
|
|
230
|
+
|
|
217
231
|
// Export IDLs and types
|
|
218
232
|
export { default as NaraQuestIDL } from "./src/idls/nara_quest.json";
|
|
219
233
|
export { default as NaraSkillsHubIDL } from "./src/idls/nara_skills_hub.json";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nara-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.77",
|
|
4
4
|
"description": "SDK for the Nara chain (Solana-compatible)",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"blockchain",
|
|
18
18
|
"sdk"
|
|
19
19
|
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "tsx test/read_api.test.ts"
|
|
22
|
+
},
|
|
20
23
|
"author": "",
|
|
21
24
|
"license": "MIT",
|
|
22
25
|
"devDependencies": {
|
package/src/agent_registry.ts
CHANGED
|
@@ -352,31 +352,21 @@ export async function getAgentInfo(
|
|
|
352
352
|
): Promise<AgentInfo> {
|
|
353
353
|
const pid = new PublicKey(options?.programId ?? DEFAULT_AGENT_REGISTRY_PROGRAM_ID);
|
|
354
354
|
const agentPda = getAgentPda(pid, agentId);
|
|
355
|
-
const accountInfo = await connection.getAccountInfo(agentPda);
|
|
356
|
-
if (!accountInfo) {
|
|
357
|
-
throw new Error(`Agent "${agentId}" not found`);
|
|
358
|
-
}
|
|
359
|
-
const record = parseAgentRecordData(accountInfo.data);
|
|
360
|
-
|
|
361
355
|
const bioPda = getBioPda(pid, agentPda);
|
|
362
356
|
const metaPda = getMetaPda(pid, agentPda);
|
|
363
357
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// account not created yet
|
|
358
|
+
// Single RPC: fetch agent record, bio, and metadata in one call
|
|
359
|
+
const [agentInfo, bioInfo, metaInfo] = await connection.getMultipleAccountsInfo(
|
|
360
|
+
[agentPda, bioPda, metaPda],
|
|
361
|
+
"confirmed"
|
|
362
|
+
);
|
|
363
|
+
if (!agentInfo) {
|
|
364
|
+
throw new Error(`Agent "${agentId}" not found`);
|
|
372
365
|
}
|
|
373
366
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
} catch {
|
|
378
|
-
// account not created yet
|
|
379
|
-
}
|
|
367
|
+
const record = parseAgentRecordData(agentInfo.data);
|
|
368
|
+
const bio = bioInfo ? deserializeRawString(Buffer.from(bioInfo.data)) : null;
|
|
369
|
+
const metadata = metaInfo ? deserializeRawString(Buffer.from(metaInfo.data)) : null;
|
|
380
370
|
|
|
381
371
|
return { record, bio, metadata };
|
|
382
372
|
}
|
package/src/bridge.ts
CHANGED
|
@@ -105,6 +105,14 @@ export const SPL_NOOP = new PublicKey(
|
|
|
105
105
|
"noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"
|
|
106
106
|
);
|
|
107
107
|
|
|
108
|
+
// ─── Token mint constants ─────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export const SOLANA_USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
|
|
111
|
+
export const NARA_USDC_MINT = new PublicKey("8P7UGWjq86N3WUmwEgKeGHJZLcoMJqr5jnRUmeBN7YwR");
|
|
112
|
+
export const SOLANA_USDT_MINT = new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB");
|
|
113
|
+
export const NARA_USDT_MINT = new PublicKey("8yQSyqC85A9Vcqz8gTU2Bk5Y63bnC5378sgx1biTKsjd");
|
|
114
|
+
export const NARA_SOL_MINT = new PublicKey("7fKh7DqPZmsYPHdGvt9Qw2rZkSEGp9F5dBa3XuuuhavU");
|
|
115
|
+
|
|
108
116
|
function mailboxFor(chain: BridgeChain): PublicKey {
|
|
109
117
|
return chain === "solana" ? SOLANA_MAILBOX : NARA_MAILBOX;
|
|
110
118
|
}
|
|
@@ -122,13 +130,29 @@ export const BRIDGE_TOKENS: Record<string, BridgeTokenConfig> = {
|
|
|
122
130
|
solana: {
|
|
123
131
|
warpProgram: new PublicKey("4GcZJTa8s9vxtTz97Vj1RrwKMqPkT3DiiJkvUQDwsuZP"),
|
|
124
132
|
mode: "collateral",
|
|
125
|
-
mint:
|
|
133
|
+
mint: SOLANA_USDC_MINT,
|
|
126
134
|
tokenProgram: TOKEN_PROGRAM_ID,
|
|
127
135
|
},
|
|
128
136
|
nara: {
|
|
129
137
|
warpProgram: new PublicKey("BC2j6WrdPs9xhU9CfBwJsYSnJrGq5Tcm4SEen9ENv7go"),
|
|
130
138
|
mode: "synthetic",
|
|
131
|
-
mint:
|
|
139
|
+
mint: NARA_USDC_MINT,
|
|
140
|
+
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
USDT: {
|
|
144
|
+
symbol: "USDT",
|
|
145
|
+
decimals: 6,
|
|
146
|
+
solana: {
|
|
147
|
+
warpProgram: new PublicKey("DCTt9H3pwwU89qC3Z4voYNThZypV68AwhYNzMNBxWXoy"),
|
|
148
|
+
mode: "collateral",
|
|
149
|
+
mint: SOLANA_USDT_MINT,
|
|
150
|
+
tokenProgram: TOKEN_PROGRAM_ID,
|
|
151
|
+
},
|
|
152
|
+
nara: {
|
|
153
|
+
warpProgram: new PublicKey("2q5HJaaagMxBM7GD5yR55xHN4tDZMh1gYraG1Y4wbry6"),
|
|
154
|
+
mode: "synthetic",
|
|
155
|
+
mint: NARA_USDT_MINT,
|
|
132
156
|
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
133
157
|
},
|
|
134
158
|
},
|
|
@@ -144,7 +168,7 @@ export const BRIDGE_TOKENS: Record<string, BridgeTokenConfig> = {
|
|
|
144
168
|
nara: {
|
|
145
169
|
warpProgram: new PublicKey("6bKmjEMbjcJUnqAiNw7AXuMvUALzw5XRKiV9dBsterxg"),
|
|
146
170
|
mode: "synthetic",
|
|
147
|
-
mint:
|
|
171
|
+
mint: NARA_SOL_MINT,
|
|
148
172
|
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
149
173
|
},
|
|
150
174
|
},
|
|
@@ -616,17 +640,23 @@ export async function queryMessageStatus(
|
|
|
616
640
|
const sigs = await destConnection.getSignaturesForAddress(mailbox, {
|
|
617
641
|
limit,
|
|
618
642
|
});
|
|
643
|
+
const validSigs = sigs.filter((s) => !s.err).map((s) => s.signature);
|
|
644
|
+
if (!validSigs.length) {
|
|
645
|
+
return { delivered: false, deliverySignature: null };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Batch-fetch transactions in a single JSON-RPC call
|
|
649
|
+
const txs = await destConnection.getTransactions(validSigs, {
|
|
650
|
+
maxSupportedTransactionVersion: 0,
|
|
651
|
+
commitment: "confirmed",
|
|
652
|
+
});
|
|
619
653
|
|
|
620
|
-
for (
|
|
621
|
-
|
|
622
|
-
const tx = await destConnection.getTransaction(entry.signature, {
|
|
623
|
-
maxSupportedTransactionVersion: 0,
|
|
624
|
-
commitment: "confirmed",
|
|
625
|
-
});
|
|
654
|
+
for (let i = 0; i < txs.length; i++) {
|
|
655
|
+
const tx = txs[i];
|
|
626
656
|
if (!tx?.meta?.logMessages) continue;
|
|
627
657
|
for (const log of tx.meta.logMessages) {
|
|
628
658
|
if (log.includes(messageId)) {
|
|
629
|
-
return { delivered: true, deliverySignature:
|
|
659
|
+
return { delivered: true, deliverySignature: validSigs[i] ?? null };
|
|
630
660
|
}
|
|
631
661
|
}
|
|
632
662
|
}
|
package/src/quest.ts
CHANGED
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
LAMPORTS_PER_SOL,
|
|
9
9
|
PublicKey,
|
|
10
10
|
} from "@solana/web3.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getAssociatedTokenAddressSync,
|
|
13
|
+
unpackAccount,
|
|
14
|
+
TOKEN_PROGRAM_ID,
|
|
15
|
+
} from "@solana/spl-token";
|
|
12
16
|
import * as anchor from "@coral-xyz/anchor";
|
|
13
17
|
import { Program, AnchorProvider, Wallet } from "@coral-xyz/anchor";
|
|
14
18
|
import BN from "bn.js";
|
|
@@ -283,7 +287,22 @@ export async function getQuestInfo(
|
|
|
283
287
|
const kp = wallet ?? Keypair.generate();
|
|
284
288
|
const program = createProgram(connection, kp, options?.programId);
|
|
285
289
|
const poolPda = getPoolPda(program.programId);
|
|
286
|
-
const
|
|
290
|
+
const programId = new PublicKey(options?.programId ?? DEFAULT_QUEST_PROGRAM_ID);
|
|
291
|
+
const [configPda] = PublicKey.findProgramAddressSync(
|
|
292
|
+
[new TextEncoder().encode("quest_config")],
|
|
293
|
+
programId
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Single RPC: fetch pool + gameConfig together
|
|
297
|
+
const [poolAcct, configAcct] = await connection.getMultipleAccountsInfo(
|
|
298
|
+
[poolPda, configPda],
|
|
299
|
+
"confirmed"
|
|
300
|
+
);
|
|
301
|
+
if (!poolAcct) throw new Error(`Pool account not found: ${poolPda.toBase58()}`);
|
|
302
|
+
if (!configAcct) throw new Error(`GameConfig account not found: ${configPda.toBase58()}`);
|
|
303
|
+
|
|
304
|
+
const pool = program.coder.accounts.decode("pool", poolAcct.data);
|
|
305
|
+
const config = program.coder.accounts.decode("gameConfig", configAcct.data);
|
|
287
306
|
|
|
288
307
|
const now = Math.floor(Date.now() / 1000);
|
|
289
308
|
const deadline = pool.deadline.toNumber();
|
|
@@ -295,13 +314,6 @@ export async function getQuestInfo(
|
|
|
295
314
|
const stakeLow = Number(pool.stakeLow.toString()) / LAMPORTS_PER_SOL;
|
|
296
315
|
const createdAt = pool.createdAt.toNumber();
|
|
297
316
|
|
|
298
|
-
// Fetch decayMs from GameConfig for effective calculation
|
|
299
|
-
const programId = new PublicKey(options?.programId ?? DEFAULT_QUEST_PROGRAM_ID);
|
|
300
|
-
const [configPda] = PublicKey.findProgramAddressSync(
|
|
301
|
-
[new TextEncoder().encode("quest_config")],
|
|
302
|
-
programId
|
|
303
|
-
);
|
|
304
|
-
const config = await program.account.gameConfig.fetch(configPda);
|
|
305
317
|
const decayMs = Number(config.decayMs.toString());
|
|
306
318
|
|
|
307
319
|
// createdAt is unix timestamp (seconds), convert to ms for decay calculation
|
|
@@ -627,25 +639,39 @@ export async function getStakeInfo(
|
|
|
627
639
|
const kp = Keypair.generate();
|
|
628
640
|
const program = createProgram(connection, kp, options?.programId);
|
|
629
641
|
const stakeRecordPda = getStakeRecordPda(program.programId, user);
|
|
642
|
+
const stakeTokenAccount = getStakeTokenAccount(stakeRecordPda);
|
|
643
|
+
|
|
644
|
+
// Single batch: fetch stake record + wSOL token account at once.
|
|
645
|
+
// Both are plain account reads, so getMultipleAccountsInfo works.
|
|
646
|
+
const [recordAcct, tokenAcct] = await connection.getMultipleAccountsInfo(
|
|
647
|
+
[stakeRecordPda, stakeTokenAccount],
|
|
648
|
+
"confirmed"
|
|
649
|
+
);
|
|
650
|
+
if (!recordAcct) return null;
|
|
651
|
+
|
|
652
|
+
let record;
|
|
630
653
|
try {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
654
|
+
record = program.coder.accounts.decode("stakeRecord", recordAcct.data);
|
|
655
|
+
} catch {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Decode wSOL token account (legacy SPL Token program)
|
|
660
|
+
let amount = 0;
|
|
661
|
+
if (tokenAcct) {
|
|
635
662
|
try {
|
|
636
|
-
const
|
|
637
|
-
amount = Number(
|
|
663
|
+
const unpacked = unpackAccount(stakeTokenAccount, tokenAcct, TOKEN_PROGRAM_ID);
|
|
664
|
+
amount = Number(unpacked.amount) / LAMPORTS_PER_SOL;
|
|
638
665
|
} catch {
|
|
639
|
-
// Token account
|
|
666
|
+
// Token account not initialized yet
|
|
640
667
|
}
|
|
641
|
-
return {
|
|
642
|
-
amount,
|
|
643
|
-
stakeRound: record.stakeRound.toNumber(),
|
|
644
|
-
freeCredits: record.freeCredits,
|
|
645
|
-
};
|
|
646
|
-
} catch {
|
|
647
|
-
return null;
|
|
648
668
|
}
|
|
669
|
+
|
|
670
|
+
return {
|
|
671
|
+
amount,
|
|
672
|
+
stakeRound: record.stakeRound.toNumber(),
|
|
673
|
+
freeCredits: record.freeCredits,
|
|
674
|
+
};
|
|
649
675
|
}
|
|
650
676
|
|
|
651
677
|
/**
|
package/src/skills.ts
CHANGED
|
@@ -216,38 +216,32 @@ export async function getSkillInfo(
|
|
|
216
216
|
): Promise<SkillInfo> {
|
|
217
217
|
const pid = new PublicKey(options?.programId ?? DEFAULT_SKILLS_PROGRAM_ID);
|
|
218
218
|
const skillPda = getSkillPda(pid, name);
|
|
219
|
-
const
|
|
220
|
-
|
|
219
|
+
const descPda = getDescPda(pid, skillPda);
|
|
220
|
+
const metaPda = getMetaPda(pid, skillPda);
|
|
221
|
+
|
|
222
|
+
// Single RPC: fetch skill record, description, and metadata in one call
|
|
223
|
+
const [skillInfo, descInfo, metaInfo] = await connection.getMultipleAccountsInfo(
|
|
224
|
+
[skillPda, descPda, metaPda],
|
|
225
|
+
"confirmed"
|
|
226
|
+
);
|
|
227
|
+
if (!skillInfo) {
|
|
221
228
|
throw new Error(`Skill "${name}" not found`);
|
|
222
229
|
}
|
|
223
|
-
const record = parseSkillRecordData(accountInfo.data);
|
|
224
230
|
|
|
225
|
-
const
|
|
226
|
-
const metaPda = getMetaPda(pid, skillPda);
|
|
231
|
+
const record = parseSkillRecordData(skillInfo.data);
|
|
227
232
|
|
|
228
233
|
let description: string | null = null;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (descInfo) {
|
|
234
|
-
const buf = Buffer.from(descInfo.data);
|
|
235
|
-
const descLen = buf.readUInt16LE(8);
|
|
236
|
-
description = buf.subarray(10, 10 + descLen).toString("utf-8");
|
|
237
|
-
}
|
|
238
|
-
} catch {
|
|
239
|
-
// account not created yet
|
|
234
|
+
if (descInfo) {
|
|
235
|
+
const buf = Buffer.from(descInfo.data);
|
|
236
|
+
const descLen = buf.readUInt16LE(8);
|
|
237
|
+
description = buf.subarray(10, 10 + descLen).toString("utf-8");
|
|
240
238
|
}
|
|
241
239
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
metadata = buf.subarray(10, 10 + dataLen).toString("utf-8");
|
|
248
|
-
}
|
|
249
|
-
} catch {
|
|
250
|
-
// account not created yet
|
|
240
|
+
let metadata: string | null = null;
|
|
241
|
+
if (metaInfo) {
|
|
242
|
+
const buf = Buffer.from(metaInfo.data);
|
|
243
|
+
const dataLen = buf.readUInt16LE(8);
|
|
244
|
+
metadata = buf.subarray(10, 10 + dataLen).toString("utf-8");
|
|
251
245
|
}
|
|
252
246
|
|
|
253
247
|
return { record, description, metadata };
|
package/src/tx_parser.ts
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction parser — decodes on-chain transaction into human-readable instructions.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - ComputeBudget (CU limit / price)
|
|
6
|
+
* - SystemProgram (transfer, createAccount, ...)
|
|
7
|
+
* - SPL Token / Token-2022 (transfer, transferChecked, burn, mintTo, ...)
|
|
8
|
+
* - Associated Token Account (create)
|
|
9
|
+
* - Quest (PoMI) — answer, stake, airdrop, etc.
|
|
10
|
+
* - Agent Registry — register, twitter, tweet, etc.
|
|
11
|
+
* - Skills Hub — register, upload, etc.
|
|
12
|
+
* - ZK ID — deposit, withdraw, transfer, etc.
|
|
13
|
+
* - Bridge (Hyperlane warp routes) — TransferRemote
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
Connection,
|
|
18
|
+
PublicKey,
|
|
19
|
+
SystemInstruction,
|
|
20
|
+
SystemProgram,
|
|
21
|
+
TransactionInstruction,
|
|
22
|
+
ComputeBudgetProgram,
|
|
23
|
+
} from "@solana/web3.js";
|
|
24
|
+
import type {
|
|
25
|
+
MessageCompiledInstruction,
|
|
26
|
+
VersionedTransactionResponse,
|
|
27
|
+
} from "@solana/web3.js";
|
|
28
|
+
import {
|
|
29
|
+
decodeInstruction as decodeSplTokenInstruction,
|
|
30
|
+
TokenInstruction,
|
|
31
|
+
TOKEN_PROGRAM_ID,
|
|
32
|
+
TOKEN_2022_PROGRAM_ID,
|
|
33
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
34
|
+
} from "@solana/spl-token";
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_QUEST_PROGRAM_ID,
|
|
37
|
+
DEFAULT_AGENT_REGISTRY_PROGRAM_ID,
|
|
38
|
+
DEFAULT_SKILLS_PROGRAM_ID,
|
|
39
|
+
DEFAULT_ZKID_PROGRAM_ID,
|
|
40
|
+
} from "./constants";
|
|
41
|
+
import { BRIDGE_TOKENS } from "./bridge";
|
|
42
|
+
|
|
43
|
+
// ─── Types ────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export interface ParsedInstruction {
|
|
46
|
+
/** Instruction index in the transaction */
|
|
47
|
+
index: number;
|
|
48
|
+
/** Human-readable program name */
|
|
49
|
+
programName: string;
|
|
50
|
+
/** Program ID */
|
|
51
|
+
programId: string;
|
|
52
|
+
/** Human-readable instruction type / name */
|
|
53
|
+
type: string;
|
|
54
|
+
/** Decoded fields (key-value) */
|
|
55
|
+
info: Record<string, unknown>;
|
|
56
|
+
/** All account pubkeys involved */
|
|
57
|
+
accounts: string[];
|
|
58
|
+
/** Raw instruction data (base64) */
|
|
59
|
+
rawData: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ParsedTransaction {
|
|
63
|
+
/** Transaction signature */
|
|
64
|
+
signature: string;
|
|
65
|
+
/** Slot number */
|
|
66
|
+
slot: number;
|
|
67
|
+
/** Block time (unix) */
|
|
68
|
+
blockTime: number | null;
|
|
69
|
+
/** Whether the transaction succeeded */
|
|
70
|
+
success: boolean;
|
|
71
|
+
/** Error message if failed */
|
|
72
|
+
error: string | null;
|
|
73
|
+
/** Fee paid in lamports */
|
|
74
|
+
fee: number;
|
|
75
|
+
/** Parsed instructions */
|
|
76
|
+
instructions: ParsedInstruction[];
|
|
77
|
+
/** Log messages */
|
|
78
|
+
logs: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Program ID registry ──────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const SYSTEM_PROGRAM = SystemProgram.programId.toBase58();
|
|
84
|
+
const COMPUTE_BUDGET = ComputeBudgetProgram.programId.toBase58();
|
|
85
|
+
const TOKEN_PROGRAM = TOKEN_PROGRAM_ID.toBase58();
|
|
86
|
+
const TOKEN_2022 = TOKEN_2022_PROGRAM_ID.toBase58();
|
|
87
|
+
const ATA_PROGRAM = ASSOCIATED_TOKEN_PROGRAM_ID.toBase58();
|
|
88
|
+
const MEMO_PROGRAM = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
89
|
+
const MEMO_PROGRAM_V1 = "Memo1UhkJBfCR6MNBin8nfyBmg8aW7S8K2LoYKrntv";
|
|
90
|
+
|
|
91
|
+
function buildProgramNameMap(): Map<string, string> {
|
|
92
|
+
const m = new Map<string, string>();
|
|
93
|
+
|
|
94
|
+
// System programs
|
|
95
|
+
m.set(SYSTEM_PROGRAM, "System Program");
|
|
96
|
+
m.set(COMPUTE_BUDGET, "Compute Budget");
|
|
97
|
+
m.set(TOKEN_PROGRAM, "SPL Token");
|
|
98
|
+
m.set(TOKEN_2022, "SPL Token-2022");
|
|
99
|
+
m.set(ATA_PROGRAM, "Associated Token");
|
|
100
|
+
m.set(MEMO_PROGRAM, "Memo");
|
|
101
|
+
m.set(MEMO_PROGRAM_V1, "Memo (v1)");
|
|
102
|
+
|
|
103
|
+
// Nara programs
|
|
104
|
+
m.set(DEFAULT_QUEST_PROGRAM_ID, "Quest (PoMI)");
|
|
105
|
+
m.set(DEFAULT_AGENT_REGISTRY_PROGRAM_ID, "Agent Registry");
|
|
106
|
+
m.set(DEFAULT_SKILLS_PROGRAM_ID, "Skills Hub");
|
|
107
|
+
m.set(DEFAULT_ZKID_PROGRAM_ID, "ZK ID");
|
|
108
|
+
|
|
109
|
+
// Hyperlane mailboxes
|
|
110
|
+
m.set("E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi", "Hyperlane Mailbox (Solana)");
|
|
111
|
+
m.set("EjtLD3MCBJregFKAce2pQqPtSnnmBWK5oAZ3wBifHnaH", "Hyperlane Mailbox (Nara)");
|
|
112
|
+
m.set("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV", "SPL Noop");
|
|
113
|
+
|
|
114
|
+
// Bridge warp routes
|
|
115
|
+
for (const [symbol, cfg] of Object.entries(BRIDGE_TOKENS)) {
|
|
116
|
+
m.set(cfg.solana.warpProgram.toBase58(), `Bridge: ${symbol} (Solana)`);
|
|
117
|
+
m.set(cfg.nara.warpProgram.toBase58(), `Bridge: ${symbol} (Nara)`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return m;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const programNames = buildProgramNameMap();
|
|
124
|
+
|
|
125
|
+
function getProgramName(pid: string): string {
|
|
126
|
+
return programNames.get(pid) ?? "Unknown";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Anchor discriminator map ─────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
type DiscriminatorMap = Map<string, string>;
|
|
132
|
+
|
|
133
|
+
function discKey(bytes: number[]): string {
|
|
134
|
+
return bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildAnchorDiscriminators(): Map<string, DiscriminatorMap> {
|
|
138
|
+
const programs = new Map<string, DiscriminatorMap>();
|
|
139
|
+
|
|
140
|
+
// Quest
|
|
141
|
+
const quest: DiscriminatorMap = new Map();
|
|
142
|
+
quest.set(discKey([221, 73, 184, 157, 1, 150, 231, 48]), "submitAnswer");
|
|
143
|
+
quest.set(discKey([206, 176, 202, 18, 200, 209, 179, 108]), "stake");
|
|
144
|
+
quest.set(discKey([90, 95, 107, 42, 205, 124, 50, 225]), "unstake");
|
|
145
|
+
quest.set(discKey([222, 74, 49, 30, 160, 220, 179, 27]), "createQuestion");
|
|
146
|
+
quest.set(discKey([137, 50, 122, 111, 89, 254, 8, 20]), "claimAirdrop");
|
|
147
|
+
quest.set(discKey([21, 111, 164, 64, 220, 115, 26, 60]), "adjustFreeStake");
|
|
148
|
+
quest.set(discKey([255, 181, 252, 34, 155, 230, 65, 227]), "setAirdropConfig");
|
|
149
|
+
quest.set(discKey([175, 175, 109, 31, 13, 152, 155, 237]), "initialize");
|
|
150
|
+
quest.set(discKey([120, 201, 195, 128, 35, 202, 73, 161]), "expandConfig");
|
|
151
|
+
quest.set(discKey([19, 140, 189, 111, 121, 111, 118, 50]), "setQuestAuthority");
|
|
152
|
+
quest.set(discKey([69, 227, 209, 29, 164, 111, 166, 41]), "setQuestInterval");
|
|
153
|
+
quest.set(discKey([163, 34, 211, 14, 25, 118, 181, 233]), "setRewardConfig");
|
|
154
|
+
quest.set(discKey([163, 41, 94, 29, 221, 56, 112, 60]), "setRewardPerShare");
|
|
155
|
+
quest.set(discKey([202, 75, 225, 146, 240, 65, 15, 60]), "setStakeAuthority");
|
|
156
|
+
quest.set(discKey([84, 37, 76, 39, 236, 111, 214, 191]), "setStakeConfig");
|
|
157
|
+
quest.set(discKey([48, 169, 76, 72, 229, 180, 55, 161]), "transferAuthority");
|
|
158
|
+
programs.set(DEFAULT_QUEST_PROGRAM_ID, quest);
|
|
159
|
+
|
|
160
|
+
// Agent Registry
|
|
161
|
+
const agent: DiscriminatorMap = new Map();
|
|
162
|
+
agent.set(discKey([135, 157, 66, 195, 2, 113, 175, 30]), "registerAgent");
|
|
163
|
+
agent.set(discKey([12, 236, 115, 32, 129, 99, 250, 6]), "registerAgentWithReferral");
|
|
164
|
+
agent.set(discKey([136, 238, 98, 223, 118, 178, 238, 183]), "setTwitter");
|
|
165
|
+
agent.set(discKey([170, 213, 202, 134, 247, 139, 15, 24]), "verifyTwitter");
|
|
166
|
+
agent.set(discKey([97, 238, 35, 162, 61, 92, 88, 183]), "rejectTwitter");
|
|
167
|
+
agent.set(discKey([119, 20, 181, 34, 178, 73, 81, 50]), "approveRejectedTwitter");
|
|
168
|
+
agent.set(discKey([93, 66, 28, 60, 27, 34, 252, 166]), "unbindTwitter");
|
|
169
|
+
agent.set(discKey([140, 200, 213, 38, 145, 255, 191, 254]), "submitTweet");
|
|
170
|
+
agent.set(discKey([57, 71, 49, 146, 108, 84, 107, 45]), "approveTweet");
|
|
171
|
+
agent.set(discKey([231, 64, 127, 185, 55, 253, 175, 30]), "rejectTweet");
|
|
172
|
+
agent.set(discKey([92, 170, 90, 13, 148, 155, 212, 55]), "deleteAgent");
|
|
173
|
+
agent.set(discKey([196, 133, 219, 43, 75, 223, 195, 213]), "setBio");
|
|
174
|
+
agent.set(discKey([78, 157, 75, 242, 151, 20, 121, 144]), "setMetadata");
|
|
175
|
+
agent.set(discKey([213, 23, 157, 74, 199, 152, 182, 8]), "setReferral");
|
|
176
|
+
agent.set(discKey([158, 66, 173, 69, 248, 86, 13, 237]), "logActivity");
|
|
177
|
+
agent.set(discKey([165, 163, 235, 109, 240, 153, 233, 188]), "logActivityWithReferral");
|
|
178
|
+
agent.set(discKey([123, 211, 233, 210, 166, 139, 218, 60]), "initBuffer");
|
|
179
|
+
agent.set(discKey([114, 53, 121, 144, 201, 97, 248, 69]), "writeToBuffer");
|
|
180
|
+
agent.set(discKey([46, 114, 179, 58, 57, 45, 194, 172]), "closeBuffer");
|
|
181
|
+
agent.set(discKey([215, 42, 43, 208, 191, 38, 11, 146]), "finalizeMemoryNew");
|
|
182
|
+
agent.set(discKey([163, 20, 118, 65, 132, 16, 239, 4]), "finalizeMemoryUpdate");
|
|
183
|
+
agent.set(discKey([50, 204, 47, 193, 90, 227, 5, 220]), "finalizeMemoryAppend");
|
|
184
|
+
agent.set(discKey([23, 235, 115, 232, 168, 96, 1, 231]), "initConfig");
|
|
185
|
+
agent.set(discKey([120, 201, 195, 128, 35, 202, 73, 161]), "expandConfig");
|
|
186
|
+
agent.set(discKey([161, 176, 40, 213, 60, 184, 179, 228]), "updateAdmin");
|
|
187
|
+
agent.set(discKey([16, 11, 242, 97, 55, 197, 142, 249]), "updateRegisterFee");
|
|
188
|
+
agent.set(discKey([129, 209, 121, 34, 163, 184, 187, 56]), "updateReferralConfig");
|
|
189
|
+
agent.set(discKey([81, 250, 176, 204, 250, 169, 146, 144]), "updateTwitterVerifier");
|
|
190
|
+
agent.set(discKey([74, 177, 105, 71, 46, 192, 112, 135]), "updateTwitterVerificationConfig");
|
|
191
|
+
agent.set(discKey([16, 173, 44, 208, 249, 61, 172, 152]), "updateTweetVerifyConfig");
|
|
192
|
+
agent.set(discKey([167, 203, 189, 80, 145, 175, 74, 127]), "updateActivityConfig");
|
|
193
|
+
agent.set(discKey([15, 89, 27, 201, 127, 239, 187, 80]), "updatePointsConfig");
|
|
194
|
+
agent.set(discKey([198, 212, 171, 109, 144, 215, 174, 89]), "withdrawFees");
|
|
195
|
+
agent.set(discKey([64, 212, 105, 60, 34, 93, 221, 176]), "withdrawTwitterVerifyFees");
|
|
196
|
+
agent.set(discKey([48, 169, 76, 72, 229, 180, 55, 161]), "transferAuthority");
|
|
197
|
+
programs.set(DEFAULT_AGENT_REGISTRY_PROGRAM_ID, agent);
|
|
198
|
+
|
|
199
|
+
// Skills Hub
|
|
200
|
+
const skills: DiscriminatorMap = new Map();
|
|
201
|
+
skills.set(discKey([166, 249, 255, 189, 192, 197, 102, 2]), "registerSkill");
|
|
202
|
+
skills.set(discKey([123, 211, 233, 210, 166, 139, 218, 60]), "initBuffer");
|
|
203
|
+
skills.set(discKey([114, 53, 121, 144, 201, 97, 248, 69]), "writeToBuffer");
|
|
204
|
+
skills.set(discKey([46, 114, 179, 58, 57, 45, 194, 172]), "closeBuffer");
|
|
205
|
+
skills.set(discKey([253, 108, 88, 38, 27, 56, 113, 217]), "finalizeSkillNew");
|
|
206
|
+
skills.set(discKey([43, 248, 97, 58, 212, 79, 238, 179]), "finalizeSkillUpdate");
|
|
207
|
+
skills.set(discKey([17, 38, 1, 212, 5, 56, 231, 151]), "deleteSkill");
|
|
208
|
+
skills.set(discKey([234, 4, 121, 243, 47, 60, 8, 236]), "setDescription");
|
|
209
|
+
skills.set(discKey([170, 182, 43, 239, 97, 78, 225, 186]), "updateMetadata");
|
|
210
|
+
skills.set(discKey([23, 235, 115, 232, 168, 96, 1, 231]), "initConfig");
|
|
211
|
+
skills.set(discKey([161, 176, 40, 213, 60, 184, 179, 228]), "updateAdmin");
|
|
212
|
+
skills.set(discKey([16, 11, 242, 97, 55, 197, 142, 249]), "updateRegisterFee");
|
|
213
|
+
skills.set(discKey([198, 212, 171, 109, 144, 215, 174, 89]), "withdrawFees");
|
|
214
|
+
skills.set(discKey([48, 169, 76, 72, 229, 180, 55, 161]), "transferAuthority");
|
|
215
|
+
programs.set(DEFAULT_SKILLS_PROGRAM_ID, skills);
|
|
216
|
+
|
|
217
|
+
// ZK ID
|
|
218
|
+
const zk: DiscriminatorMap = new Map();
|
|
219
|
+
zk.set(discKey([211, 124, 67, 15, 211, 194, 178, 240]), "register");
|
|
220
|
+
zk.set(discKey([242, 35, 198, 137, 82, 225, 242, 182]), "deposit");
|
|
221
|
+
zk.set(discKey([183, 18, 70, 156, 148, 109, 161, 34]), "withdraw");
|
|
222
|
+
zk.set(discKey([58, 45, 142, 162, 7, 21, 17, 83]), "transferZkId");
|
|
223
|
+
zk.set(discKey([175, 175, 109, 31, 13, 152, 155, 237]), "initialize");
|
|
224
|
+
zk.set(discKey([208, 127, 21, 1, 194, 190, 196, 70]), "initializeConfig");
|
|
225
|
+
zk.set(discKey([29, 158, 252, 191, 10, 83, 219, 99]), "updateConfig");
|
|
226
|
+
zk.set(discKey([198, 212, 171, 109, 144, 215, 174, 89]), "withdrawFees");
|
|
227
|
+
programs.set(DEFAULT_ZKID_PROGRAM_ID, zk);
|
|
228
|
+
|
|
229
|
+
return programs;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const anchorDiscs = buildAnchorDiscriminators();
|
|
233
|
+
|
|
234
|
+
// ─── Instruction decoders ─────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
function resolveAccounts(
|
|
237
|
+
ix: MessageCompiledInstruction,
|
|
238
|
+
accountKeys: string[]
|
|
239
|
+
): string[] {
|
|
240
|
+
return ix.accountKeyIndexes.map((i) => accountKeys[i] ?? `<index:${i}>`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build a full TransactionInstruction from a compiled ix + account key list,
|
|
245
|
+
* so built-in decoders (SystemInstruction, spl-token decodeInstruction) can consume it.
|
|
246
|
+
*/
|
|
247
|
+
function toTransactionInstruction(
|
|
248
|
+
ix: MessageCompiledInstruction,
|
|
249
|
+
accountKeys: string[]
|
|
250
|
+
): TransactionInstruction {
|
|
251
|
+
const programIdStr = accountKeys[ix.programIdIndex] ?? "11111111111111111111111111111111";
|
|
252
|
+
return new TransactionInstruction({
|
|
253
|
+
programId: new PublicKey(programIdStr),
|
|
254
|
+
keys: ix.accountKeyIndexes.map((i) => ({
|
|
255
|
+
pubkey: new PublicKey(accountKeys[i] ?? "11111111111111111111111111111111"),
|
|
256
|
+
isSigner: false,
|
|
257
|
+
isWritable: false,
|
|
258
|
+
})),
|
|
259
|
+
data: Buffer.from(ix.data),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function parseComputeBudget(
|
|
264
|
+
data: Buffer
|
|
265
|
+
): { type: string; info: Record<string, unknown> } {
|
|
266
|
+
// ComputeBudget layout is simple and stable; no built-in decoder in web3.js.
|
|
267
|
+
const ixType = data[0];
|
|
268
|
+
if (ixType === 2) {
|
|
269
|
+
return { type: "setComputeUnitLimit", info: { units: data.readUInt32LE(1) } };
|
|
270
|
+
}
|
|
271
|
+
if (ixType === 3) {
|
|
272
|
+
return { type: "setComputeUnitPrice", info: { microLamports: Number(data.readBigUInt64LE(1)) } };
|
|
273
|
+
}
|
|
274
|
+
if (ixType === 1) {
|
|
275
|
+
return { type: "requestHeapFrame", info: { bytes: data.readUInt32LE(1) } };
|
|
276
|
+
}
|
|
277
|
+
if (ixType === 0) {
|
|
278
|
+
return { type: "requestUnits", info: {} };
|
|
279
|
+
}
|
|
280
|
+
return { type: `unknown(${ixType})`, info: {} };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function parseSystemProgram(
|
|
284
|
+
txIx: TransactionInstruction
|
|
285
|
+
): { type: string; info: Record<string, unknown> } {
|
|
286
|
+
let type: string;
|
|
287
|
+
try {
|
|
288
|
+
type = SystemInstruction.decodeInstructionType(txIx);
|
|
289
|
+
} catch {
|
|
290
|
+
return { type: "unknown", info: {} };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
switch (type) {
|
|
295
|
+
case "Create": {
|
|
296
|
+
const d = SystemInstruction.decodeCreateAccount(txIx);
|
|
297
|
+
return {
|
|
298
|
+
type: "createAccount",
|
|
299
|
+
info: {
|
|
300
|
+
from: d.fromPubkey.toBase58(),
|
|
301
|
+
newAccount: d.newAccountPubkey.toBase58(),
|
|
302
|
+
lamports: d.lamports,
|
|
303
|
+
space: d.space,
|
|
304
|
+
owner: d.programId.toBase58(),
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
case "Transfer": {
|
|
309
|
+
const d = SystemInstruction.decodeTransfer(txIx);
|
|
310
|
+
const lamports = Number(d.lamports);
|
|
311
|
+
return {
|
|
312
|
+
type: "transfer",
|
|
313
|
+
info: {
|
|
314
|
+
from: d.fromPubkey.toBase58(),
|
|
315
|
+
to: d.toPubkey.toBase58(),
|
|
316
|
+
lamports,
|
|
317
|
+
sol: lamports / 1e9,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
case "TransferWithSeed": {
|
|
322
|
+
const d = SystemInstruction.decodeTransferWithSeed(txIx);
|
|
323
|
+
return {
|
|
324
|
+
type: "transferWithSeed",
|
|
325
|
+
info: {
|
|
326
|
+
from: d.fromPubkey.toBase58(),
|
|
327
|
+
to: d.toPubkey.toBase58(),
|
|
328
|
+
lamports: Number(d.lamports),
|
|
329
|
+
seed: d.seed,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
case "Allocate": {
|
|
334
|
+
const d = SystemInstruction.decodeAllocate(txIx);
|
|
335
|
+
return { type: "allocate", info: { account: d.accountPubkey.toBase58(), space: d.space } };
|
|
336
|
+
}
|
|
337
|
+
case "Assign": {
|
|
338
|
+
const d = SystemInstruction.decodeAssign(txIx);
|
|
339
|
+
return {
|
|
340
|
+
type: "assign",
|
|
341
|
+
info: { account: d.accountPubkey.toBase58(), owner: d.programId.toBase58() },
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
case "AdvanceNonceAccount": {
|
|
345
|
+
const d = SystemInstruction.decodeNonceAdvance(txIx);
|
|
346
|
+
return {
|
|
347
|
+
type: "advanceNonce",
|
|
348
|
+
info: { nonce: d.noncePubkey.toBase58(), authorized: d.authorizedPubkey.toBase58() },
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
case "WithdrawNonceAccount": {
|
|
352
|
+
const d = SystemInstruction.decodeNonceWithdraw(txIx);
|
|
353
|
+
return {
|
|
354
|
+
type: "withdrawNonce",
|
|
355
|
+
info: {
|
|
356
|
+
nonce: d.noncePubkey.toBase58(),
|
|
357
|
+
to: d.toPubkey.toBase58(),
|
|
358
|
+
lamports: Number(d.lamports),
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
default:
|
|
363
|
+
return { type: type.charAt(0).toLowerCase() + type.slice(1), info: {} };
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
return { type: type.charAt(0).toLowerCase() + type.slice(1), info: {} };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function parseSplToken(
|
|
371
|
+
txIx: TransactionInstruction,
|
|
372
|
+
is2022: boolean
|
|
373
|
+
): { type: string; info: Record<string, unknown> } {
|
|
374
|
+
const programId = is2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
|
|
375
|
+
const program = is2022 ? "Token-2022" : "Token";
|
|
376
|
+
|
|
377
|
+
let decoded: any;
|
|
378
|
+
try {
|
|
379
|
+
decoded = decodeSplTokenInstruction(txIx, programId);
|
|
380
|
+
} catch {
|
|
381
|
+
const ixByte = txIx.data[0];
|
|
382
|
+
return { type: `splToken(${ixByte})`, info: { program } };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const ixKind: number = decoded.data.instruction;
|
|
386
|
+
const name = TokenInstruction[ixKind] ?? "unknown";
|
|
387
|
+
const typeStr = name.charAt(0).toLowerCase() + name.slice(1);
|
|
388
|
+
const info: Record<string, unknown> = { program };
|
|
389
|
+
|
|
390
|
+
// `decoded.keys` is an object with named fields — expose them all as base58 strings.
|
|
391
|
+
for (const [key, value] of Object.entries(decoded.keys)) {
|
|
392
|
+
if (value && typeof value === "object" && "pubkey" in (value as object)) {
|
|
393
|
+
info[key] = (value as { pubkey: PublicKey }).pubkey.toBase58();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Expose decoded data fields (amount, decimals, etc.)
|
|
398
|
+
for (const [key, value] of Object.entries(decoded.data)) {
|
|
399
|
+
if (key === "instruction") continue;
|
|
400
|
+
if (typeof value === "bigint") info[key] = Number(value);
|
|
401
|
+
else info[key] = value;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return { type: typeStr, info };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function parseAta(
|
|
408
|
+
data: Buffer,
|
|
409
|
+
accounts: string[]
|
|
410
|
+
): { type: string; info: Record<string, unknown> } {
|
|
411
|
+
// ATA program: no built-in decoder; layout is trivially [0]=create, [1]=createIdempotent
|
|
412
|
+
const ixType = data.length > 0 ? data[0] : 0;
|
|
413
|
+
return {
|
|
414
|
+
type: ixType === 1 ? "createIdempotent" : "create",
|
|
415
|
+
info: {
|
|
416
|
+
payer: accounts[0],
|
|
417
|
+
ata: accounts[1],
|
|
418
|
+
owner: accounts[2],
|
|
419
|
+
mint: accounts[3],
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function parseAnchor(
|
|
425
|
+
pid: string,
|
|
426
|
+
data: Buffer,
|
|
427
|
+
accounts: string[]
|
|
428
|
+
): { type: string; info: Record<string, unknown> } | null {
|
|
429
|
+
if (data.length < 8) return null;
|
|
430
|
+
const disc = data.subarray(0, 8);
|
|
431
|
+
const key = Buffer.from(disc).toString("hex");
|
|
432
|
+
const discMap = anchorDiscs.get(pid);
|
|
433
|
+
if (!discMap) return null;
|
|
434
|
+
const name = discMap.get(key);
|
|
435
|
+
if (!name) return null;
|
|
436
|
+
|
|
437
|
+
const info: Record<string, unknown> = {};
|
|
438
|
+
|
|
439
|
+
// Extract common fields for known instructions
|
|
440
|
+
if (pid === DEFAULT_AGENT_REGISTRY_PROGRAM_ID) {
|
|
441
|
+
if (name === "registerAgent" || name === "registerAgentWithReferral") {
|
|
442
|
+
if (accounts[0]) info.authority = accounts[0];
|
|
443
|
+
// agentId is first arg after discriminator: 4-byte string length + string
|
|
444
|
+
try {
|
|
445
|
+
const len = data.readUInt32LE(8);
|
|
446
|
+
if (len > 0 && len < 100) info.agentId = data.subarray(12, 12 + len).toString("utf-8");
|
|
447
|
+
} catch {}
|
|
448
|
+
}
|
|
449
|
+
if (name === "setTwitter") {
|
|
450
|
+
if (accounts[0]) info.authority = accounts[0];
|
|
451
|
+
try {
|
|
452
|
+
const len1 = data.readUInt32LE(8);
|
|
453
|
+
const agentId = data.subarray(12, 12 + len1).toString("utf-8");
|
|
454
|
+
const off2 = 12 + len1;
|
|
455
|
+
const len2 = data.readUInt32LE(off2);
|
|
456
|
+
const handle = data.subarray(off2 + 4, off2 + 4 + len2).toString("utf-8");
|
|
457
|
+
info.agentId = agentId;
|
|
458
|
+
info.handle = handle;
|
|
459
|
+
} catch {}
|
|
460
|
+
}
|
|
461
|
+
if (name === "submitTweet") {
|
|
462
|
+
if (accounts[0]) info.authority = accounts[0];
|
|
463
|
+
try {
|
|
464
|
+
const len1 = data.readUInt32LE(8);
|
|
465
|
+
info.agentId = data.subarray(12, 12 + len1).toString("utf-8");
|
|
466
|
+
} catch {}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (pid === DEFAULT_QUEST_PROGRAM_ID) {
|
|
471
|
+
if (name === "submitAnswer") {
|
|
472
|
+
if (accounts[0]) info.user = accounts[0];
|
|
473
|
+
}
|
|
474
|
+
if (name === "stake" || name === "unstake") {
|
|
475
|
+
if (accounts[0]) info.user = accounts[0];
|
|
476
|
+
}
|
|
477
|
+
if (name === "adjustFreeStake") {
|
|
478
|
+
if (accounts[0]) info.authority = accounts[0];
|
|
479
|
+
}
|
|
480
|
+
if (name === "claimAirdrop") {
|
|
481
|
+
if (accounts[0]) info.user = accounts[0];
|
|
482
|
+
}
|
|
483
|
+
if (name === "createQuestion") {
|
|
484
|
+
if (accounts[0]) info.authority = accounts[0];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (pid === DEFAULT_ZKID_PROGRAM_ID) {
|
|
489
|
+
if (name === "register") {
|
|
490
|
+
if (accounts[0]) info.authority = accounts[0];
|
|
491
|
+
}
|
|
492
|
+
if (name === "deposit" || name === "withdraw" || name === "transferZkId") {
|
|
493
|
+
if (accounts[0]) info.user = accounts[0];
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return { type: name, info };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function parseBridgeWarpRoute(
|
|
501
|
+
pid: string,
|
|
502
|
+
data: Buffer,
|
|
503
|
+
accounts: string[]
|
|
504
|
+
): { type: string; info: Record<string, unknown> } | null {
|
|
505
|
+
// TransferRemote: 9 bytes prefix (all 0x01) + 4 bytes domain + 32 bytes recipient + 32 bytes amount
|
|
506
|
+
if (data.length < 77) return null;
|
|
507
|
+
// Check prefix: first 9 bytes all 0x01
|
|
508
|
+
for (let i = 0; i < 9; i++) {
|
|
509
|
+
if (data[i] !== 0x01) return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const destinationDomain = data.readUInt32LE(9);
|
|
513
|
+
const recipient = new PublicKey(data.subarray(13, 45)).toBase58();
|
|
514
|
+
const amount = Number(data.readBigUInt64LE(45));
|
|
515
|
+
|
|
516
|
+
// Find token symbol from program ID
|
|
517
|
+
let tokenSymbol = "unknown";
|
|
518
|
+
let mode = "";
|
|
519
|
+
for (const [sym, cfg] of Object.entries(BRIDGE_TOKENS)) {
|
|
520
|
+
if (cfg.solana.warpProgram.toBase58() === pid) {
|
|
521
|
+
tokenSymbol = sym;
|
|
522
|
+
mode = cfg.solana.mode;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
if (cfg.nara.warpProgram.toBase58() === pid) {
|
|
526
|
+
tokenSymbol = sym;
|
|
527
|
+
mode = cfg.nara.mode;
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
type: "transferRemote",
|
|
534
|
+
info: {
|
|
535
|
+
token: tokenSymbol,
|
|
536
|
+
mode,
|
|
537
|
+
destinationDomain,
|
|
538
|
+
recipient,
|
|
539
|
+
amount,
|
|
540
|
+
sender: accounts[6],
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function parseMemo(
|
|
546
|
+
data: Buffer
|
|
547
|
+
): { type: string; info: Record<string, unknown> } {
|
|
548
|
+
return { type: "memo", info: { text: data.toString("utf-8") } };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ─── Main decoder ─────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
function decodeInstruction(
|
|
554
|
+
ix: MessageCompiledInstruction,
|
|
555
|
+
index: number,
|
|
556
|
+
accountKeys: string[]
|
|
557
|
+
): ParsedInstruction {
|
|
558
|
+
const pid = accountKeys[ix.programIdIndex] ?? "<unknown>";
|
|
559
|
+
const accounts = resolveAccounts(ix, accountKeys);
|
|
560
|
+
const data = Buffer.from(ix.data);
|
|
561
|
+
const rawData = Buffer.from(ix.data).toString("base64");
|
|
562
|
+
|
|
563
|
+
let type = "unknown";
|
|
564
|
+
let info: Record<string, unknown> = {};
|
|
565
|
+
|
|
566
|
+
if (pid === COMPUTE_BUDGET) {
|
|
567
|
+
const r = parseComputeBudget(data);
|
|
568
|
+
type = r.type;
|
|
569
|
+
info = r.info;
|
|
570
|
+
} else if (pid === SYSTEM_PROGRAM) {
|
|
571
|
+
const r = parseSystemProgram(toTransactionInstruction(ix, accountKeys));
|
|
572
|
+
type = r.type;
|
|
573
|
+
info = r.info;
|
|
574
|
+
} else if (pid === TOKEN_PROGRAM || pid === TOKEN_2022) {
|
|
575
|
+
const r = parseSplToken(
|
|
576
|
+
toTransactionInstruction(ix, accountKeys),
|
|
577
|
+
pid === TOKEN_2022
|
|
578
|
+
);
|
|
579
|
+
type = r.type;
|
|
580
|
+
info = r.info;
|
|
581
|
+
} else if (pid === ATA_PROGRAM) {
|
|
582
|
+
const r = parseAta(data, accounts);
|
|
583
|
+
type = r.type;
|
|
584
|
+
info = r.info;
|
|
585
|
+
} else if (pid === MEMO_PROGRAM || pid === MEMO_PROGRAM_V1) {
|
|
586
|
+
const r = parseMemo(data);
|
|
587
|
+
type = r.type;
|
|
588
|
+
info = r.info;
|
|
589
|
+
} else if (anchorDiscs.has(pid)) {
|
|
590
|
+
const r = parseAnchor(pid, data, accounts);
|
|
591
|
+
if (r) {
|
|
592
|
+
type = r.type;
|
|
593
|
+
info = r.info;
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
// Try bridge warp route
|
|
597
|
+
const r = parseBridgeWarpRoute(pid, data, accounts);
|
|
598
|
+
if (r) {
|
|
599
|
+
type = r.type;
|
|
600
|
+
info = r.info;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
index,
|
|
606
|
+
programName: getProgramName(pid),
|
|
607
|
+
programId: pid,
|
|
608
|
+
type,
|
|
609
|
+
info,
|
|
610
|
+
accounts,
|
|
611
|
+
rawData,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ─── Public API ───────────────────────────────────────────────────
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Fetch a transaction by signature and parse it.
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```ts
|
|
622
|
+
* const parsed = await parseTxFromHash(connection, "5abc...");
|
|
623
|
+
* console.log(formatParsedTx(parsed));
|
|
624
|
+
* ```
|
|
625
|
+
*/
|
|
626
|
+
export async function parseTxFromHash(
|
|
627
|
+
connection: Connection,
|
|
628
|
+
signature: string
|
|
629
|
+
): Promise<ParsedTransaction> {
|
|
630
|
+
const tx = await connection.getTransaction(signature, {
|
|
631
|
+
maxSupportedTransactionVersion: 0,
|
|
632
|
+
commitment: "confirmed",
|
|
633
|
+
});
|
|
634
|
+
if (!tx) throw new Error(`Transaction not found: ${signature}`);
|
|
635
|
+
return parseTxResponse(tx);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Parse a VersionedTransactionResponse (from getTransaction) synchronously.
|
|
640
|
+
*
|
|
641
|
+
* @example
|
|
642
|
+
* ```ts
|
|
643
|
+
* const txResp = await connection.getTransaction(sig, { maxSupportedTransactionVersion: 0 });
|
|
644
|
+
* const parsed = parseTxResponse(txResp);
|
|
645
|
+
* ```
|
|
646
|
+
*/
|
|
647
|
+
export function parseTxResponse(tx: VersionedTransactionResponse): ParsedTransaction {
|
|
648
|
+
const sig = tx.transaction.signatures[0] ?? "";
|
|
649
|
+
|
|
650
|
+
// Collect all account keys (static + loaded via ALT)
|
|
651
|
+
const msg = tx.transaction.message;
|
|
652
|
+
const staticKeys = msg.getAccountKeys({
|
|
653
|
+
accountKeysFromLookups: tx.meta?.loadedAddresses,
|
|
654
|
+
});
|
|
655
|
+
const accountKeys: string[] = [];
|
|
656
|
+
for (let i = 0; i < staticKeys.length; i++) {
|
|
657
|
+
accountKeys.push(staticKeys.get(i)!.toBase58());
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Parse each top-level instruction
|
|
661
|
+
const compiled = msg.compiledInstructions;
|
|
662
|
+
const instructions: ParsedInstruction[] = compiled.map((ix, i) =>
|
|
663
|
+
decodeInstruction(ix, i, accountKeys)
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
// Also parse inner instructions (CPI)
|
|
667
|
+
if (tx.meta?.innerInstructions) {
|
|
668
|
+
for (const inner of tx.meta.innerInstructions) {
|
|
669
|
+
for (const innerIx of inner.instructions) {
|
|
670
|
+
let dataBuf: Buffer;
|
|
671
|
+
if (typeof innerIx.data === "string") {
|
|
672
|
+
dataBuf = Buffer.from(decodeBase58(innerIx.data));
|
|
673
|
+
} else {
|
|
674
|
+
dataBuf = Buffer.from(innerIx.data);
|
|
675
|
+
}
|
|
676
|
+
const innerCompiled: MessageCompiledInstruction = {
|
|
677
|
+
programIdIndex: innerIx.programIdIndex,
|
|
678
|
+
accountKeyIndexes: innerIx.accounts,
|
|
679
|
+
data: dataBuf,
|
|
680
|
+
};
|
|
681
|
+
instructions.push(
|
|
682
|
+
decodeInstruction(innerCompiled, instructions.length, accountKeys)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return {
|
|
689
|
+
signature: sig,
|
|
690
|
+
slot: tx.slot,
|
|
691
|
+
blockTime: tx.blockTime ?? null,
|
|
692
|
+
success: tx.meta?.err === null,
|
|
693
|
+
error: tx.meta?.err ? JSON.stringify(tx.meta.err) : null,
|
|
694
|
+
fee: tx.meta?.fee ?? 0,
|
|
695
|
+
instructions,
|
|
696
|
+
logs: tx.meta?.logMessages ?? [],
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function decodeBase58(str: string): Uint8Array {
|
|
701
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
702
|
+
const BASE = 58;
|
|
703
|
+
const bytes: number[] = [0];
|
|
704
|
+
for (const char of str) {
|
|
705
|
+
let carry = ALPHABET.indexOf(char);
|
|
706
|
+
if (carry < 0) throw new Error(`Invalid base58 character: ${char}`);
|
|
707
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
708
|
+
carry += bytes[j]! * BASE;
|
|
709
|
+
bytes[j] = carry & 0xff;
|
|
710
|
+
carry >>= 8;
|
|
711
|
+
}
|
|
712
|
+
while (carry > 0) {
|
|
713
|
+
bytes.push(carry & 0xff);
|
|
714
|
+
carry >>= 8;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Leading zeros
|
|
718
|
+
for (const char of str) {
|
|
719
|
+
if (char !== "1") break;
|
|
720
|
+
bytes.push(0);
|
|
721
|
+
}
|
|
722
|
+
return new Uint8Array(bytes.reverse());
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Pretty-print a parsed transaction to a human-readable string.
|
|
727
|
+
*/
|
|
728
|
+
export function formatParsedTx(parsed: ParsedTransaction): string {
|
|
729
|
+
const lines: string[] = [];
|
|
730
|
+
const status = parsed.success ? "SUCCESS" : `FAILED: ${parsed.error}`;
|
|
731
|
+
const time = parsed.blockTime
|
|
732
|
+
? new Date(parsed.blockTime * 1000).toISOString()
|
|
733
|
+
: "unknown";
|
|
734
|
+
|
|
735
|
+
lines.push(`Tx: ${parsed.signature}`);
|
|
736
|
+
lines.push(`Status: ${status}`);
|
|
737
|
+
lines.push(`Slot: ${parsed.slot} | Time: ${time} | Fee: ${parsed.fee} lamports`);
|
|
738
|
+
lines.push(`Instructions (${parsed.instructions.length}):`);
|
|
739
|
+
lines.push("");
|
|
740
|
+
|
|
741
|
+
for (const ix of parsed.instructions) {
|
|
742
|
+
const infoStr = Object.entries(ix.info)
|
|
743
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
744
|
+
.join(", ");
|
|
745
|
+
lines.push(` #${ix.index} [${ix.programName}] ${ix.type}`);
|
|
746
|
+
if (infoStr) lines.push(` ${infoStr}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return lines.join("\n");
|
|
750
|
+
}
|
package/src/zkid.ts
CHANGED
|
@@ -400,8 +400,16 @@ export async function scanClaimableDeposits(
|
|
|
400
400
|
const [zkIdPda] = findZkIdPda(nameHashBuf, programId);
|
|
401
401
|
const [inboxPda] = findInboxPda(nameHashBuf, programId);
|
|
402
402
|
|
|
403
|
-
|
|
404
|
-
const
|
|
403
|
+
// Step 1: fetch zkId + inbox in a single RPC call
|
|
404
|
+
const [zkIdAcct, inboxAcct] = await connection.getMultipleAccountsInfo(
|
|
405
|
+
[zkIdPda, inboxPda],
|
|
406
|
+
"confirmed"
|
|
407
|
+
);
|
|
408
|
+
if (!zkIdAcct) throw new Error(`ZK ID account not found: ${zkIdPda.toBase58()}`);
|
|
409
|
+
if (!inboxAcct) throw new Error(`Inbox account not found: ${inboxPda.toBase58()}`);
|
|
410
|
+
|
|
411
|
+
const zkId = program.coder.accounts.decode("zkIdAccount", zkIdAcct.data);
|
|
412
|
+
const inbox = program.coder.accounts.decode("inboxAccount", inboxAcct.data);
|
|
405
413
|
|
|
406
414
|
const depositCount: number = zkId.depositCount;
|
|
407
415
|
const commitmentStart: number = zkId.commitmentStartIndex;
|
|
@@ -422,23 +430,34 @@ export async function scanClaimableDeposits(
|
|
|
422
430
|
// Oldest entry in inbox corresponds to depositIndex = (depositCount - count)
|
|
423
431
|
const startDepositIndex = depositCount - count;
|
|
424
432
|
|
|
425
|
-
|
|
433
|
+
// Step 2: derive all nullifier PDAs for candidate entries (owned deposits only)
|
|
434
|
+
type Candidate = { leafIndex: bigint; depositIndex: number; denomination: bigint; nullifierPda: PublicKey };
|
|
435
|
+
const candidates: Candidate[] = [];
|
|
426
436
|
for (let i = 0; i < entries.length; i++) {
|
|
427
437
|
const depositIndex = startDepositIndex + i;
|
|
428
|
-
|
|
429
|
-
// Deposits before commitmentStartIndex belong to a previous owner
|
|
430
438
|
if (depositIndex < commitmentStart) continue;
|
|
431
439
|
|
|
432
440
|
const { leafIndex, denomination } = entries[i]!;
|
|
433
|
-
|
|
434
|
-
// Check if nullifier has been spent
|
|
435
441
|
const nullifierHash_bi = await poseidonHash([idSecret, BigInt(depositIndex)]);
|
|
436
442
|
const nullifierHashBuf = bigIntToBytes32BE(nullifierHash_bi);
|
|
437
443
|
const denominationBN = new BN(denomination.toString());
|
|
438
444
|
const [nullifierPda] = findNullifierPda(denominationBN, nullifierHashBuf, programId);
|
|
439
|
-
|
|
445
|
+
candidates.push({ leafIndex, depositIndex, denomination, nullifierPda });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!candidates.length) return [];
|
|
440
449
|
|
|
441
|
-
|
|
450
|
+
// Step 3: batch-check all nullifiers in a single RPC call
|
|
451
|
+
// (inbox is capped at 64 entries, well under the 100-key limit)
|
|
452
|
+
const nullifierInfos = await connection.getMultipleAccountsInfo(
|
|
453
|
+
candidates.map((c) => c.nullifierPda),
|
|
454
|
+
"confirmed"
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
const claimable: ClaimableDeposit[] = [];
|
|
458
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
459
|
+
if (nullifierInfos[i] === null) {
|
|
460
|
+
const { leafIndex, depositIndex, denomination } = candidates[i]!;
|
|
442
461
|
claimable.push({ leafIndex, depositIndex, denomination });
|
|
443
462
|
}
|
|
444
463
|
}
|