nara-sdk 1.0.76 → 1.0.78
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 +8 -3
- package/index.ts +11 -1
- package/package.json +4 -1
- package/src/agent_registry.ts +10 -20
- package/src/bridge.ts +40 -18
- package/src/constants.ts +12 -8
- 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/README.md
CHANGED
|
@@ -127,12 +127,17 @@ registerBridgeToken('USDT', {
|
|
|
127
127
|
### Fee configuration
|
|
128
128
|
|
|
129
129
|
Default fee: **0.5%** (50 bps), deducted from the bridged amount on the source chain.
|
|
130
|
+
Fee recipients are chain-specific (one per source chain).
|
|
130
131
|
|
|
131
132
|
```ts
|
|
132
|
-
import { setBridgeFeeRecipient } from 'nara-sdk';
|
|
133
|
+
import { setBridgeFeeRecipient, getBridgeFeeRecipient } from 'nara-sdk';
|
|
133
134
|
|
|
134
|
-
// Override fee recipient at runtime
|
|
135
|
-
setBridgeFeeRecipient('
|
|
135
|
+
// Override fee recipient at runtime (per chain)
|
|
136
|
+
setBridgeFeeRecipient('solana', 'SolanaFeeRecipientPubkey...');
|
|
137
|
+
setBridgeFeeRecipient('nara', 'NaraFeeRecipientPubkey...');
|
|
138
|
+
|
|
139
|
+
// Read current recipient
|
|
140
|
+
const recipient = getBridgeFeeRecipient('solana'); // PublicKey
|
|
136
141
|
|
|
137
142
|
// Or per-call
|
|
138
143
|
await bridgeTransfer(conn, wallet, {
|
package/index.ts
CHANGED
|
@@ -16,7 +16,8 @@ export {
|
|
|
16
16
|
DEFAULT_AGENT_REGISTRY_PROGRAM_ID,
|
|
17
17
|
DEFAULT_ALT_ADDRESS,
|
|
18
18
|
DEFAULT_BRIDGE_FEE_BPS,
|
|
19
|
-
|
|
19
|
+
DEFAULT_BRIDGE_FEE_RECIPIENT_SOLANA,
|
|
20
|
+
DEFAULT_BRIDGE_FEE_RECIPIENT_NARA,
|
|
20
21
|
BRIDGE_FEE_BPS_DENOMINATOR,
|
|
21
22
|
} from "./src/constants";
|
|
22
23
|
|
|
@@ -219,6 +220,15 @@ export {
|
|
|
219
220
|
type MessageSignatureStatus,
|
|
220
221
|
} from "./src/bridge";
|
|
221
222
|
|
|
223
|
+
// Export transaction parser
|
|
224
|
+
export {
|
|
225
|
+
parseTxFromHash,
|
|
226
|
+
parseTxResponse,
|
|
227
|
+
formatParsedTx,
|
|
228
|
+
type ParsedInstruction,
|
|
229
|
+
type ParsedTransaction,
|
|
230
|
+
} from "./src/tx_parser";
|
|
231
|
+
|
|
222
232
|
// Export IDLs and types
|
|
223
233
|
export { default as NaraQuestIDL } from "./src/idls/nara_quest.json";
|
|
224
234
|
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.78",
|
|
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
|
@@ -27,7 +27,8 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
BRIDGE_FEE_BPS_DENOMINATOR,
|
|
29
29
|
DEFAULT_BRIDGE_FEE_BPS,
|
|
30
|
-
|
|
30
|
+
DEFAULT_BRIDGE_FEE_RECIPIENT_SOLANA,
|
|
31
|
+
DEFAULT_BRIDGE_FEE_RECIPIENT_NARA,
|
|
31
32
|
} from "./constants";
|
|
32
33
|
import { sendTx } from "./tx";
|
|
33
34
|
|
|
@@ -185,23 +186,38 @@ function getToken(symbol: string): BridgeTokenConfig {
|
|
|
185
186
|
return t;
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
// ─── Fee recipient (runtime override)
|
|
189
|
+
// ─── Fee recipient (runtime override, per chain) ──────────────────
|
|
189
190
|
|
|
190
|
-
|
|
191
|
+
const _feeRecipientOverrides: Record<BridgeChain, PublicKey | null> = {
|
|
192
|
+
solana: null,
|
|
193
|
+
nara: null,
|
|
194
|
+
};
|
|
191
195
|
|
|
192
|
-
/**
|
|
193
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Override the bridge fee recipient for a specific source chain at runtime.
|
|
198
|
+
* Pass `null` as recipient to clear the override and fall back to the default.
|
|
199
|
+
*/
|
|
200
|
+
export function setBridgeFeeRecipient(
|
|
201
|
+
chain: BridgeChain,
|
|
202
|
+
recipient: PublicKey | string | null
|
|
203
|
+
): void {
|
|
194
204
|
if (recipient === null) {
|
|
195
|
-
|
|
205
|
+
_feeRecipientOverrides[chain] = null;
|
|
196
206
|
return;
|
|
197
207
|
}
|
|
198
|
-
|
|
208
|
+
_feeRecipientOverrides[chain] =
|
|
199
209
|
typeof recipient === "string" ? new PublicKey(recipient) : recipient;
|
|
200
210
|
}
|
|
201
211
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
/** Get the current fee recipient for a source chain (override or default). */
|
|
213
|
+
export function getBridgeFeeRecipient(chain: BridgeChain): PublicKey {
|
|
214
|
+
const override = _feeRecipientOverrides[chain];
|
|
215
|
+
if (override) return override;
|
|
216
|
+
const defaultAddr =
|
|
217
|
+
chain === "solana"
|
|
218
|
+
? DEFAULT_BRIDGE_FEE_RECIPIENT_SOLANA
|
|
219
|
+
: DEFAULT_BRIDGE_FEE_RECIPIENT_NARA;
|
|
220
|
+
return new PublicKey(defaultAddr);
|
|
205
221
|
}
|
|
206
222
|
|
|
207
223
|
// ─── PDA derivation ───────────────────────────────────────────────
|
|
@@ -515,7 +531,7 @@ export function makeBridgeIxs(params: BridgeTransferParams): BridgeIxsResult {
|
|
|
515
531
|
throw new Error("bridge amount after fee is zero — increase amount or lower feeBps");
|
|
516
532
|
}
|
|
517
533
|
|
|
518
|
-
const recipientForFee = feeRecipient ?? getBridgeFeeRecipient();
|
|
534
|
+
const recipientForFee = feeRecipient ?? getBridgeFeeRecipient(fromChain);
|
|
519
535
|
const feeIxs = makeBridgeFeeIxs({
|
|
520
536
|
token,
|
|
521
537
|
fromChain,
|
|
@@ -640,17 +656,23 @@ export async function queryMessageStatus(
|
|
|
640
656
|
const sigs = await destConnection.getSignaturesForAddress(mailbox, {
|
|
641
657
|
limit,
|
|
642
658
|
});
|
|
659
|
+
const validSigs = sigs.filter((s) => !s.err).map((s) => s.signature);
|
|
660
|
+
if (!validSigs.length) {
|
|
661
|
+
return { delivered: false, deliverySignature: null };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Batch-fetch transactions in a single JSON-RPC call
|
|
665
|
+
const txs = await destConnection.getTransactions(validSigs, {
|
|
666
|
+
maxSupportedTransactionVersion: 0,
|
|
667
|
+
commitment: "confirmed",
|
|
668
|
+
});
|
|
643
669
|
|
|
644
|
-
for (
|
|
645
|
-
|
|
646
|
-
const tx = await destConnection.getTransaction(entry.signature, {
|
|
647
|
-
maxSupportedTransactionVersion: 0,
|
|
648
|
-
commitment: "confirmed",
|
|
649
|
-
});
|
|
670
|
+
for (let i = 0; i < txs.length; i++) {
|
|
671
|
+
const tx = txs[i];
|
|
650
672
|
if (!tx?.meta?.logMessages) continue;
|
|
651
673
|
for (const log of tx.meta.logMessages) {
|
|
652
674
|
if (log.includes(messageId)) {
|
|
653
|
-
return { delivered: true, deliverySignature:
|
|
675
|
+
return { delivered: true, deliverySignature: validSigs[i] ?? null };
|
|
654
676
|
}
|
|
655
677
|
}
|
|
656
678
|
}
|
package/src/constants.ts
CHANGED
|
@@ -48,8 +48,8 @@ export const DEFAULT_ALT_ADDRESS = process.env.ALT_ADDRESS || "3uw7RatGTB4hdHnuV
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* Bridge fee in basis points (1 bps = 0.01%). 50 = 0.5%.
|
|
51
|
-
* Deducted from the bridged amount and transferred to
|
|
52
|
-
* in the same transaction.
|
|
51
|
+
* Deducted from the bridged amount and transferred to the chain-specific
|
|
52
|
+
* fee recipient in the same transaction.
|
|
53
53
|
*/
|
|
54
54
|
export const DEFAULT_BRIDGE_FEE_BPS = 50;
|
|
55
55
|
|
|
@@ -57,11 +57,15 @@ export const DEFAULT_BRIDGE_FEE_BPS = 50;
|
|
|
57
57
|
export const BRIDGE_FEE_BPS_DENOMINATOR = 10000;
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Default fee recipient pubkey
|
|
61
|
-
*
|
|
62
|
-
* Override at runtime via setBridgeFeeRecipient() in src/bridge.ts.
|
|
63
|
-
*
|
|
64
|
-
* NOTE: replace with actual fee recipient before mainnet usage.
|
|
60
|
+
* Default fee recipient pubkey on Solana (when bridging FROM Solana).
|
|
61
|
+
* Override at runtime via setBridgeFeeRecipient("solana", ...).
|
|
65
62
|
*/
|
|
66
|
-
export const
|
|
63
|
+
export const DEFAULT_BRIDGE_FEE_RECIPIENT_SOLANA =
|
|
64
|
+
"HaPQTvGJBunoWA3AyyWRL9etVEbQWsXVoj3fHpBprLy5";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Default fee recipient pubkey on Nara (when bridging FROM Nara).
|
|
68
|
+
* Override at runtime via setBridgeFeeRecipient("nara", ...).
|
|
69
|
+
*/
|
|
70
|
+
export const DEFAULT_BRIDGE_FEE_RECIPIENT_NARA =
|
|
67
71
|
"FERLFwBpCyoEuvFP68eP6Fv4FCVocnNyyFUCYwpfmjqn";
|
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
|
}
|