nulltrace-sdk 1.0.0

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/dist/index.mjs ADDED
@@ -0,0 +1,1074 @@
1
+ // src/index.js
2
+ import {
3
+ VersionedTransaction,
4
+ PublicKey,
5
+ TransactionMessage,
6
+ ComputeBudgetProgram,
7
+ Keypair,
8
+ SystemProgram
9
+ } from "@solana/web3.js";
10
+ import {
11
+ TOKEN_PROGRAM_ID,
12
+ TOKEN_2022_PROGRAM_ID,
13
+ getAssociatedTokenAddress,
14
+ createAssociatedTokenAccountInstruction,
15
+ createTransferCheckedInstruction,
16
+ NATIVE_MINT
17
+ } from "@solana/spl-token";
18
+ import {
19
+ createRpc,
20
+ bn,
21
+ LightSystemProgram,
22
+ COMPRESSED_TOKEN_PROGRAM_ID,
23
+ selectStateTreeInfo
24
+ } from "@lightprotocol/stateless.js";
25
+ import {
26
+ getTokenPoolInfos,
27
+ selectTokenPoolInfosForDecompression,
28
+ CompressedTokenProgram
29
+ } from "@lightprotocol/compressed-token";
30
+ import bs58 from "bs58";
31
+ import { createHmac } from "crypto";
32
+ import nacl from "tweetnacl";
33
+ var OPERATOR_KEY = "5STUuhrL8kJ4up9spEY39VJ6ibQCFrg8x8cRV5UeEcfv";
34
+ var OPERATOR_PUBLIC_KEY = new PublicKey(OPERATOR_KEY);
35
+ var ALT_ADDRESS = new PublicKey("9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ");
36
+ var REMOTE_OPERATOR_URL = "http://34.68.76.183:3333";
37
+ var SHARED_SECRET = "NULL_TRACE_OPERATOR_SECRET_BASE_V1";
38
+ var FEE_BPS = 1e-3;
39
+ var COMPUTE_UNITS = 14e5;
40
+ var COMPUTE_PRICE = 5e3;
41
+ var MAX_TX_SIZE = 1232;
42
+ function _getAuthToken() {
43
+ const step = 180;
44
+ const counter = Math.floor(Date.now() / 1e3 / step);
45
+ const buf = Buffer.alloc(8);
46
+ buf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
47
+ buf.writeUInt32BE(counter >>> 0, 4);
48
+ const hmac = createHmac("sha1", Buffer.from(SHARED_SECRET, "ascii"));
49
+ hmac.update(buf);
50
+ const hash = hmac.digest();
51
+ const offset = hash[hash.length - 1] & 15;
52
+ const code = (hash[offset] & 127) << 24 | (hash[offset + 1] & 255) << 16 | (hash[offset + 2] & 255) << 8 | hash[offset + 3] & 255;
53
+ return (code % 1e6).toString().padStart(6, "0");
54
+ }
55
+ function _validateHeliusRpc(url) {
56
+ if (!url || typeof url !== "string") {
57
+ throw new Error("NullTrace: Valid Helius rpcUrl is required");
58
+ }
59
+ const lower = url.toLowerCase();
60
+ if (!lower.includes("helius")) {
61
+ throw new Error(
62
+ "NullTrace: A Helius RPC endpoint is required. Get a key at https://helius.dev"
63
+ );
64
+ }
65
+ return url;
66
+ }
67
+ function _sleep(ms) {
68
+ return new Promise((r) => setTimeout(r, ms));
69
+ }
70
+ async function _getMintInfo(connection, mintAddress) {
71
+ if (mintAddress === NATIVE_MINT.toBase58()) {
72
+ return { decimals: 9, tokenProgram: TOKEN_PROGRAM_ID };
73
+ }
74
+ const mintInfo = await connection.getParsedAccountInfo(new PublicKey(mintAddress));
75
+ if (!mintInfo.value)
76
+ throw new Error(`Mint not found: ${mintAddress}`);
77
+ return {
78
+ decimals: mintInfo.value.data.parsed.info.decimals,
79
+ tokenProgram: new PublicKey(mintInfo.value.owner)
80
+ };
81
+ }
82
+ async function _getCompressedAccounts(connection, owner, mint, isSOL) {
83
+ const accounts = isSOL ? await connection.getCompressedAccountsByOwner(owner) : await connection.getCompressedTokenAccountsByOwner(owner, { mint: new PublicKey(mint) });
84
+ return accounts.items.sort((a, b) => {
85
+ const aAmt = isSOL ? a.lamports : a.parsed.amount;
86
+ const bAmt = isSOL ? b.lamports : b.parsed.amount;
87
+ return Number(bAmt) - Number(aAmt);
88
+ });
89
+ }
90
+ function _selectInputs(sortedAccounts, amountLamports, isSOL) {
91
+ const selected = [];
92
+ let total = 0;
93
+ for (const a of sortedAccounts) {
94
+ if (total >= amountLamports)
95
+ break;
96
+ total += Number(isSOL ? a.lamports : a.parsed.amount);
97
+ selected.push(a);
98
+ }
99
+ return { selected, total };
100
+ }
101
+ function _batchAccounts(accounts) {
102
+ const validSizes = [8, 4, 2, 1];
103
+ const batches = [];
104
+ let remaining = [...accounts];
105
+ while (remaining.length > 0) {
106
+ const size = validSizes.find((s) => remaining.length >= s) || 1;
107
+ batches.push(remaining.slice(0, size));
108
+ remaining = remaining.slice(size);
109
+ }
110
+ return batches;
111
+ }
112
+ async function _packTransactions(connection, payer, instructions, adl) {
113
+ const { blockhash } = await connection.getLatestBlockhash();
114
+ const computeIxs = [
115
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
116
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_PRICE })
117
+ ];
118
+ let current = [...computeIxs];
119
+ const messages = [];
120
+ for (const ix of instructions) {
121
+ try {
122
+ current.push(ix);
123
+ const msg = new TransactionMessage({
124
+ payerKey: payer,
125
+ recentBlockhash: blockhash,
126
+ instructions: current
127
+ }).compileToV0Message([adl]);
128
+ if (msg.serialize().length > MAX_TX_SIZE)
129
+ throw new Error("oversize");
130
+ } catch {
131
+ current.pop();
132
+ if (current.length > computeIxs.length) {
133
+ messages.push(
134
+ new TransactionMessage({
135
+ payerKey: payer,
136
+ recentBlockhash: blockhash,
137
+ instructions: current
138
+ }).compileToV0Message([adl])
139
+ );
140
+ }
141
+ current = [...computeIxs, ix];
142
+ }
143
+ }
144
+ if (current.length > computeIxs.length) {
145
+ messages.push(
146
+ new TransactionMessage({
147
+ payerKey: payer,
148
+ recentBlockhash: blockhash,
149
+ instructions: current
150
+ }).compileToV0Message([adl])
151
+ );
152
+ }
153
+ return messages.map((m) => new VersionedTransaction(m));
154
+ }
155
+ async function _signSendConfirm(connection, wallet, transactions) {
156
+ const signed = await wallet.signAllTransactions(transactions);
157
+ const sigs = [];
158
+ for (const tx of signed) {
159
+ const sig = await connection.sendRawTransaction(tx.serialize());
160
+ await connection.confirmTransaction(sig);
161
+ sigs.push(sig);
162
+ }
163
+ return sigs;
164
+ }
165
+ async function _enrichMetadata(tokenBalances) {
166
+ if (!tokenBalances.length)
167
+ return tokenBalances;
168
+ const addresses = tokenBalances.map((t) => t.address).join(",");
169
+ try {
170
+ const dexRes = await fetch("https://api.dexscreener.com/latest/dex/tokens/" + addresses);
171
+ if (dexRes.ok) {
172
+ const data = await dexRes.json();
173
+ if (data.pairs) {
174
+ const pairs = data.pairs.sort(
175
+ (a, b) => parseFloat(b.volume?.h24 ?? "0") - parseFloat(a.volume?.h24 ?? "0")
176
+ );
177
+ for (const t of tokenBalances) {
178
+ const pair = pairs.find((p) => p.baseToken.address === t.address);
179
+ if (pair) {
180
+ t.symbol = t.symbol || pair.baseToken.symbol;
181
+ t.name = t.name || pair.baseToken.name;
182
+ t.logo = t.logo || (pair.info?.imageUrl ?? "");
183
+ t.dexscreener = t.dexscreener || (pair.url ?? "");
184
+ }
185
+ }
186
+ }
187
+ }
188
+ } catch {
189
+ }
190
+ try {
191
+ const geckoRes = await fetch(
192
+ "https://api.geckoterminal.com/api/v2/networks/solana/tokens/multi/" + addresses + "?include=top_pools"
193
+ );
194
+ if (geckoRes.ok) {
195
+ const data = await geckoRes.json();
196
+ if (data.data) {
197
+ for (const t of tokenBalances) {
198
+ const gt = Object.values(data.data).find((x) => x.attributes?.address === t.address);
199
+ if (gt) {
200
+ t.symbol = t.symbol || gt.attributes.symbol;
201
+ t.name = t.name || gt.attributes.name;
202
+ t.logo = t.logo || gt.attributes.image_url;
203
+ t.decimals = t.decimals || gt.attributes.decimals;
204
+ if (t.decimals)
205
+ t.amount = (t.lamports / 10 ** t.decimals).toFixed(t.decimals);
206
+ }
207
+ }
208
+ }
209
+ }
210
+ } catch {
211
+ }
212
+ try {
213
+ const jupRes = await fetch(
214
+ "https://datapi.jup.ag/v1/assets/search?query=" + addresses + "&sortBy=verified"
215
+ );
216
+ if (jupRes.ok) {
217
+ const tokens = await jupRes.json();
218
+ if (tokens) {
219
+ for (const t of tokenBalances) {
220
+ const jt = tokens.find((x) => x.id === t.address);
221
+ if (jt) {
222
+ t.symbol = t.symbol || jt.symbol;
223
+ t.name = t.name || jt.name;
224
+ t.logo = t.logo || jt.icon;
225
+ t.decimals = t.decimals || jt.decimals;
226
+ if (t.decimals)
227
+ t.amount = (t.lamports / 10 ** t.decimals).toFixed(t.decimals);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ } catch {
233
+ }
234
+ return tokenBalances;
235
+ }
236
+ var NullTrace = class _NullTrace {
237
+ /**
238
+ * Create a standalone NullTrace client.
239
+ *
240
+ * The second argument can be any of the following:
241
+ *
242
+ * 1. **Wallet adapter** — an object with `publicKey`, `signAllTransactions`, and optionally `signMessage`.
243
+ * 2. **Keypair** — a `@solana/web3.js` Keypair instance.
244
+ * 3. **Secret key (Uint8Array)** — a 64-byte secret key array.
245
+ * 4. **Private key (base58 string)** — a base58-encoded private key.
246
+ *
247
+ * @param {string} rpcUrl A Helius RPC endpoint (required for ZK compression).
248
+ * @param {Object|Keypair|Uint8Array|string} walletOrKey Wallet adapter, Keypair, secret key bytes, or base58 private key.
249
+ *
250
+ * @example
251
+ * // Wallet adapter (browser)
252
+ * const nt = new NullTrace(rpcUrl, wallet);
253
+ *
254
+ * @example
255
+ * // Keypair (Node.js)
256
+ * const nt = new NullTrace(rpcUrl, Keypair.fromSecretKey(secretKey));
257
+ *
258
+ * @example
259
+ * // Raw secret key bytes
260
+ * const nt = new NullTrace(rpcUrl, mySecretKeyUint8Array);
261
+ *
262
+ * @example
263
+ * // Base58 private key string
264
+ * const nt = new NullTrace(rpcUrl, '4wBqp...');
265
+ */
266
+ constructor(rpcUrl, walletOrKey) {
267
+ _validateHeliusRpc(rpcUrl);
268
+ if (!walletOrKey)
269
+ throw new Error("NullTrace: a wallet, Keypair, secret key, or private key string is required");
270
+ this.rpcUrl = rpcUrl;
271
+ this.wallet = _NullTrace._resolveWallet(walletOrKey);
272
+ this.connection = createRpc(rpcUrl, rpcUrl, rpcUrl, { commitment: "processed" });
273
+ this._adlCache = null;
274
+ this._sigCache = null;
275
+ }
276
+ // -----------------------------------------------------------------------
277
+ // Static helpers for wallet resolution
278
+ // -----------------------------------------------------------------------
279
+ /**
280
+ * Create a wallet adapter interface from a Keypair.
281
+ *
282
+ * @param {Keypair} keypair A `@solana/web3.js` Keypair.
283
+ * @returns {{ publicKey: PublicKey, signAllTransactions: Function, signMessage: Function }}
284
+ */
285
+ static fromKeypair(keypair) {
286
+ if (!keypair?.publicKey || !keypair?.secretKey) {
287
+ throw new Error("NullTrace.fromKeypair: invalid Keypair");
288
+ }
289
+ return {
290
+ publicKey: keypair.publicKey,
291
+ signAllTransactions: async (txs) => {
292
+ for (const tx of txs)
293
+ tx.sign([keypair]);
294
+ return txs;
295
+ },
296
+ signMessage: async (msg) => nacl.sign.detached(msg, keypair.secretKey)
297
+ };
298
+ }
299
+ /**
300
+ * Create a wallet adapter interface from a raw secret key (64 bytes).
301
+ *
302
+ * @param {Uint8Array} secretKey A 64-byte Ed25519 secret key.
303
+ * @returns {{ publicKey: PublicKey, signAllTransactions: Function, signMessage: Function }}
304
+ */
305
+ static fromSecretKey(secretKey) {
306
+ if (!(secretKey instanceof Uint8Array) || secretKey.length !== 64) {
307
+ throw new Error("NullTrace.fromSecretKey: expected a 64-byte Uint8Array");
308
+ }
309
+ return _NullTrace.fromKeypair(Keypair.fromSecretKey(secretKey));
310
+ }
311
+ /**
312
+ * Create a wallet adapter interface from a base58-encoded private key string.
313
+ *
314
+ * @param {string} base58Key A base58-encoded private key (as exported by Phantom, Solflare, etc.).
315
+ * @returns {{ publicKey: PublicKey, signAllTransactions: Function, signMessage: Function }}
316
+ */
317
+ static fromPrivateKey(base58Key) {
318
+ if (typeof base58Key !== "string" || base58Key.length < 32) {
319
+ throw new Error("NullTrace.fromPrivateKey: expected a base58-encoded private key string");
320
+ }
321
+ const decoded = bs58.decode(base58Key);
322
+ return _NullTrace.fromKeypair(Keypair.fromSecretKey(decoded));
323
+ }
324
+ /**
325
+ * @internal Resolve any supported wallet input into a wallet adapter interface.
326
+ */
327
+ static _resolveWallet(input) {
328
+ if (input?.publicKey && typeof input.signAllTransactions === "function") {
329
+ return input;
330
+ }
331
+ if (input instanceof Keypair) {
332
+ return _NullTrace.fromKeypair(input);
333
+ }
334
+ if (input instanceof Uint8Array && input.length === 64) {
335
+ return _NullTrace.fromSecretKey(input);
336
+ }
337
+ if (typeof input === "string") {
338
+ return _NullTrace.fromPrivateKey(input);
339
+ }
340
+ throw new Error(
341
+ "NullTrace: unsupported wallet type. Provide a wallet adapter, Keypair, 64-byte Uint8Array, or base58 private key string."
342
+ );
343
+ }
344
+ /** @internal Get the address lookup table (cached). */
345
+ async _getAlt() {
346
+ if (this._adlCache)
347
+ return this._adlCache;
348
+ const result = await this.connection.getAddressLookupTable(ALT_ADDRESS);
349
+ this._adlCache = result.value;
350
+ return this._adlCache;
351
+ }
352
+ // -----------------------------------------------------------------------
353
+ // Nullify (public -> private)
354
+ // -----------------------------------------------------------------------
355
+ /**
356
+ * Convert public tokens into private ZK-compressed state.
357
+ *
358
+ * @param {string} mint Token mint address (use NATIVE_MINT for SOL).
359
+ * @param {string} amount Human-readable amount (e.g. "1.5").
360
+ * @returns {Promise<string[]>} Transaction signatures.
361
+ *
362
+ * @example
363
+ * const sigs = await nt.nullify('So11111111111111111111111111111111111111112', '0.5');
364
+ */
365
+ async nullify(mint, amount) {
366
+ if (!mint || !amount)
367
+ throw new Error("NullTrace.nullify: mint and amount are required");
368
+ const owner = this.wallet.publicKey;
369
+ const isSOL = mint === NATIVE_MINT.toBase58();
370
+ const { decimals, tokenProgram } = await _getMintInfo(this.connection, mint);
371
+ const amountLamports = bn(Math.floor(parseFloat(amount) * 10 ** decimals).toString());
372
+ const feeLamports = bn(Math.floor(parseInt(amountLamports.toString()) * FEE_BPS).toString());
373
+ const ixs = [];
374
+ const activeStateTrees = await this.connection.getStateTreeInfos();
375
+ const tree = selectStateTreeInfo(activeStateTrees);
376
+ if (isSOL) {
377
+ ixs.push(
378
+ await LightSystemProgram.compress({
379
+ payer: owner,
380
+ toAddress: owner,
381
+ lamports: amountLamports.sub(feeLamports),
382
+ outputStateTreeInfo: tree
383
+ })
384
+ );
385
+ ixs.push(
386
+ SystemProgram.transfer({
387
+ fromPubkey: owner,
388
+ toPubkey: OPERATOR_PUBLIC_KEY,
389
+ lamports: feeLamports
390
+ })
391
+ );
392
+ } else {
393
+ const mintPk = new PublicKey(mint);
394
+ const sourceAta = await getAssociatedTokenAddress(mintPk, owner, false, tokenProgram);
395
+ const [tokenPoolPda] = PublicKey.findProgramAddressSync(
396
+ [Buffer.from("pool"), mintPk.toBuffer()],
397
+ COMPRESSED_TOKEN_PROGRAM_ID
398
+ );
399
+ const poolInfo = await this.connection.getAccountInfo(tokenPoolPda);
400
+ if (!poolInfo) {
401
+ ixs.push(
402
+ await CompressedTokenProgram.createTokenPool({
403
+ feePayer: owner,
404
+ mint: mintPk,
405
+ tokenProgramId: tokenProgram
406
+ })
407
+ );
408
+ }
409
+ ixs.push(
410
+ await CompressedTokenProgram.compress({
411
+ payer: owner,
412
+ owner,
413
+ source: sourceAta,
414
+ toAddress: owner,
415
+ amount: amountLamports.sub(feeLamports),
416
+ mint: mintPk,
417
+ outputStateTreeInfo: tree,
418
+ tokenPoolInfo: {
419
+ tokenPoolPda,
420
+ tokenProgram,
421
+ isInitialized: true,
422
+ balance: bn("0"),
423
+ poolIndex: 0,
424
+ mint: mintPk
425
+ }
426
+ })
427
+ );
428
+ const operatorAta = await getAssociatedTokenAddress(mintPk, OPERATOR_PUBLIC_KEY, false, tokenProgram);
429
+ const operatorInfo = await this.connection.getAccountInfo(operatorAta);
430
+ if (!operatorInfo) {
431
+ ixs.push(createAssociatedTokenAccountInstruction(owner, operatorAta, OPERATOR_PUBLIC_KEY, mintPk, tokenProgram));
432
+ }
433
+ ixs.push(
434
+ createTransferCheckedInstruction(sourceAta, mintPk, operatorAta, owner, feeLamports, decimals, [], tokenProgram)
435
+ );
436
+ }
437
+ const adl = await this._getAlt();
438
+ const txs = await _packTransactions(this.connection, owner, ixs, adl);
439
+ return _signSendConfirm(this.connection, this.wallet, txs);
440
+ }
441
+ // -----------------------------------------------------------------------
442
+ // Reveal (private -> public)
443
+ // -----------------------------------------------------------------------
444
+ /**
445
+ * Decompress private tokens back to public state.
446
+ *
447
+ * @param {string} mint Token mint address.
448
+ * @param {string} amount Human-readable amount.
449
+ * @returns {Promise<string[]>} Transaction signatures.
450
+ *
451
+ * @example
452
+ * const sigs = await nt.reveal('So11111111111111111111111111111111111111112', '0.5');
453
+ */
454
+ async reveal(mint, amount) {
455
+ if (!mint || !amount)
456
+ throw new Error("NullTrace.reveal: mint and amount are required");
457
+ const owner = this.wallet.publicKey;
458
+ const isSOL = mint === NATIVE_MINT.toBase58();
459
+ const { decimals, tokenProgram } = await _getMintInfo(this.connection, mint);
460
+ const amountLamports = Math.floor(parseFloat(amount) * 10 ** decimals);
461
+ const sorted = await _getCompressedAccounts(this.connection, owner, mint, isSOL);
462
+ const { selected, total } = _selectInputs(sorted, amountLamports, isSOL);
463
+ if (total < amountLamports)
464
+ throw new Error("Insufficient private balance");
465
+ const batches = _batchAccounts(selected);
466
+ const ixs = [];
467
+ let selectedTokenPoolInfos;
468
+ let destinationAta;
469
+ if (!isSOL) {
470
+ const tokenPoolInfos = await getTokenPoolInfos(this.connection, new PublicKey(mint));
471
+ selectedTokenPoolInfos = selectTokenPoolInfosForDecompression(tokenPoolInfos, amountLamports);
472
+ destinationAta = await getAssociatedTokenAddress(new PublicKey(mint), owner, false, tokenProgram);
473
+ const info = await this.connection.getAccountInfo(destinationAta);
474
+ if (!info) {
475
+ ixs.push(createAssociatedTokenAccountInstruction(owner, destinationAta, owner, new PublicKey(mint), tokenProgram));
476
+ }
477
+ }
478
+ let remaining = amountLamports;
479
+ for (const batch of batches) {
480
+ const proof = await this.connection.getValidityProofV0(
481
+ batch.map((a) => ({
482
+ hash: a.hash ?? a.compressedAccount?.hash,
483
+ tree: a.treeInfo?.tree ?? a.compressedAccount?.treeInfo?.tree,
484
+ queue: a.treeInfo?.queue ?? a.compressedAccount?.treeInfo?.queue
485
+ }))
486
+ );
487
+ const batchAmount = decimals === 0 ? 1 : Math.min(remaining, batch.reduce((s, a) => s + Number(isSOL ? a.lamports : a.parsed.amount), 0));
488
+ ixs.push(
489
+ await (isSOL ? LightSystemProgram.decompress({
490
+ payer: owner,
491
+ inputCompressedAccounts: batch,
492
+ toAddress: owner,
493
+ lamports: bn(batchAmount.toString()),
494
+ recentInputStateRootIndices: proof.rootIndices,
495
+ recentValidityProof: proof.compressedProof
496
+ }) : CompressedTokenProgram.decompress({
497
+ payer: owner,
498
+ inputCompressedTokenAccounts: batch,
499
+ toAddress: destinationAta,
500
+ amount: bn(batchAmount.toString()),
501
+ recentInputStateRootIndices: proof.rootIndices,
502
+ recentValidityProof: proof.compressedProof,
503
+ tokenPoolInfos: selectedTokenPoolInfos
504
+ }))
505
+ );
506
+ remaining -= batchAmount;
507
+ }
508
+ const adl = await this._getAlt();
509
+ const txs = await _packTransactions(this.connection, owner, ixs, adl);
510
+ return _signSendConfirm(this.connection, this.wallet, txs);
511
+ }
512
+ // -----------------------------------------------------------------------
513
+ // Transfer (private -> private)
514
+ // -----------------------------------------------------------------------
515
+ /**
516
+ * Send compressed tokens privately to another Solana address.
517
+ *
518
+ * @param {string} mint Token mint address.
519
+ * @param {string} amount Human-readable amount.
520
+ * @param {string} recipient Recipient's Solana public key.
521
+ * @returns {Promise<string[]>} Transaction signatures.
522
+ *
523
+ * @example
524
+ * const sigs = await nt.transfer('So11...', '1.0', 'Recip1ent...');
525
+ */
526
+ async transfer(mint, amount, recipient) {
527
+ if (!mint || !amount || !recipient) {
528
+ throw new Error("NullTrace.transfer: mint, amount, and recipient are required");
529
+ }
530
+ const owner = this.wallet.publicKey;
531
+ const recipientPk = new PublicKey(recipient);
532
+ const isSOL = mint === NATIVE_MINT.toBase58();
533
+ const { decimals, tokenProgram } = await _getMintInfo(this.connection, mint);
534
+ const amountLamports = Math.floor(parseFloat(amount) * 10 ** decimals);
535
+ const sorted = await _getCompressedAccounts(this.connection, owner, mint, isSOL);
536
+ const { selected, total } = _selectInputs(sorted, amountLamports, isSOL);
537
+ const { blockhash } = await this.connection.getLatestBlockhash();
538
+ const adl = await this._getAlt();
539
+ const preTransactions = [];
540
+ if (total < amountLamports) {
541
+ const deficit = amountLamports - total;
542
+ const fee = bn(Math.floor(deficit * FEE_BPS).toString());
543
+ const trees = await this.connection.getStateTreeInfos();
544
+ const tree = selectStateTreeInfo(trees);
545
+ if (isSOL) {
546
+ const solBal = await this.connection.getBalance(owner);
547
+ if (solBal < deficit + 1e5)
548
+ throw new Error("Insufficient balance");
549
+ const compressIx = await LightSystemProgram.compress({
550
+ payer: owner,
551
+ toAddress: OPERATOR_PUBLIC_KEY,
552
+ lamports: bn(deficit.toString()).sub(fee),
553
+ outputStateTreeInfo: tree
554
+ });
555
+ const feeIx = SystemProgram.transfer({
556
+ fromPubkey: owner,
557
+ toPubkey: OPERATOR_PUBLIC_KEY,
558
+ lamports: fee
559
+ });
560
+ const msg = new TransactionMessage({
561
+ payerKey: owner,
562
+ recentBlockhash: blockhash,
563
+ instructions: [
564
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
565
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_PRICE }),
566
+ compressIx,
567
+ feeIx
568
+ ]
569
+ }).compileToV0Message([adl]);
570
+ preTransactions.push(new VersionedTransaction(msg));
571
+ } else {
572
+ let instructions = [
573
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
574
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_PRICE })
575
+ ];
576
+ const sourceAta = await getAssociatedTokenAddress(
577
+ new PublicKey(mint),
578
+ owner,
579
+ false,
580
+ tokenProgram
581
+ );
582
+ const tokenAccountInfos = await this.connection.getParsedTokenAccountsByOwner(
583
+ owner,
584
+ { programId: tokenProgram, mint: new PublicKey(mint) },
585
+ "processed"
586
+ );
587
+ const publicBalance = tokenAccountInfos.value?.[0].account.data.parsed.info.tokenAmount.amount ?? 0;
588
+ if (publicBalance < deficit)
589
+ throw new Error("Insufficient balance");
590
+ const [tokenPoolPda] = PublicKey.findProgramAddressSync(
591
+ [Buffer.from("pool"), new PublicKey(mint).toBuffer()],
592
+ COMPRESSED_TOKEN_PROGRAM_ID
593
+ );
594
+ const tokenPoolInfo = await this.connection.getAccountInfo(tokenPoolPda, "processed");
595
+ if (!tokenPoolInfo) {
596
+ const createTokenPoolIx = await CompressedTokenProgram.createTokenPool({
597
+ feePayer: owner,
598
+ mint: new PublicKey(mint),
599
+ tokenProgramId: tokenProgram
600
+ });
601
+ instructions.push(createTokenPoolIx);
602
+ }
603
+ const compressInstruction = await CompressedTokenProgram.compress({
604
+ payer: owner,
605
+ owner,
606
+ source: sourceAta,
607
+ toAddress: recipientPk,
608
+ amount: bn(deficit.toString()).sub(fee),
609
+ mint: new PublicKey(mint),
610
+ outputStateTreeInfo: tree,
611
+ tokenPoolInfo: {
612
+ tokenPoolPda,
613
+ tokenProgram,
614
+ isInitialized: true,
615
+ balance: bn("0"),
616
+ poolIndex: 0,
617
+ mint: new PublicKey(mint)
618
+ }
619
+ });
620
+ instructions.push(compressInstruction);
621
+ const operatorTokenAccount = await getAssociatedTokenAddress(new PublicKey(mint), OPERATOR_PUBLIC_KEY, false, tokenProgram);
622
+ const operatorPoolInfo = await this.connection.getAccountInfo(operatorTokenAccount);
623
+ if (!operatorPoolInfo) {
624
+ instructions.push(createAssociatedTokenAccountInstruction(owner, operatorTokenAccount, OPERATOR_PUBLIC_KEY, new PublicKey(mint), tokenProgram));
625
+ }
626
+ instructions.push(createTransferCheckedInstruction(
627
+ new PublicKey(sourceAta),
628
+ new PublicKey(mint),
629
+ new PublicKey(operatorTokenAccount),
630
+ owner,
631
+ fee,
632
+ decimals,
633
+ [],
634
+ tokenProgram
635
+ ));
636
+ let tx = new VersionedTransaction(new TransactionMessage({
637
+ payerKey: owner,
638
+ recentBlockhash: blockhash,
639
+ instructions
640
+ }).compileToV0Message([adl]));
641
+ preTransactions.push(tx);
642
+ }
643
+ }
644
+ if (total < amountLamports && preTransactions.length === 0) {
645
+ throw new Error("Insufficient balance");
646
+ }
647
+ const batches = _batchAccounts(selected);
648
+ let remaining = amountLamports;
649
+ const ixs = [];
650
+ for (const batch of batches) {
651
+ const proof = await this.connection.getValidityProofV0(
652
+ batch.map((a) => ({
653
+ hash: a.hash ?? a.compressedAccount?.hash,
654
+ tree: a.treeInfo?.tree ?? a.compressedAccount?.treeInfo?.tree,
655
+ queue: a.treeInfo?.queue ?? a.compressedAccount?.treeInfo?.queue
656
+ }))
657
+ );
658
+ const batchAmount = decimals === 0 ? 1 : Math.min(remaining, batch.reduce((s, a) => s + Number(isSOL ? a.lamports : a.parsed.amount), 0));
659
+ ixs.push(
660
+ await (isSOL ? LightSystemProgram.transfer({
661
+ payer: owner,
662
+ inputCompressedAccounts: batch,
663
+ toAddress: recipientPk,
664
+ lamports: bn(batchAmount.toString()),
665
+ recentInputStateRootIndices: proof.rootIndices,
666
+ recentValidityProof: proof.compressedProof
667
+ }) : CompressedTokenProgram.transfer({
668
+ payer: owner,
669
+ inputCompressedTokenAccounts: batch,
670
+ toAddress: recipientPk,
671
+ amount: bn(batchAmount.toString()),
672
+ recentInputStateRootIndices: proof.rootIndices,
673
+ recentValidityProof: proof.compressedProof
674
+ }))
675
+ );
676
+ remaining -= batchAmount;
677
+ }
678
+ const txs = await _packTransactions(this.connection, owner, ixs, adl);
679
+ const allTxs = [...preTransactions, ...txs];
680
+ return _signSendConfirm(this.connection, this.wallet, allTxs);
681
+ }
682
+ // -----------------------------------------------------------------------
683
+ // Swap (private swap via operator)
684
+ // -----------------------------------------------------------------------
685
+ /**
686
+ * Get a swap quote. No signing required.
687
+ *
688
+ * @param {string} inputMint Input token mint.
689
+ * @param {string} outputMint Output token mint.
690
+ * @param {string} amount Human-readable input amount.
691
+ * @returns {Promise<{inAmount: string, outAmount: string, priceImpact: number}>}
692
+ *
693
+ * @example
694
+ * const quote = await nt.quoteSwap('So11...', 'Es9v...', '1.0');
695
+ */
696
+ async quoteSwap(inputMint, outputMint, amount) {
697
+ if (!inputMint || !outputMint || !amount) {
698
+ throw new Error("NullTrace.quoteSwap: inputMint, outputMint, and amount are required");
699
+ }
700
+ const { decimals } = await _getMintInfo(this.connection, inputMint);
701
+ const amountLamports = Math.floor(parseFloat(amount) * 10 ** decimals);
702
+ const res = await fetch(`${REMOTE_OPERATOR_URL}/operator/quote-swap`, {
703
+ method: "POST",
704
+ headers: {
705
+ "Content-Type": "application/json",
706
+ "x-null-client-secret": _getAuthToken()
707
+ },
708
+ body: JSON.stringify({ inputMint, outputMint, amount: amountLamports })
709
+ });
710
+ if (!res.ok) {
711
+ const err = await res.json().catch(() => ({}));
712
+ throw new Error(err.error || `Quote failed: ${res.status}`);
713
+ }
714
+ return res.json();
715
+ }
716
+ /**
717
+ * Execute a private swap via the NullTrace operator.
718
+ *
719
+ * @param {string} fromMint Input token mint.
720
+ * @param {string} toMint Output token mint.
721
+ * @param {string} amount Human-readable input amount.
722
+ * @param {Object} [options]
723
+ * @param {(status: string) => void} [options.onStatusChange]
724
+ * @param {number} [options.timeout=120000]
725
+ * @returns {Promise<{status: string, result: Object}>}
726
+ *
727
+ * @example
728
+ * const result = await nt.swap('So11...', 'Es9v...', '1.0');
729
+ */
730
+ async swap(fromMint, toMint, amount, options = {}) {
731
+ if (!fromMint || !toMint || !amount) {
732
+ throw new Error("NullTrace.swap: fromMint, toMint, and amount are required");
733
+ }
734
+ const { onStatusChange, timeout = 12e4 } = options;
735
+ const owner = this.wallet.publicKey;
736
+ const isSOL = fromMint === NATIVE_MINT.toBase58();
737
+ const { decimals, tokenProgram } = await _getMintInfo(this.connection, fromMint);
738
+ const amountLamports = Math.floor(parseFloat(amount) * 10 ** decimals);
739
+ const sorted = await _getCompressedAccounts(this.connection, owner, fromMint, isSOL);
740
+ const { selected, total } = _selectInputs(sorted, amountLamports, isSOL);
741
+ const { blockhash } = await this.connection.getLatestBlockhash();
742
+ const adl = await this._getAlt();
743
+ const preTransactions = [];
744
+ if (total < amountLamports) {
745
+ const deficit = amountLamports - total;
746
+ const fee = bn(Math.floor(deficit * FEE_BPS).toString());
747
+ const trees = await this.connection.getStateTreeInfos();
748
+ const tree = selectStateTreeInfo(trees);
749
+ if (isSOL) {
750
+ const solBal = await this.connection.getBalance(owner);
751
+ if (solBal < deficit + 1e5)
752
+ throw new Error("Insufficient balance");
753
+ const compressIx = await LightSystemProgram.compress({
754
+ payer: owner,
755
+ toAddress: OPERATOR_PUBLIC_KEY,
756
+ lamports: bn(deficit.toString()).sub(fee),
757
+ outputStateTreeInfo: tree
758
+ });
759
+ const feeIx = SystemProgram.transfer({
760
+ fromPubkey: owner,
761
+ toPubkey: OPERATOR_PUBLIC_KEY,
762
+ lamports: fee
763
+ });
764
+ const msg = new TransactionMessage({
765
+ payerKey: owner,
766
+ recentBlockhash: blockhash,
767
+ instructions: [
768
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
769
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_PRICE }),
770
+ compressIx,
771
+ feeIx
772
+ ]
773
+ }).compileToV0Message([adl]);
774
+ preTransactions.push(new VersionedTransaction(msg));
775
+ } else {
776
+ let instructions = [
777
+ ComputeBudgetProgram.setComputeUnitLimit({ units: COMPUTE_UNITS }),
778
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: COMPUTE_PRICE })
779
+ ];
780
+ const sourceAta = await getAssociatedTokenAddress(
781
+ new PublicKey(fromMint),
782
+ owner,
783
+ false,
784
+ tokenProgram
785
+ );
786
+ const tokenAccountInfos = await this.connection.getParsedTokenAccountsByOwner(
787
+ owner,
788
+ { programId: tokenProgram, mint: new PublicKey(fromMint) },
789
+ "processed"
790
+ );
791
+ const publicBalance = tokenAccountInfos.value?.[0].account.data.parsed.info.tokenAmount.amount ?? 0;
792
+ if (publicBalance < deficit)
793
+ throw new Error("Insufficient balance");
794
+ const [tokenPoolPda] = PublicKey.findProgramAddressSync(
795
+ [Buffer.from("pool"), new PublicKey(fromMint).toBuffer()],
796
+ COMPRESSED_TOKEN_PROGRAM_ID
797
+ );
798
+ const tokenPoolInfo = await this.connection.getAccountInfo(tokenPoolPda, "processed");
799
+ if (!tokenPoolInfo) {
800
+ const createTokenPoolIx = await CompressedTokenProgram.createTokenPool({
801
+ feePayer: owner,
802
+ mint: new PublicKey(fromMint),
803
+ tokenProgramId: tokenProgram
804
+ });
805
+ instructions.push(createTokenPoolIx);
806
+ }
807
+ const compressInstruction = await CompressedTokenProgram.compress({
808
+ payer: owner,
809
+ owner,
810
+ source: sourceAta,
811
+ toAddress: OPERATOR_PUBLIC_KEY,
812
+ amount: bn(deficit.toString()).sub(fee),
813
+ mint: new PublicKey(fromMint),
814
+ outputStateTreeInfo: tree,
815
+ tokenPoolInfo: {
816
+ tokenPoolPda,
817
+ tokenProgram,
818
+ isInitialized: true,
819
+ balance: bn("0"),
820
+ poolIndex: 0,
821
+ mint: new PublicKey(fromMint)
822
+ }
823
+ });
824
+ instructions.push(compressInstruction);
825
+ const operatorTokenAccount = await getAssociatedTokenAddress(new PublicKey(fromMint), OPERATOR_PUBLIC_KEY, false, tokenProgram);
826
+ const operatorPoolInfo = await this.connection.getAccountInfo(operatorTokenAccount);
827
+ if (!operatorPoolInfo) {
828
+ instructions.push(createAssociatedTokenAccountInstruction(owner, operatorTokenAccount, OPERATOR_PUBLIC_KEY, new PublicKey(fromMint), tokenProgram));
829
+ }
830
+ instructions.push(createTransferCheckedInstruction(
831
+ new PublicKey(sourceAta),
832
+ new PublicKey(fromMint),
833
+ new PublicKey(operatorTokenAccount),
834
+ owner,
835
+ fee,
836
+ decimals,
837
+ [],
838
+ tokenProgram
839
+ ));
840
+ let tx = new VersionedTransaction(new TransactionMessage({
841
+ payerKey: owner,
842
+ recentBlockhash: blockhash,
843
+ instructions
844
+ }).compileToV0Message([adl]));
845
+ preTransactions.push(tx);
846
+ }
847
+ }
848
+ if (total < amountLamports && preTransactions.length === 0) {
849
+ throw new Error("Insufficient balance");
850
+ }
851
+ const batches = _batchAccounts(selected);
852
+ const ixs = [];
853
+ let remaining = amountLamports;
854
+ for (const batch of batches) {
855
+ const proof = await this.connection.getValidityProofV0(
856
+ batch.map((a) => ({
857
+ hash: a.hash ?? a.compressedAccount?.hash,
858
+ tree: a.treeInfo?.tree ?? a.compressedAccount?.treeInfo?.tree,
859
+ queue: a.treeInfo?.queue ?? a.compressedAccount?.treeInfo?.queue
860
+ }))
861
+ );
862
+ const batchAmount = decimals === 0 ? 1 : Math.min(remaining, batch.reduce((s, a) => s + Number(isSOL ? a.lamports : a.parsed.amount), 0));
863
+ ixs.push(
864
+ await (isSOL ? LightSystemProgram.transfer({
865
+ payer: owner,
866
+ inputCompressedAccounts: batch,
867
+ toAddress: OPERATOR_PUBLIC_KEY,
868
+ lamports: bn(batchAmount.toString()),
869
+ recentInputStateRootIndices: proof.rootIndices,
870
+ recentValidityProof: proof.compressedProof
871
+ }) : CompressedTokenProgram.transfer({
872
+ payer: owner,
873
+ inputCompressedTokenAccounts: batch,
874
+ toAddress: OPERATOR_PUBLIC_KEY,
875
+ amount: bn(batchAmount.toString()),
876
+ recentInputStateRootIndices: proof.rootIndices,
877
+ recentValidityProof: proof.compressedProof
878
+ }))
879
+ );
880
+ remaining -= batchAmount;
881
+ }
882
+ const transferTxs = await _packTransactions(this.connection, owner, ixs, adl);
883
+ const allTxs = [...preTransactions, ...transferTxs];
884
+ if (onStatusChange)
885
+ onStatusChange("signing");
886
+ const signed = await this.wallet.signAllTransactions(allTxs);
887
+ const signedBase64 = signed.map((tx) => {
888
+ const bytes = tx.serialize();
889
+ return typeof Buffer !== "undefined" ? Buffer.from(bytes).toString("base64") : btoa(String.fromCharCode(...bytes));
890
+ });
891
+ const preSigs = [];
892
+ for (let i = 0; i < preTransactions.length; i++) {
893
+ const sig = await this.connection.sendRawTransaction(signed[i].serialize());
894
+ await this.connection.confirmTransaction(sig);
895
+ preSigs.push(sig);
896
+ }
897
+ const swapId = Keypair.generate().publicKey.toString();
898
+ const swapData = {
899
+ id: swapId,
900
+ fromToken: fromMint,
901
+ toToken: toMint,
902
+ amount,
903
+ amountValue: amountLamports,
904
+ fromTokenDecimals: decimals,
905
+ userPublicKey: owner.toString(),
906
+ recipient: OPERATOR_PUBLIC_KEY.toString(),
907
+ status: "initialized",
908
+ created: Date.now()
909
+ };
910
+ if (onStatusChange)
911
+ onStatusChange("processing");
912
+ const transferBase64 = signedBase64.slice(preTransactions.length);
913
+ const execRes = await fetch(`${REMOTE_OPERATOR_URL}/operator/process-swap`, {
914
+ method: "POST",
915
+ headers: {
916
+ "Content-Type": "application/json",
917
+ "x-null-client-secret": _getAuthToken()
918
+ },
919
+ body: JSON.stringify({ swapData, signedTransferData: transferBase64 })
920
+ });
921
+ if (!execRes.ok) {
922
+ const err = await execRes.json().catch(() => ({}));
923
+ throw new Error(err.error || `Swap submission failed: ${execRes.status}`);
924
+ }
925
+ const execData = await execRes.json();
926
+ if (execData.status === "completed") {
927
+ if (onStatusChange)
928
+ onStatusChange("completed");
929
+ return { status: "completed", result: execData };
930
+ }
931
+ const deadline = Date.now() + timeout;
932
+ while (Date.now() < deadline) {
933
+ await _sleep(2e3);
934
+ if (onStatusChange)
935
+ onStatusChange("pending");
936
+ }
937
+ return { status: "pending", swapId, result: execData };
938
+ }
939
+ // -----------------------------------------------------------------------
940
+ // Balances
941
+ // -----------------------------------------------------------------------
942
+ /**
943
+ * Get public (on-chain) token balances.
944
+ *
945
+ * @returns {Promise<Array<{symbol: string, name: string, amount: string, lamports: number, decimals: number, address: string}>>}
946
+ */
947
+ async getPublicBalances() {
948
+ const owner = this.wallet.publicKey;
949
+ const tokenBalances = [];
950
+ const solBal = await this.connection.getBalance(owner);
951
+ if (solBal > 0) {
952
+ tokenBalances.push({
953
+ symbol: "SOL",
954
+ name: "Solana",
955
+ amount: (solBal / 1e9 - 0.01).toString(),
956
+ lamports: solBal - 0.01 * 1e9,
957
+ decimals: 9,
958
+ logo: "",
959
+ address: NATIVE_MINT.toString()
960
+ });
961
+ }
962
+ const [spl, spl22] = await Promise.all([
963
+ this.connection.getParsedTokenAccountsByOwner(owner, { programId: TOKEN_PROGRAM_ID }, "processed"),
964
+ this.connection.getParsedTokenAccountsByOwner(owner, { programId: TOKEN_2022_PROGRAM_ID }, "processed")
965
+ ]);
966
+ for (const ta of [...spl.value, ...spl22.value]) {
967
+ const p = ta.account.data.parsed;
968
+ if (p.info.tokenAmount.amount === "0")
969
+ continue;
970
+ tokenBalances.push({
971
+ symbol: "",
972
+ name: "",
973
+ amount: (parseInt(p.info.tokenAmount.amount) / 10 ** p.info.tokenAmount.decimals).toString(),
974
+ lamports: parseInt(p.info.tokenAmount.amount),
975
+ decimals: p.info.tokenAmount.decimals,
976
+ logo: "",
977
+ address: p.info.mint,
978
+ dexscreener: ""
979
+ });
980
+ }
981
+ return _enrichMetadata(tokenBalances);
982
+ }
983
+ /**
984
+ * Get private (ZK-compressed) token balances.
985
+ * Requires `wallet.signMessage`.
986
+ *
987
+ * @returns {Promise<Array<{symbol: string, name: string, amount: string, lamports: number, decimals: number, address: string}>>}
988
+ */
989
+ async getPrivateBalances() {
990
+ if (!this.wallet.signMessage) {
991
+ throw new Error("NullTrace: wallet.signMessage is required for getPrivateBalances");
992
+ }
993
+ const owner = this.wallet.publicKey;
994
+ if (!this._sigCache) {
995
+ const msg = new TextEncoder().encode("Reveal Private Balances");
996
+ this._sigCache = await this.wallet.signMessage(msg);
997
+ }
998
+ const tokenBalances = [];
999
+ const compressedSol = await this.connection.getCompressedBalanceByOwner(owner);
1000
+ if (parseInt(compressedSol.toString()) > 0) {
1001
+ tokenBalances.push({
1002
+ symbol: "SOL",
1003
+ name: "Solana",
1004
+ amount: (parseInt(compressedSol.toString()) / 1e9).toString(),
1005
+ lamports: parseInt(compressedSol.toString()),
1006
+ decimals: 9,
1007
+ logo: "",
1008
+ address: NATIVE_MINT.toString()
1009
+ });
1010
+ }
1011
+ const compressedTokens = await this.connection.getCompressedTokenAccountsByOwner(owner);
1012
+ for (const item of compressedTokens.items) {
1013
+ const mintAddr = item.parsed.mint.toString();
1014
+ const amt = bn(item.parsed.amount.toString());
1015
+ let entry = tokenBalances.find((t) => t.address === mintAddr);
1016
+ if (!entry) {
1017
+ entry = { symbol: "", name: "", amount: "0", lamports: 0, decimals: 0, logo: "", address: mintAddr };
1018
+ tokenBalances.push(entry);
1019
+ }
1020
+ entry.lamports += parseInt(amt.toString());
1021
+ }
1022
+ return _enrichMetadata(tokenBalances.filter((t) => t.lamports > 0));
1023
+ }
1024
+ /**
1025
+ * Get all balances merged (public + private) with `publicAmount` and `privateAmount` fields.
1026
+ * Requires `wallet.signMessage`.
1027
+ *
1028
+ * @returns {Promise<Array<{symbol: string, name: string, amount: number, publicAmount: number, privateAmount: number, address: string}>>}
1029
+ */
1030
+ async getBalances() {
1031
+ const [pub, priv] = await Promise.all([this.getPublicBalances(), this.getPrivateBalances()]);
1032
+ const merged = pub.map((t) => ({
1033
+ ...t,
1034
+ publicAmount: parseFloat(t.amount),
1035
+ privateAmount: 0,
1036
+ amount: parseFloat(t.amount)
1037
+ }));
1038
+ for (const token of priv) {
1039
+ const existing = merged.find((t) => t.address === token.address);
1040
+ if (existing) {
1041
+ existing.privateAmount += parseFloat(token.amount);
1042
+ existing.amount += parseFloat(token.amount);
1043
+ } else {
1044
+ merged.push({ ...token, publicAmount: 0, privateAmount: parseFloat(token.amount), amount: parseFloat(token.amount) });
1045
+ }
1046
+ }
1047
+ return merged;
1048
+ }
1049
+ /**
1050
+ * Fetch metadata for a token (symbol, name, logo, decimals).
1051
+ *
1052
+ * @param {string} mint Token mint address.
1053
+ * @returns {Promise<{symbol: string, name: string, logo: string, decimals: number}>}
1054
+ */
1055
+ async getTokenMetadata(mint) {
1056
+ if (!mint)
1057
+ throw new Error("NullTrace.getTokenMetadata: mint is required");
1058
+ if (mint === NATIVE_MINT.toBase58()) {
1059
+ return { symbol: "SOL", name: "Solana", logo: "", decimals: 9 };
1060
+ }
1061
+ const result = [{ address: mint, symbol: "", name: "", logo: "", decimals: 0, lamports: 0 }];
1062
+ await _enrichMetadata(result);
1063
+ return result[0];
1064
+ }
1065
+ /** Clear cached message signature. Next getPrivateBalances will re-prompt. */
1066
+ clearSignatureCache() {
1067
+ this._sigCache = null;
1068
+ }
1069
+ };
1070
+ var src_default = NullTrace;
1071
+ export {
1072
+ NullTrace,
1073
+ src_default as default
1074
+ };