@sip-protocol/sdk 0.7.0 → 0.7.2

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.
@@ -0,0 +1,1495 @@
1
+ import {
2
+ ValidationError
3
+ } from "./chunk-6WGN57S2.mjs";
4
+ import {
5
+ ESTIMATED_TX_FEE_LAMPORTS,
6
+ MEMO_PROGRAM_ID,
7
+ SIP_MEMO_PREFIX,
8
+ SOLANA_TOKEN_MINTS,
9
+ getExplorerUrl
10
+ } from "./chunk-E6SZWREQ.mjs";
11
+
12
+ // src/chains/solana/types.ts
13
+ function parseAnnouncement(memo) {
14
+ if (!memo.startsWith("SIP:1:")) {
15
+ return null;
16
+ }
17
+ const parts = memo.slice(6).split(":");
18
+ if (parts.length < 2) {
19
+ return null;
20
+ }
21
+ return {
22
+ ephemeralPublicKey: parts[0],
23
+ viewTag: parts[1],
24
+ stealthAddress: parts[2]
25
+ };
26
+ }
27
+ function createAnnouncementMemo(ephemeralPublicKey, viewTag, stealthAddress) {
28
+ const parts = ["SIP:1", ephemeralPublicKey, viewTag];
29
+ if (stealthAddress) {
30
+ parts.push(stealthAddress);
31
+ }
32
+ return parts.join(":");
33
+ }
34
+
35
+ // src/chains/solana/transfer.ts
36
+ import {
37
+ PublicKey,
38
+ Transaction,
39
+ TransactionInstruction
40
+ } from "@solana/web3.js";
41
+ import {
42
+ getAssociatedTokenAddress,
43
+ createAssociatedTokenAccountInstruction,
44
+ createTransferInstruction,
45
+ TOKEN_PROGRAM_ID,
46
+ ASSOCIATED_TOKEN_PROGRAM_ID,
47
+ getAccount
48
+ } from "@solana/spl-token";
49
+
50
+ // src/stealth.ts
51
+ import { secp256k1 } from "@noble/curves/secp256k1";
52
+ import { ed25519 } from "@noble/curves/ed25519";
53
+ import { sha256 } from "@noble/hashes/sha256";
54
+ import { sha512 } from "@noble/hashes/sha512";
55
+ import { keccak_256 } from "@noble/hashes/sha3";
56
+ import { bytesToHex, hexToBytes, randomBytes as randomBytes2 } from "@noble/hashes/utils";
57
+
58
+ // src/validation.ts
59
+ var VALID_CHAIN_IDS = [
60
+ "solana",
61
+ "ethereum",
62
+ "near",
63
+ "zcash",
64
+ "polygon",
65
+ "arbitrum",
66
+ "optimism",
67
+ "base",
68
+ "bitcoin",
69
+ "aptos",
70
+ "sui",
71
+ "cosmos",
72
+ "osmosis",
73
+ "injective",
74
+ "celestia",
75
+ "sei",
76
+ "dydx"
77
+ ];
78
+ function isValidChainId(chain) {
79
+ return VALID_CHAIN_IDS.includes(chain);
80
+ }
81
+ function isValidPrivacyLevel(level) {
82
+ if (typeof level !== "string") return false;
83
+ return ["transparent", "shielded", "compliant"].includes(level);
84
+ }
85
+ function isValidHex(value) {
86
+ if (typeof value !== "string") return false;
87
+ if (!value.startsWith("0x")) return false;
88
+ const hex = value.slice(2);
89
+ if (hex.length === 0) return false;
90
+ return /^[0-9a-fA-F]+$/.test(hex);
91
+ }
92
+ function isValidHexLength(value, byteLength) {
93
+ if (!isValidHex(value)) return false;
94
+ const hex = value.slice(2);
95
+ return hex.length === byteLength * 2;
96
+ }
97
+ function isValidAmount(value) {
98
+ return typeof value === "bigint" && value > 0n;
99
+ }
100
+ function isNonNegativeAmount(value) {
101
+ return typeof value === "bigint" && value >= 0n;
102
+ }
103
+ function isValidSlippage(value) {
104
+ return typeof value === "number" && !isNaN(value) && value >= 0 && value < 1;
105
+ }
106
+ var STEALTH_META_ADDRESS_REGEX = /^sip:[a-z]+:0x[0-9a-fA-F]{64,66}:0x[0-9a-fA-F]{64,66}$/;
107
+ function isValidStealthMetaAddress(addr) {
108
+ if (typeof addr !== "string") return false;
109
+ return STEALTH_META_ADDRESS_REGEX.test(addr);
110
+ }
111
+ function isValidCompressedPublicKey(key) {
112
+ if (!isValidHexLength(key, 33)) return false;
113
+ const prefix = key.slice(2, 4);
114
+ return prefix === "02" || prefix === "03";
115
+ }
116
+ function isValidEd25519PublicKey(key) {
117
+ return isValidHexLength(key, 32);
118
+ }
119
+ function isValidPrivateKey(key) {
120
+ return isValidHexLength(key, 32);
121
+ }
122
+ function validateAsset(asset, field) {
123
+ if (!asset || typeof asset !== "object") {
124
+ throw new ValidationError("must be an object", field);
125
+ }
126
+ const a = asset;
127
+ if (!a.chain || !isValidChainId(a.chain)) {
128
+ throw new ValidationError(
129
+ `invalid chain '${a.chain}', must be one of: ${VALID_CHAIN_IDS.join(", ")}`,
130
+ `${field}.chain`
131
+ );
132
+ }
133
+ if (typeof a.symbol !== "string" || a.symbol.length === 0) {
134
+ throw new ValidationError("symbol must be a non-empty string", `${field}.symbol`);
135
+ }
136
+ if (a.address !== null && !isValidHex(a.address)) {
137
+ throw new ValidationError("address must be null or valid hex string", `${field}.address`);
138
+ }
139
+ if (typeof a.decimals !== "number" || !Number.isInteger(a.decimals) || a.decimals < 0) {
140
+ throw new ValidationError("decimals must be a non-negative integer", `${field}.decimals`);
141
+ }
142
+ }
143
+ function validateIntentInput(input, field = "input") {
144
+ if (!input || typeof input !== "object") {
145
+ throw new ValidationError("must be an object", field);
146
+ }
147
+ const i = input;
148
+ validateAsset(i.asset, `${field}.asset`);
149
+ if (!isValidAmount(i.amount)) {
150
+ throw new ValidationError(
151
+ "amount must be a positive bigint",
152
+ `${field}.amount`,
153
+ { received: typeof i.amount, value: String(i.amount) }
154
+ );
155
+ }
156
+ }
157
+ function validateIntentOutput(output, field = "output") {
158
+ if (!output || typeof output !== "object") {
159
+ throw new ValidationError("must be an object", field);
160
+ }
161
+ const o = output;
162
+ validateAsset(o.asset, `${field}.asset`);
163
+ if (!isNonNegativeAmount(o.minAmount)) {
164
+ throw new ValidationError(
165
+ "minAmount must be a non-negative bigint",
166
+ `${field}.minAmount`,
167
+ { received: typeof o.minAmount, value: String(o.minAmount) }
168
+ );
169
+ }
170
+ if (!isValidSlippage(o.maxSlippage)) {
171
+ throw new ValidationError(
172
+ "maxSlippage must be a number between 0 and 1 (exclusive)",
173
+ `${field}.maxSlippage`,
174
+ { received: o.maxSlippage }
175
+ );
176
+ }
177
+ }
178
+ function validateCreateIntentParams(params) {
179
+ if (!params || typeof params !== "object") {
180
+ throw new ValidationError("params must be an object");
181
+ }
182
+ const p = params;
183
+ if (!p.input) {
184
+ throw new ValidationError("input is required", "input");
185
+ }
186
+ validateIntentInput(p.input, "input");
187
+ if (!p.output) {
188
+ throw new ValidationError("output is required", "output");
189
+ }
190
+ validateIntentOutput(p.output, "output");
191
+ if (!p.privacy) {
192
+ throw new ValidationError("privacy is required", "privacy");
193
+ }
194
+ if (!isValidPrivacyLevel(p.privacy)) {
195
+ throw new ValidationError(
196
+ `invalid privacy level '${p.privacy}', must be one of: transparent, shielded, compliant`,
197
+ "privacy"
198
+ );
199
+ }
200
+ if ((p.privacy === "shielded" || p.privacy === "compliant") && p.recipientMetaAddress) {
201
+ if (!isValidStealthMetaAddress(p.recipientMetaAddress)) {
202
+ throw new ValidationError(
203
+ "invalid stealth meta-address format, expected: sip:<chain>:<spendingKey>:<viewingKey>",
204
+ "recipientMetaAddress"
205
+ );
206
+ }
207
+ }
208
+ if (p.privacy === "compliant" && !p.viewingKey) {
209
+ throw new ValidationError(
210
+ "viewingKey is required for compliant mode",
211
+ "viewingKey"
212
+ );
213
+ }
214
+ if (p.viewingKey && !isValidHex(p.viewingKey)) {
215
+ throw new ValidationError(
216
+ "viewingKey must be a valid hex string",
217
+ "viewingKey"
218
+ );
219
+ }
220
+ if (p.ttl !== void 0) {
221
+ if (typeof p.ttl !== "number" || !Number.isInteger(p.ttl) || p.ttl <= 0) {
222
+ throw new ValidationError(
223
+ "ttl must be a positive integer (seconds)",
224
+ "ttl",
225
+ { received: p.ttl }
226
+ );
227
+ }
228
+ }
229
+ }
230
+ function validateViewingKey(key, field = "viewingKey") {
231
+ if (!key || typeof key !== "string") {
232
+ throw new ValidationError("must be a string", field);
233
+ }
234
+ if (!isValidHex(key)) {
235
+ throw new ValidationError("must be a valid hex string with 0x prefix", field);
236
+ }
237
+ if (!isValidHexLength(key, 32)) {
238
+ throw new ValidationError("must be 32 bytes (64 hex characters)", field);
239
+ }
240
+ }
241
+ var SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n;
242
+ function isValidScalar(value) {
243
+ return value > 0n && value < SECP256K1_ORDER;
244
+ }
245
+ function validateScalar(value, field) {
246
+ if (typeof value !== "bigint") {
247
+ throw new ValidationError("must be a bigint", field);
248
+ }
249
+ if (!isValidScalar(value)) {
250
+ throw new ValidationError(
251
+ "must be in range (0, curve order)",
252
+ field,
253
+ { curveOrder: SECP256K1_ORDER.toString(16) }
254
+ );
255
+ }
256
+ }
257
+ function isValidEvmAddress(address) {
258
+ if (typeof address !== "string") return false;
259
+ return /^0x[0-9a-fA-F]{40}$/.test(address);
260
+ }
261
+ function isValidSolanaAddressFormat(address) {
262
+ if (typeof address !== "string") return false;
263
+ return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
264
+ }
265
+ function isValidNearAddressFormat(address) {
266
+ if (typeof address !== "string") return false;
267
+ if (/^[0-9a-f]{64}$/.test(address)) return true;
268
+ if (address.length < 2 || address.length > 64) return false;
269
+ if (!/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/.test(address)) return false;
270
+ if (address.includes("..")) return false;
271
+ return true;
272
+ }
273
+ function isValidCosmosAddressFormat(address) {
274
+ if (typeof address !== "string") return false;
275
+ if (address.length < 39 || address.length > 90) return false;
276
+ const bech32Pattern = /^[a-z]+1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38,}$/;
277
+ return bech32Pattern.test(address);
278
+ }
279
+ function getChainAddressType(chain) {
280
+ switch (chain) {
281
+ case "ethereum":
282
+ case "polygon":
283
+ case "arbitrum":
284
+ case "optimism":
285
+ case "base":
286
+ return "evm";
287
+ case "solana":
288
+ return "solana";
289
+ case "near":
290
+ return "near";
291
+ case "zcash":
292
+ return "zcash";
293
+ case "cosmos":
294
+ case "osmosis":
295
+ case "injective":
296
+ case "celestia":
297
+ case "sei":
298
+ case "dydx":
299
+ return "cosmos";
300
+ default:
301
+ return "unknown";
302
+ }
303
+ }
304
+ function validateAddressForChain(address, chain, field = "address") {
305
+ const addressType = getChainAddressType(chain);
306
+ switch (addressType) {
307
+ case "evm":
308
+ if (!isValidEvmAddress(address)) {
309
+ throw new ValidationError(
310
+ `Invalid address format for ${chain}. Expected EVM address (0x + 40 hex chars), got: ${address.slice(0, 20)}...`,
311
+ field,
312
+ { chain, expectedFormat: "0x...", receivedFormat: address.startsWith("0x") ? "hex but wrong length" : "not hex" }
313
+ );
314
+ }
315
+ break;
316
+ case "solana":
317
+ if (!isValidSolanaAddressFormat(address)) {
318
+ throw new ValidationError(
319
+ `Invalid address format for ${chain}. Expected Solana address (base58, 32-44 chars), got: ${address.slice(0, 20)}...`,
320
+ field,
321
+ { chain, expectedFormat: "base58", receivedFormat: address.startsWith("0x") ? "looks like EVM" : "unknown" }
322
+ );
323
+ }
324
+ break;
325
+ case "near":
326
+ if (!isValidNearAddressFormat(address)) {
327
+ throw new ValidationError(
328
+ `Invalid address format for ${chain}. Expected NEAR account ID (named or implicit), got: ${address.slice(0, 20)}...`,
329
+ field,
330
+ { chain, expectedFormat: "alice.near or 64 hex chars" }
331
+ );
332
+ }
333
+ break;
334
+ case "zcash":
335
+ if (!address || address.length === 0) {
336
+ throw new ValidationError(
337
+ `Invalid address format for ${chain}. Expected Zcash address.`,
338
+ field,
339
+ { chain }
340
+ );
341
+ }
342
+ break;
343
+ case "cosmos":
344
+ if (!isValidCosmosAddressFormat(address)) {
345
+ throw new ValidationError(
346
+ `Invalid address format for ${chain}. Expected Cosmos bech32 address, got: ${address.slice(0, 20)}...`,
347
+ field,
348
+ { chain, expectedFormat: "bech32" }
349
+ );
350
+ }
351
+ break;
352
+ default:
353
+ break;
354
+ }
355
+ }
356
+ function isAddressValidForChain(address, chain) {
357
+ try {
358
+ validateAddressForChain(address, chain);
359
+ return true;
360
+ } catch {
361
+ return false;
362
+ }
363
+ }
364
+
365
+ // src/secure-memory.ts
366
+ import { randomBytes } from "@noble/hashes/utils";
367
+ function secureWipe(buffer) {
368
+ if (!buffer || buffer.length === 0) {
369
+ return;
370
+ }
371
+ const random = randomBytes(buffer.length);
372
+ buffer.set(random);
373
+ buffer.fill(0);
374
+ }
375
+ async function withSecureBuffer(createSecret, useSecret) {
376
+ const secret = createSecret();
377
+ try {
378
+ return await useSecret(secret);
379
+ } finally {
380
+ secureWipe(secret);
381
+ }
382
+ }
383
+ function withSecureBufferSync(createSecret, useSecret) {
384
+ const secret = createSecret();
385
+ try {
386
+ return useSecret(secret);
387
+ } finally {
388
+ secureWipe(secret);
389
+ }
390
+ }
391
+ function secureWipeAll(...buffers) {
392
+ for (const buffer of buffers) {
393
+ if (buffer) {
394
+ secureWipe(buffer);
395
+ }
396
+ }
397
+ }
398
+
399
+ // src/stealth.ts
400
+ function generateStealthMetaAddress(chain, label) {
401
+ if (!isValidChainId(chain)) {
402
+ throw new ValidationError(
403
+ `invalid chain '${chain}', must be one of: solana, ethereum, near, zcash, polygon, arbitrum, optimism, base, bitcoin, aptos, sui, cosmos, osmosis, injective, celestia, sei, dydx`,
404
+ "chain"
405
+ );
406
+ }
407
+ if (isEd25519Chain(chain)) {
408
+ return generateEd25519StealthMetaAddress(chain, label);
409
+ }
410
+ const spendingPrivateKey = randomBytes2(32);
411
+ const viewingPrivateKey = randomBytes2(32);
412
+ try {
413
+ const spendingKey = secp256k1.getPublicKey(spendingPrivateKey, true);
414
+ const viewingKey = secp256k1.getPublicKey(viewingPrivateKey, true);
415
+ const result = {
416
+ metaAddress: {
417
+ spendingKey: `0x${bytesToHex(spendingKey)}`,
418
+ viewingKey: `0x${bytesToHex(viewingKey)}`,
419
+ chain,
420
+ label
421
+ },
422
+ spendingPrivateKey: `0x${bytesToHex(spendingPrivateKey)}`,
423
+ viewingPrivateKey: `0x${bytesToHex(viewingPrivateKey)}`
424
+ };
425
+ return result;
426
+ } finally {
427
+ secureWipeAll(spendingPrivateKey, viewingPrivateKey);
428
+ }
429
+ }
430
+ function validateStealthMetaAddress(metaAddress, field = "recipientMetaAddress") {
431
+ if (!metaAddress || typeof metaAddress !== "object") {
432
+ throw new ValidationError("must be an object", field);
433
+ }
434
+ if (!isValidChainId(metaAddress.chain)) {
435
+ throw new ValidationError(
436
+ `invalid chain '${metaAddress.chain}'`,
437
+ `${field}.chain`
438
+ );
439
+ }
440
+ const isEd25519 = isEd25519Chain(metaAddress.chain);
441
+ if (isEd25519) {
442
+ if (!isValidEd25519PublicKey(metaAddress.spendingKey)) {
443
+ throw new ValidationError(
444
+ "spendingKey must be a valid ed25519 public key (32 bytes)",
445
+ `${field}.spendingKey`
446
+ );
447
+ }
448
+ if (!isValidEd25519PublicKey(metaAddress.viewingKey)) {
449
+ throw new ValidationError(
450
+ "viewingKey must be a valid ed25519 public key (32 bytes)",
451
+ `${field}.viewingKey`
452
+ );
453
+ }
454
+ } else {
455
+ if (!isValidCompressedPublicKey(metaAddress.spendingKey)) {
456
+ throw new ValidationError(
457
+ "spendingKey must be a valid compressed secp256k1 public key (33 bytes, starting with 02 or 03)",
458
+ `${field}.spendingKey`
459
+ );
460
+ }
461
+ if (!isValidCompressedPublicKey(metaAddress.viewingKey)) {
462
+ throw new ValidationError(
463
+ "viewingKey must be a valid compressed secp256k1 public key (33 bytes, starting with 02 or 03)",
464
+ `${field}.viewingKey`
465
+ );
466
+ }
467
+ }
468
+ }
469
+ function generateStealthAddress(recipientMetaAddress) {
470
+ validateStealthMetaAddress(recipientMetaAddress);
471
+ if (isEd25519Chain(recipientMetaAddress.chain)) {
472
+ return generateEd25519StealthAddress(recipientMetaAddress);
473
+ }
474
+ const ephemeralPrivateKey = randomBytes2(32);
475
+ try {
476
+ const ephemeralPublicKey = secp256k1.getPublicKey(ephemeralPrivateKey, true);
477
+ const spendingKeyBytes = hexToBytes(recipientMetaAddress.spendingKey.slice(2));
478
+ const viewingKeyBytes = hexToBytes(recipientMetaAddress.viewingKey.slice(2));
479
+ const sharedSecretPoint = secp256k1.getSharedSecret(
480
+ ephemeralPrivateKey,
481
+ spendingKeyBytes
482
+ );
483
+ const sharedSecretHash = sha256(sharedSecretPoint);
484
+ const hashTimesG = secp256k1.getPublicKey(sharedSecretHash, true);
485
+ const viewingKeyPoint = secp256k1.ProjectivePoint.fromHex(viewingKeyBytes);
486
+ const hashTimesGPoint = secp256k1.ProjectivePoint.fromHex(hashTimesG);
487
+ const stealthPoint = viewingKeyPoint.add(hashTimesGPoint);
488
+ const stealthAddressBytes = stealthPoint.toRawBytes(true);
489
+ const viewTag = sharedSecretHash[0];
490
+ return {
491
+ stealthAddress: {
492
+ address: `0x${bytesToHex(stealthAddressBytes)}`,
493
+ ephemeralPublicKey: `0x${bytesToHex(ephemeralPublicKey)}`,
494
+ viewTag
495
+ },
496
+ sharedSecret: `0x${bytesToHex(sharedSecretHash)}`
497
+ };
498
+ } finally {
499
+ secureWipe(ephemeralPrivateKey);
500
+ }
501
+ }
502
+ function validateStealthAddress(stealthAddress, field = "stealthAddress") {
503
+ if (!stealthAddress || typeof stealthAddress !== "object") {
504
+ throw new ValidationError("must be an object", field);
505
+ }
506
+ if (!isValidCompressedPublicKey(stealthAddress.address)) {
507
+ throw new ValidationError(
508
+ "address must be a valid compressed secp256k1 public key",
509
+ `${field}.address`
510
+ );
511
+ }
512
+ if (!isValidCompressedPublicKey(stealthAddress.ephemeralPublicKey)) {
513
+ throw new ValidationError(
514
+ "ephemeralPublicKey must be a valid compressed secp256k1 public key",
515
+ `${field}.ephemeralPublicKey`
516
+ );
517
+ }
518
+ if (typeof stealthAddress.viewTag !== "number" || !Number.isInteger(stealthAddress.viewTag) || stealthAddress.viewTag < 0 || stealthAddress.viewTag > 255) {
519
+ throw new ValidationError(
520
+ "viewTag must be an integer between 0 and 255",
521
+ `${field}.viewTag`
522
+ );
523
+ }
524
+ }
525
+ function deriveStealthPrivateKey(stealthAddress, spendingPrivateKey, viewingPrivateKey) {
526
+ validateStealthAddress(stealthAddress);
527
+ if (!isValidPrivateKey(spendingPrivateKey)) {
528
+ throw new ValidationError(
529
+ "must be a valid 32-byte hex string",
530
+ "spendingPrivateKey"
531
+ );
532
+ }
533
+ if (!isValidPrivateKey(viewingPrivateKey)) {
534
+ throw new ValidationError(
535
+ "must be a valid 32-byte hex string",
536
+ "viewingPrivateKey"
537
+ );
538
+ }
539
+ const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2));
540
+ const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2));
541
+ const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2));
542
+ try {
543
+ const sharedSecretPoint = secp256k1.getSharedSecret(
544
+ spendingPrivBytes,
545
+ ephemeralPubBytes
546
+ );
547
+ const sharedSecretHash = sha256(sharedSecretPoint);
548
+ const viewingScalar = bytesToBigInt(viewingPrivBytes);
549
+ const hashScalar = bytesToBigInt(sharedSecretHash);
550
+ const stealthPrivateScalar = (viewingScalar + hashScalar) % secp256k1.CURVE.n;
551
+ const stealthPrivateKey = bigIntToBytes(stealthPrivateScalar, 32);
552
+ const result = {
553
+ stealthAddress: stealthAddress.address,
554
+ ephemeralPublicKey: stealthAddress.ephemeralPublicKey,
555
+ privateKey: `0x${bytesToHex(stealthPrivateKey)}`
556
+ };
557
+ secureWipe(stealthPrivateKey);
558
+ return result;
559
+ } finally {
560
+ secureWipeAll(spendingPrivBytes, viewingPrivBytes);
561
+ }
562
+ }
563
+ function checkStealthAddress(stealthAddress, spendingPrivateKey, viewingPrivateKey) {
564
+ validateStealthAddress(stealthAddress);
565
+ if (!isValidPrivateKey(spendingPrivateKey)) {
566
+ throw new ValidationError(
567
+ "must be a valid 32-byte hex string",
568
+ "spendingPrivateKey"
569
+ );
570
+ }
571
+ if (!isValidPrivateKey(viewingPrivateKey)) {
572
+ throw new ValidationError(
573
+ "must be a valid 32-byte hex string",
574
+ "viewingPrivateKey"
575
+ );
576
+ }
577
+ const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2));
578
+ const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2));
579
+ const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2));
580
+ try {
581
+ const sharedSecretPoint = secp256k1.getSharedSecret(
582
+ spendingPrivBytes,
583
+ ephemeralPubBytes
584
+ );
585
+ const sharedSecretHash = sha256(sharedSecretPoint);
586
+ if (sharedSecretHash[0] !== stealthAddress.viewTag) {
587
+ return false;
588
+ }
589
+ const viewingScalar = bytesToBigInt(viewingPrivBytes);
590
+ const hashScalar = bytesToBigInt(sharedSecretHash);
591
+ const stealthPrivateScalar = (viewingScalar + hashScalar) % secp256k1.CURVE.n;
592
+ const derivedKeyBytes = bigIntToBytes(stealthPrivateScalar, 32);
593
+ const expectedPubKey = secp256k1.getPublicKey(derivedKeyBytes, true);
594
+ secureWipe(derivedKeyBytes);
595
+ const providedAddress = hexToBytes(stealthAddress.address.slice(2));
596
+ return bytesToHex(expectedPubKey) === bytesToHex(providedAddress);
597
+ } finally {
598
+ secureWipeAll(spendingPrivBytes, viewingPrivBytes);
599
+ }
600
+ }
601
+ function encodeStealthMetaAddress(metaAddress) {
602
+ return `sip:${metaAddress.chain}:${metaAddress.spendingKey}:${metaAddress.viewingKey}`;
603
+ }
604
+ function decodeStealthMetaAddress(encoded) {
605
+ if (typeof encoded !== "string") {
606
+ throw new ValidationError("must be a string", "encoded");
607
+ }
608
+ const parts = encoded.split(":");
609
+ if (parts.length < 4 || parts[0] !== "sip") {
610
+ throw new ValidationError(
611
+ "invalid format, expected: sip:<chain>:<spendingKey>:<viewingKey>",
612
+ "encoded"
613
+ );
614
+ }
615
+ const [, chain, spendingKey, viewingKey] = parts;
616
+ if (!isValidChainId(chain)) {
617
+ throw new ValidationError(
618
+ `invalid chain '${chain}'`,
619
+ "encoded.chain"
620
+ );
621
+ }
622
+ const chainId = chain;
623
+ if (isEd25519Chain(chainId)) {
624
+ if (!isValidEd25519PublicKey(spendingKey)) {
625
+ throw new ValidationError(
626
+ "spendingKey must be a valid 32-byte ed25519 public key",
627
+ "encoded.spendingKey"
628
+ );
629
+ }
630
+ if (!isValidEd25519PublicKey(viewingKey)) {
631
+ throw new ValidationError(
632
+ "viewingKey must be a valid 32-byte ed25519 public key",
633
+ "encoded.viewingKey"
634
+ );
635
+ }
636
+ } else {
637
+ if (!isValidCompressedPublicKey(spendingKey)) {
638
+ throw new ValidationError(
639
+ "spendingKey must be a valid compressed secp256k1 public key",
640
+ "encoded.spendingKey"
641
+ );
642
+ }
643
+ if (!isValidCompressedPublicKey(viewingKey)) {
644
+ throw new ValidationError(
645
+ "viewingKey must be a valid compressed secp256k1 public key",
646
+ "encoded.viewingKey"
647
+ );
648
+ }
649
+ }
650
+ return {
651
+ chain,
652
+ spendingKey,
653
+ viewingKey
654
+ };
655
+ }
656
+ function bytesToBigInt(bytes) {
657
+ let result = 0n;
658
+ for (const byte of bytes) {
659
+ result = (result << 8n) + BigInt(byte);
660
+ }
661
+ return result;
662
+ }
663
+ function bigIntToBytes(value, length) {
664
+ const bytes = new Uint8Array(length);
665
+ for (let i = length - 1; i >= 0; i--) {
666
+ bytes[i] = Number(value & 0xffn);
667
+ value >>= 8n;
668
+ }
669
+ return bytes;
670
+ }
671
+ function publicKeyToEthAddress(publicKey) {
672
+ const keyHex = publicKey.startsWith("0x") ? publicKey.slice(2) : publicKey;
673
+ const keyBytes = hexToBytes(keyHex);
674
+ let uncompressedBytes;
675
+ if (keyBytes.length === 33) {
676
+ const point = secp256k1.ProjectivePoint.fromHex(keyBytes);
677
+ uncompressedBytes = point.toRawBytes(false);
678
+ } else if (keyBytes.length === 65) {
679
+ uncompressedBytes = keyBytes;
680
+ } else {
681
+ throw new ValidationError(
682
+ `invalid public key length: ${keyBytes.length}, expected 33 (compressed) or 65 (uncompressed)`,
683
+ "publicKey"
684
+ );
685
+ }
686
+ const pubKeyWithoutPrefix = uncompressedBytes.slice(1);
687
+ const hash = keccak_256(pubKeyWithoutPrefix);
688
+ const addressBytes = hash.slice(-20);
689
+ return toChecksumAddress(`0x${bytesToHex(addressBytes)}`);
690
+ }
691
+ function toChecksumAddress(address) {
692
+ const addr = address.toLowerCase().replace("0x", "");
693
+ const hash = bytesToHex(keccak_256(new TextEncoder().encode(addr)));
694
+ let checksummed = "0x";
695
+ for (let i = 0; i < addr.length; i++) {
696
+ if (parseInt(hash[i], 16) >= 8) {
697
+ checksummed += addr[i].toUpperCase();
698
+ } else {
699
+ checksummed += addr[i];
700
+ }
701
+ }
702
+ return checksummed;
703
+ }
704
+ var ED25519_ORDER = 2n ** 252n + 27742317777372353535851937790883648493n;
705
+ var ED25519_CHAINS = ["solana", "near", "aptos", "sui"];
706
+ function isEd25519Chain(chain) {
707
+ return ED25519_CHAINS.includes(chain);
708
+ }
709
+ function getCurveForChain(chain) {
710
+ return isEd25519Chain(chain) ? "ed25519" : "secp256k1";
711
+ }
712
+ function validateEd25519StealthMetaAddress(metaAddress, field = "recipientMetaAddress") {
713
+ if (!metaAddress || typeof metaAddress !== "object") {
714
+ throw new ValidationError("must be an object", field);
715
+ }
716
+ if (!isValidChainId(metaAddress.chain)) {
717
+ throw new ValidationError(
718
+ `invalid chain '${metaAddress.chain}'`,
719
+ `${field}.chain`
720
+ );
721
+ }
722
+ if (!isEd25519Chain(metaAddress.chain)) {
723
+ throw new ValidationError(
724
+ `chain '${metaAddress.chain}' does not use ed25519, use secp256k1 functions instead`,
725
+ `${field}.chain`
726
+ );
727
+ }
728
+ if (!isValidEd25519PublicKey(metaAddress.spendingKey)) {
729
+ throw new ValidationError(
730
+ "spendingKey must be a valid ed25519 public key (32 bytes)",
731
+ `${field}.spendingKey`
732
+ );
733
+ }
734
+ if (!isValidEd25519PublicKey(metaAddress.viewingKey)) {
735
+ throw new ValidationError(
736
+ "viewingKey must be a valid ed25519 public key (32 bytes)",
737
+ `${field}.viewingKey`
738
+ );
739
+ }
740
+ }
741
+ function validateEd25519StealthAddress(stealthAddress, field = "stealthAddress") {
742
+ if (!stealthAddress || typeof stealthAddress !== "object") {
743
+ throw new ValidationError("must be an object", field);
744
+ }
745
+ if (!isValidEd25519PublicKey(stealthAddress.address)) {
746
+ throw new ValidationError(
747
+ "address must be a valid ed25519 public key (32 bytes)",
748
+ `${field}.address`
749
+ );
750
+ }
751
+ if (!isValidEd25519PublicKey(stealthAddress.ephemeralPublicKey)) {
752
+ throw new ValidationError(
753
+ "ephemeralPublicKey must be a valid ed25519 public key (32 bytes)",
754
+ `${field}.ephemeralPublicKey`
755
+ );
756
+ }
757
+ if (typeof stealthAddress.viewTag !== "number" || !Number.isInteger(stealthAddress.viewTag) || stealthAddress.viewTag < 0 || stealthAddress.viewTag > 255) {
758
+ throw new ValidationError(
759
+ "viewTag must be an integer between 0 and 255",
760
+ `${field}.viewTag`
761
+ );
762
+ }
763
+ }
764
+ function getEd25519Scalar(privateKey) {
765
+ const hash = sha512(privateKey);
766
+ const scalar = hash.slice(0, 32);
767
+ scalar[0] &= 248;
768
+ scalar[31] &= 127;
769
+ scalar[31] |= 64;
770
+ return bytesToBigIntLE(scalar);
771
+ }
772
+ function bytesToBigIntLE(bytes) {
773
+ let result = 0n;
774
+ for (let i = bytes.length - 1; i >= 0; i--) {
775
+ result = (result << 8n) + BigInt(bytes[i]);
776
+ }
777
+ return result;
778
+ }
779
+ function bigIntToBytesLE(value, length) {
780
+ const bytes = new Uint8Array(length);
781
+ for (let i = 0; i < length; i++) {
782
+ bytes[i] = Number(value & 0xffn);
783
+ value >>= 8n;
784
+ }
785
+ return bytes;
786
+ }
787
+ function generateEd25519StealthMetaAddress(chain, label) {
788
+ if (!isValidChainId(chain)) {
789
+ throw new ValidationError(
790
+ `invalid chain '${chain}', must be one of: solana, ethereum, near, zcash, polygon, arbitrum, optimism, base, bitcoin, aptos, sui, cosmos, osmosis, injective, celestia, sei, dydx`,
791
+ "chain"
792
+ );
793
+ }
794
+ if (!isEd25519Chain(chain)) {
795
+ throw new ValidationError(
796
+ `chain '${chain}' does not use ed25519, use generateStealthMetaAddress() for secp256k1 chains`,
797
+ "chain"
798
+ );
799
+ }
800
+ const spendingPrivateKey = randomBytes2(32);
801
+ const viewingPrivateKey = randomBytes2(32);
802
+ try {
803
+ const spendingKey = ed25519.getPublicKey(spendingPrivateKey);
804
+ const viewingKey = ed25519.getPublicKey(viewingPrivateKey);
805
+ const result = {
806
+ metaAddress: {
807
+ spendingKey: `0x${bytesToHex(spendingKey)}`,
808
+ viewingKey: `0x${bytesToHex(viewingKey)}`,
809
+ chain,
810
+ label
811
+ },
812
+ spendingPrivateKey: `0x${bytesToHex(spendingPrivateKey)}`,
813
+ viewingPrivateKey: `0x${bytesToHex(viewingPrivateKey)}`
814
+ };
815
+ return result;
816
+ } finally {
817
+ secureWipeAll(spendingPrivateKey, viewingPrivateKey);
818
+ }
819
+ }
820
+ function generateEd25519StealthAddress(recipientMetaAddress) {
821
+ validateEd25519StealthMetaAddress(recipientMetaAddress);
822
+ const ephemeralPrivateKey = randomBytes2(32);
823
+ try {
824
+ const ephemeralPublicKey = ed25519.getPublicKey(ephemeralPrivateKey);
825
+ const spendingKeyBytes = hexToBytes(recipientMetaAddress.spendingKey.slice(2));
826
+ const viewingKeyBytes = hexToBytes(recipientMetaAddress.viewingKey.slice(2));
827
+ const rawEphemeralScalar = getEd25519Scalar(ephemeralPrivateKey);
828
+ const ephemeralScalar = rawEphemeralScalar % ED25519_ORDER;
829
+ if (ephemeralScalar === 0n) {
830
+ throw new Error("CRITICAL: Zero ephemeral scalar after reduction - investigate RNG");
831
+ }
832
+ const spendingPoint = ed25519.ExtendedPoint.fromHex(spendingKeyBytes);
833
+ const sharedSecretPoint = spendingPoint.multiply(ephemeralScalar);
834
+ const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes());
835
+ const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER;
836
+ if (hashScalar === 0n) {
837
+ throw new Error("CRITICAL: Zero hash scalar after reduction - investigate hash computation");
838
+ }
839
+ const hashTimesG = ed25519.ExtendedPoint.BASE.multiply(hashScalar);
840
+ const viewingPoint = ed25519.ExtendedPoint.fromHex(viewingKeyBytes);
841
+ const stealthPoint = viewingPoint.add(hashTimesG);
842
+ const stealthAddressBytes = stealthPoint.toRawBytes();
843
+ const viewTag = sharedSecretHash[0];
844
+ return {
845
+ stealthAddress: {
846
+ address: `0x${bytesToHex(stealthAddressBytes)}`,
847
+ ephemeralPublicKey: `0x${bytesToHex(ephemeralPublicKey)}`,
848
+ viewTag
849
+ },
850
+ sharedSecret: `0x${bytesToHex(sharedSecretHash)}`
851
+ };
852
+ } finally {
853
+ secureWipe(ephemeralPrivateKey);
854
+ }
855
+ }
856
+ function deriveEd25519StealthPrivateKey(stealthAddress, spendingPrivateKey, viewingPrivateKey) {
857
+ validateEd25519StealthAddress(stealthAddress);
858
+ if (!isValidPrivateKey(spendingPrivateKey)) {
859
+ throw new ValidationError(
860
+ "must be a valid 32-byte hex string",
861
+ "spendingPrivateKey"
862
+ );
863
+ }
864
+ if (!isValidPrivateKey(viewingPrivateKey)) {
865
+ throw new ValidationError(
866
+ "must be a valid 32-byte hex string",
867
+ "viewingPrivateKey"
868
+ );
869
+ }
870
+ const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2));
871
+ const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2));
872
+ const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2));
873
+ try {
874
+ const rawSpendingScalar = getEd25519Scalar(spendingPrivBytes);
875
+ const spendingScalar = rawSpendingScalar % ED25519_ORDER;
876
+ if (spendingScalar === 0n) {
877
+ throw new Error("CRITICAL: Zero spending scalar after reduction - investigate key derivation");
878
+ }
879
+ const ephemeralPoint = ed25519.ExtendedPoint.fromHex(ephemeralPubBytes);
880
+ const sharedSecretPoint = ephemeralPoint.multiply(spendingScalar);
881
+ const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes());
882
+ const rawViewingScalar = getEd25519Scalar(viewingPrivBytes);
883
+ const viewingScalar = rawViewingScalar % ED25519_ORDER;
884
+ if (viewingScalar === 0n) {
885
+ throw new Error("CRITICAL: Zero viewing scalar after reduction - investigate key derivation");
886
+ }
887
+ const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER;
888
+ if (hashScalar === 0n) {
889
+ throw new Error("CRITICAL: Zero hash scalar after reduction - investigate hash computation");
890
+ }
891
+ const stealthPrivateScalar = (viewingScalar + hashScalar) % ED25519_ORDER;
892
+ if (stealthPrivateScalar === 0n) {
893
+ throw new Error("CRITICAL: Zero stealth scalar after reduction - investigate key derivation");
894
+ }
895
+ const stealthPrivateKey = bigIntToBytesLE(stealthPrivateScalar, 32);
896
+ const result = {
897
+ stealthAddress: stealthAddress.address,
898
+ ephemeralPublicKey: stealthAddress.ephemeralPublicKey,
899
+ privateKey: `0x${bytesToHex(stealthPrivateKey)}`
900
+ };
901
+ secureWipe(stealthPrivateKey);
902
+ return result;
903
+ } finally {
904
+ secureWipeAll(spendingPrivBytes, viewingPrivBytes);
905
+ }
906
+ }
907
+ function checkEd25519StealthAddress(stealthAddress, spendingPrivateKey, viewingPrivateKey) {
908
+ validateEd25519StealthAddress(stealthAddress);
909
+ if (!isValidPrivateKey(spendingPrivateKey)) {
910
+ throw new ValidationError(
911
+ "must be a valid 32-byte hex string",
912
+ "spendingPrivateKey"
913
+ );
914
+ }
915
+ if (!isValidPrivateKey(viewingPrivateKey)) {
916
+ throw new ValidationError(
917
+ "must be a valid 32-byte hex string",
918
+ "viewingPrivateKey"
919
+ );
920
+ }
921
+ const spendingPrivBytes = hexToBytes(spendingPrivateKey.slice(2));
922
+ const viewingPrivBytes = hexToBytes(viewingPrivateKey.slice(2));
923
+ const ephemeralPubBytes = hexToBytes(stealthAddress.ephemeralPublicKey.slice(2));
924
+ try {
925
+ const rawSpendingScalar = getEd25519Scalar(spendingPrivBytes);
926
+ const spendingScalar = rawSpendingScalar % ED25519_ORDER;
927
+ if (spendingScalar === 0n) {
928
+ throw new Error("CRITICAL: Zero spending scalar after reduction - investigate key derivation");
929
+ }
930
+ const ephemeralPoint = ed25519.ExtendedPoint.fromHex(ephemeralPubBytes);
931
+ const sharedSecretPoint = ephemeralPoint.multiply(spendingScalar);
932
+ const sharedSecretHash = sha256(sharedSecretPoint.toRawBytes());
933
+ if (sharedSecretHash[0] !== stealthAddress.viewTag) {
934
+ return false;
935
+ }
936
+ const rawViewingScalar = getEd25519Scalar(viewingPrivBytes);
937
+ const viewingScalar = rawViewingScalar % ED25519_ORDER;
938
+ if (viewingScalar === 0n) {
939
+ throw new Error("CRITICAL: Zero viewing scalar after reduction - investigate key derivation");
940
+ }
941
+ const hashScalar = bytesToBigInt(sharedSecretHash) % ED25519_ORDER;
942
+ if (hashScalar === 0n) {
943
+ throw new Error("CRITICAL: Zero hash scalar after reduction - investigate hash computation");
944
+ }
945
+ const stealthPrivateScalar = (viewingScalar + hashScalar) % ED25519_ORDER;
946
+ if (stealthPrivateScalar === 0n) {
947
+ throw new Error("CRITICAL: Zero stealth scalar after reduction - investigate key derivation");
948
+ }
949
+ const expectedPubKey = ed25519.ExtendedPoint.BASE.multiply(stealthPrivateScalar);
950
+ const expectedPubKeyBytes = expectedPubKey.toRawBytes();
951
+ const providedAddress = hexToBytes(stealthAddress.address.slice(2));
952
+ return bytesToHex(expectedPubKeyBytes) === bytesToHex(providedAddress);
953
+ } finally {
954
+ secureWipeAll(spendingPrivBytes, viewingPrivBytes);
955
+ }
956
+ }
957
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
958
+ function bytesToBase58(bytes) {
959
+ let leadingZeros = 0;
960
+ for (let i = 0; i < bytes.length && bytes[i] === 0; i++) {
961
+ leadingZeros++;
962
+ }
963
+ let value = 0n;
964
+ for (const byte of bytes) {
965
+ value = value * 256n + BigInt(byte);
966
+ }
967
+ let result = "";
968
+ while (value > 0n) {
969
+ const remainder = value % 58n;
970
+ value = value / 58n;
971
+ result = BASE58_ALPHABET[Number(remainder)] + result;
972
+ }
973
+ return "1".repeat(leadingZeros) + result;
974
+ }
975
+ function base58ToBytes(str) {
976
+ let leadingOnes = 0;
977
+ for (let i = 0; i < str.length && str[i] === "1"; i++) {
978
+ leadingOnes++;
979
+ }
980
+ let value = 0n;
981
+ for (const char of str) {
982
+ const index = BASE58_ALPHABET.indexOf(char);
983
+ if (index === -1) {
984
+ throw new ValidationError(`Invalid base58 character: ${char}`, "address");
985
+ }
986
+ value = value * 58n + BigInt(index);
987
+ }
988
+ const bytes = [];
989
+ while (value > 0n) {
990
+ bytes.unshift(Number(value % 256n));
991
+ value = value / 256n;
992
+ }
993
+ const result = new Uint8Array(leadingOnes + bytes.length);
994
+ for (let i = 0; i < leadingOnes; i++) {
995
+ result[i] = 0;
996
+ }
997
+ for (let i = 0; i < bytes.length; i++) {
998
+ result[leadingOnes + i] = bytes[i];
999
+ }
1000
+ return result;
1001
+ }
1002
+ function ed25519PublicKeyToSolanaAddress(publicKey) {
1003
+ if (!isValidHex(publicKey)) {
1004
+ throw new ValidationError(
1005
+ "publicKey must be a valid hex string with 0x prefix",
1006
+ "publicKey"
1007
+ );
1008
+ }
1009
+ if (!isValidEd25519PublicKey(publicKey)) {
1010
+ throw new ValidationError(
1011
+ "publicKey must be 32 bytes (64 hex characters)",
1012
+ "publicKey"
1013
+ );
1014
+ }
1015
+ const publicKeyBytes = hexToBytes(publicKey.slice(2));
1016
+ return bytesToBase58(publicKeyBytes);
1017
+ }
1018
+ function isValidSolanaAddress(address) {
1019
+ if (typeof address !== "string" || address.length === 0) {
1020
+ return false;
1021
+ }
1022
+ if (address.length < 32 || address.length > 44) {
1023
+ return false;
1024
+ }
1025
+ try {
1026
+ const decoded = base58ToBytes(address);
1027
+ return decoded.length === 32;
1028
+ } catch {
1029
+ return false;
1030
+ }
1031
+ }
1032
+ function solanaAddressToEd25519PublicKey(address) {
1033
+ if (!isValidSolanaAddress(address)) {
1034
+ throw new ValidationError(
1035
+ "Invalid Solana address format",
1036
+ "address"
1037
+ );
1038
+ }
1039
+ const decoded = base58ToBytes(address);
1040
+ return `0x${bytesToHex(decoded)}`;
1041
+ }
1042
+ function ed25519PublicKeyToNearAddress(publicKey) {
1043
+ if (!isValidHex(publicKey)) {
1044
+ throw new ValidationError(
1045
+ "publicKey must be a valid hex string with 0x prefix",
1046
+ "publicKey"
1047
+ );
1048
+ }
1049
+ if (!isValidEd25519PublicKey(publicKey)) {
1050
+ throw new ValidationError(
1051
+ "publicKey must be 32 bytes (64 hex characters)",
1052
+ "publicKey"
1053
+ );
1054
+ }
1055
+ return publicKey.slice(2).toLowerCase();
1056
+ }
1057
+ function nearAddressToEd25519PublicKey(address) {
1058
+ if (!isValidNearImplicitAddress(address)) {
1059
+ throw new ValidationError(
1060
+ "Invalid NEAR implicit address format",
1061
+ "address"
1062
+ );
1063
+ }
1064
+ return `0x${address.toLowerCase()}`;
1065
+ }
1066
+ function isValidNearImplicitAddress(address) {
1067
+ if (typeof address !== "string" || address.length === 0) {
1068
+ return false;
1069
+ }
1070
+ if (address.length !== 64) {
1071
+ return false;
1072
+ }
1073
+ return /^[0-9a-f]{64}$/.test(address);
1074
+ }
1075
+ function isValidNearAccountId(accountId) {
1076
+ if (typeof accountId !== "string" || accountId.length === 0) {
1077
+ return false;
1078
+ }
1079
+ if (isValidNearImplicitAddress(accountId)) {
1080
+ return true;
1081
+ }
1082
+ if (accountId.length < 2 || accountId.length > 64) {
1083
+ return false;
1084
+ }
1085
+ const nearAccountPattern = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/;
1086
+ if (!nearAccountPattern.test(accountId)) {
1087
+ return false;
1088
+ }
1089
+ if (accountId.includes("..")) {
1090
+ return false;
1091
+ }
1092
+ return true;
1093
+ }
1094
+
1095
+ // src/chains/solana/transfer.ts
1096
+ async function sendPrivateSPLTransfer(params) {
1097
+ const {
1098
+ connection,
1099
+ sender,
1100
+ senderTokenAccount,
1101
+ recipientMetaAddress,
1102
+ mint,
1103
+ amount,
1104
+ signTransaction
1105
+ } = params;
1106
+ if (recipientMetaAddress.chain !== "solana") {
1107
+ throw new Error(
1108
+ `Invalid chain: expected 'solana', got '${recipientMetaAddress.chain}'`
1109
+ );
1110
+ }
1111
+ const { stealthAddress } = generateEd25519StealthAddress(recipientMetaAddress);
1112
+ const stealthAddressBase58 = ed25519PublicKeyToSolanaAddress(stealthAddress.address);
1113
+ const stealthPubkey = new PublicKey(stealthAddressBase58);
1114
+ const ephemeralPubkeyBase58 = ed25519PublicKeyToSolanaAddress(
1115
+ stealthAddress.ephemeralPublicKey
1116
+ );
1117
+ const stealthATA = await getAssociatedTokenAddress(
1118
+ mint,
1119
+ stealthPubkey,
1120
+ true
1121
+ // allowOwnerOffCurve - stealth addresses may be off-curve
1122
+ );
1123
+ const transaction = new Transaction();
1124
+ let stealthATAExists = false;
1125
+ try {
1126
+ await getAccount(connection, stealthATA);
1127
+ stealthATAExists = true;
1128
+ } catch {
1129
+ stealthATAExists = false;
1130
+ }
1131
+ if (!stealthATAExists) {
1132
+ transaction.add(
1133
+ createAssociatedTokenAccountInstruction(
1134
+ sender,
1135
+ // payer
1136
+ stealthATA,
1137
+ // associatedToken
1138
+ stealthPubkey,
1139
+ // owner
1140
+ mint,
1141
+ // mint
1142
+ TOKEN_PROGRAM_ID,
1143
+ ASSOCIATED_TOKEN_PROGRAM_ID
1144
+ )
1145
+ );
1146
+ }
1147
+ transaction.add(
1148
+ createTransferInstruction(
1149
+ senderTokenAccount,
1150
+ // source
1151
+ stealthATA,
1152
+ // destination
1153
+ sender,
1154
+ // owner
1155
+ amount
1156
+ // amount
1157
+ )
1158
+ );
1159
+ const memoContent = createAnnouncementMemo(
1160
+ ephemeralPubkeyBase58,
1161
+ stealthAddress.viewTag.slice(2),
1162
+ // Remove 0x prefix
1163
+ stealthAddressBase58
1164
+ );
1165
+ const memoInstruction = new TransactionInstruction({
1166
+ keys: [],
1167
+ programId: new PublicKey(MEMO_PROGRAM_ID),
1168
+ data: Buffer.from(memoContent, "utf-8")
1169
+ });
1170
+ transaction.add(memoInstruction);
1171
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
1172
+ transaction.recentBlockhash = blockhash;
1173
+ transaction.lastValidBlockHeight = lastValidBlockHeight;
1174
+ transaction.feePayer = sender;
1175
+ const signedTx = await signTransaction(transaction);
1176
+ const txSignature = await connection.sendRawTransaction(signedTx.serialize(), {
1177
+ skipPreflight: false,
1178
+ preflightCommitment: "confirmed"
1179
+ });
1180
+ await connection.confirmTransaction(
1181
+ {
1182
+ signature: txSignature,
1183
+ blockhash,
1184
+ lastValidBlockHeight
1185
+ },
1186
+ "confirmed"
1187
+ );
1188
+ const cluster = detectCluster(connection.rpcEndpoint);
1189
+ return {
1190
+ txSignature,
1191
+ stealthAddress: stealthAddressBase58,
1192
+ ephemeralPublicKey: ephemeralPubkeyBase58,
1193
+ viewTag: stealthAddress.viewTag,
1194
+ explorerUrl: getExplorerUrl(txSignature, cluster),
1195
+ cluster
1196
+ };
1197
+ }
1198
+ async function estimatePrivateTransferFee(connection, needsATACreation = true) {
1199
+ let fee = ESTIMATED_TX_FEE_LAMPORTS;
1200
+ if (needsATACreation) {
1201
+ const rentExemption = await connection.getMinimumBalanceForRentExemption(165);
1202
+ fee += BigInt(rentExemption);
1203
+ }
1204
+ return fee;
1205
+ }
1206
+ async function hasTokenAccount(connection, stealthAddress, mint) {
1207
+ try {
1208
+ const stealthPubkey = new PublicKey(stealthAddress);
1209
+ const ata = await getAssociatedTokenAddress(mint, stealthPubkey, true);
1210
+ await getAccount(connection, ata);
1211
+ return true;
1212
+ } catch {
1213
+ return false;
1214
+ }
1215
+ }
1216
+ function detectCluster(endpoint) {
1217
+ if (endpoint.includes("devnet")) {
1218
+ return "devnet";
1219
+ }
1220
+ if (endpoint.includes("testnet")) {
1221
+ return "testnet";
1222
+ }
1223
+ if (endpoint.includes("localhost") || endpoint.includes("127.0.0.1")) {
1224
+ return "localnet";
1225
+ }
1226
+ return "mainnet-beta";
1227
+ }
1228
+
1229
+ // src/chains/solana/scan.ts
1230
+ import {
1231
+ PublicKey as PublicKey2,
1232
+ Transaction as Transaction2,
1233
+ Keypair
1234
+ } from "@solana/web3.js";
1235
+ import {
1236
+ getAssociatedTokenAddress as getAssociatedTokenAddress2,
1237
+ createTransferInstruction as createTransferInstruction2,
1238
+ getAccount as getAccount2
1239
+ } from "@solana/spl-token";
1240
+ import { hexToBytes as hexToBytes2 } from "@noble/hashes/utils";
1241
+ async function scanForPayments(params) {
1242
+ const {
1243
+ connection,
1244
+ viewingPrivateKey,
1245
+ spendingPublicKey,
1246
+ fromSlot,
1247
+ toSlot,
1248
+ limit = 100
1249
+ } = params;
1250
+ const results = [];
1251
+ const memoProgram = new PublicKey2(MEMO_PROGRAM_ID);
1252
+ try {
1253
+ const signatures = await connection.getSignaturesForAddress(
1254
+ memoProgram,
1255
+ {
1256
+ limit,
1257
+ minContextSlot: fromSlot
1258
+ }
1259
+ );
1260
+ const filteredSignatures = toSlot ? signatures.filter((s) => s.slot <= toSlot) : signatures;
1261
+ for (const sigInfo of filteredSignatures) {
1262
+ try {
1263
+ const tx = await connection.getTransaction(sigInfo.signature, {
1264
+ maxSupportedTransactionVersion: 0
1265
+ });
1266
+ if (!tx?.meta?.logMessages) continue;
1267
+ for (const log of tx.meta.logMessages) {
1268
+ if (!log.includes(SIP_MEMO_PREFIX)) continue;
1269
+ const memoMatch = log.match(/Program log: (.+)/);
1270
+ if (!memoMatch) continue;
1271
+ const memoContent = memoMatch[1];
1272
+ const announcement = parseAnnouncement(memoContent);
1273
+ if (!announcement) continue;
1274
+ const ephemeralPubKeyHex = solanaAddressToEd25519PublicKey(
1275
+ announcement.ephemeralPublicKey
1276
+ );
1277
+ const stealthAddressToCheck = {
1278
+ address: announcement.stealthAddress ? solanaAddressToEd25519PublicKey(announcement.stealthAddress) : "0x" + "00".repeat(32),
1279
+ // Will be computed
1280
+ ephemeralPublicKey: ephemeralPubKeyHex,
1281
+ viewTag: "0x" + announcement.viewTag
1282
+ };
1283
+ const isOurs = checkEd25519StealthAddress(
1284
+ stealthAddressToCheck,
1285
+ viewingPrivateKey,
1286
+ spendingPublicKey
1287
+ );
1288
+ if (isOurs) {
1289
+ const transferInfo = parseTokenTransfer(tx);
1290
+ if (transferInfo) {
1291
+ results.push({
1292
+ stealthAddress: announcement.stealthAddress || "",
1293
+ ephemeralPublicKey: announcement.ephemeralPublicKey,
1294
+ amount: transferInfo.amount,
1295
+ mint: transferInfo.mint,
1296
+ tokenSymbol: getTokenSymbol(transferInfo.mint),
1297
+ txSignature: sigInfo.signature,
1298
+ slot: sigInfo.slot,
1299
+ timestamp: sigInfo.blockTime || 0
1300
+ });
1301
+ }
1302
+ }
1303
+ }
1304
+ } catch (err) {
1305
+ console.warn(`Failed to parse tx ${sigInfo.signature}:`, err);
1306
+ }
1307
+ }
1308
+ } catch (err) {
1309
+ console.error("Scan failed:", err);
1310
+ throw new Error(`Failed to scan for payments: ${err}`);
1311
+ }
1312
+ return results;
1313
+ }
1314
+ async function claimStealthPayment(params) {
1315
+ const {
1316
+ connection,
1317
+ stealthAddress,
1318
+ ephemeralPublicKey,
1319
+ viewingPrivateKey,
1320
+ spendingPrivateKey,
1321
+ destinationAddress,
1322
+ mint
1323
+ } = params;
1324
+ const stealthAddressHex = solanaAddressToEd25519PublicKey(stealthAddress);
1325
+ const ephemeralPubKeyHex = solanaAddressToEd25519PublicKey(ephemeralPublicKey);
1326
+ const stealthAddressObj = {
1327
+ address: stealthAddressHex,
1328
+ ephemeralPublicKey: ephemeralPubKeyHex,
1329
+ viewTag: "0x00"
1330
+ // Not needed for derivation
1331
+ };
1332
+ const recovery = deriveEd25519StealthPrivateKey(
1333
+ stealthAddressObj,
1334
+ spendingPrivateKey,
1335
+ viewingPrivateKey
1336
+ );
1337
+ const stealthPrivKeyBytes = hexToBytes2(recovery.privateKey.slice(2));
1338
+ const stealthPubkey = new PublicKey2(stealthAddress);
1339
+ const stealthKeypair = Keypair.fromSecretKey(
1340
+ new Uint8Array([...stealthPrivKeyBytes, ...stealthPubkey.toBytes()])
1341
+ );
1342
+ const stealthATA = await getAssociatedTokenAddress2(
1343
+ mint,
1344
+ stealthPubkey,
1345
+ true
1346
+ );
1347
+ const destinationPubkey = new PublicKey2(destinationAddress);
1348
+ const destinationATA = await getAssociatedTokenAddress2(
1349
+ mint,
1350
+ destinationPubkey
1351
+ );
1352
+ const stealthAccount = await getAccount2(connection, stealthATA);
1353
+ const amount = stealthAccount.amount;
1354
+ const transaction = new Transaction2();
1355
+ transaction.add(
1356
+ createTransferInstruction2(
1357
+ stealthATA,
1358
+ destinationATA,
1359
+ stealthPubkey,
1360
+ amount
1361
+ )
1362
+ );
1363
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
1364
+ transaction.recentBlockhash = blockhash;
1365
+ transaction.lastValidBlockHeight = lastValidBlockHeight;
1366
+ transaction.feePayer = stealthPubkey;
1367
+ transaction.sign(stealthKeypair);
1368
+ const txSignature = await connection.sendRawTransaction(
1369
+ transaction.serialize(),
1370
+ {
1371
+ skipPreflight: false,
1372
+ preflightCommitment: "confirmed"
1373
+ }
1374
+ );
1375
+ await connection.confirmTransaction(
1376
+ {
1377
+ signature: txSignature,
1378
+ blockhash,
1379
+ lastValidBlockHeight
1380
+ },
1381
+ "confirmed"
1382
+ );
1383
+ const cluster = detectCluster2(connection.rpcEndpoint);
1384
+ return {
1385
+ txSignature,
1386
+ destinationAddress,
1387
+ amount,
1388
+ explorerUrl: getExplorerUrl(txSignature, cluster)
1389
+ };
1390
+ }
1391
+ async function getStealthBalance(connection, stealthAddress, mint) {
1392
+ try {
1393
+ const stealthPubkey = new PublicKey2(stealthAddress);
1394
+ const ata = await getAssociatedTokenAddress2(mint, stealthPubkey, true);
1395
+ const account = await getAccount2(connection, ata);
1396
+ return account.amount;
1397
+ } catch {
1398
+ return 0n;
1399
+ }
1400
+ }
1401
+ function parseTokenTransfer(tx) {
1402
+ if (!tx?.meta?.postTokenBalances || !tx.meta.preTokenBalances) {
1403
+ return null;
1404
+ }
1405
+ for (let i = 0; i < tx.meta.postTokenBalances.length; i++) {
1406
+ const post = tx.meta.postTokenBalances[i];
1407
+ const pre = tx.meta.preTokenBalances.find(
1408
+ (p) => p.accountIndex === post.accountIndex
1409
+ );
1410
+ const postAmount = BigInt(post.uiTokenAmount.amount);
1411
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
1412
+ if (postAmount > preAmount) {
1413
+ return {
1414
+ mint: post.mint,
1415
+ amount: postAmount - preAmount
1416
+ };
1417
+ }
1418
+ }
1419
+ return null;
1420
+ }
1421
+ function getTokenSymbol(mint) {
1422
+ for (const [symbol, address] of Object.entries(SOLANA_TOKEN_MINTS)) {
1423
+ if (address === mint) {
1424
+ return symbol;
1425
+ }
1426
+ }
1427
+ return void 0;
1428
+ }
1429
+ function detectCluster2(endpoint) {
1430
+ if (endpoint.includes("devnet")) {
1431
+ return "devnet";
1432
+ }
1433
+ if (endpoint.includes("testnet")) {
1434
+ return "testnet";
1435
+ }
1436
+ if (endpoint.includes("localhost") || endpoint.includes("127.0.0.1")) {
1437
+ return "localnet";
1438
+ }
1439
+ return "mainnet-beta";
1440
+ }
1441
+
1442
+ export {
1443
+ isValidChainId,
1444
+ isValidPrivacyLevel,
1445
+ isValidHex,
1446
+ isValidHexLength,
1447
+ isValidAmount,
1448
+ isNonNegativeAmount,
1449
+ isValidSlippage,
1450
+ isValidStealthMetaAddress,
1451
+ isValidCompressedPublicKey,
1452
+ isValidEd25519PublicKey,
1453
+ isValidPrivateKey,
1454
+ validateAsset,
1455
+ validateIntentInput,
1456
+ validateIntentOutput,
1457
+ validateCreateIntentParams,
1458
+ validateViewingKey,
1459
+ isValidScalar,
1460
+ validateScalar,
1461
+ getChainAddressType,
1462
+ isAddressValidForChain,
1463
+ secureWipe,
1464
+ withSecureBuffer,
1465
+ withSecureBufferSync,
1466
+ secureWipeAll,
1467
+ generateStealthMetaAddress,
1468
+ generateStealthAddress,
1469
+ deriveStealthPrivateKey,
1470
+ checkStealthAddress,
1471
+ encodeStealthMetaAddress,
1472
+ decodeStealthMetaAddress,
1473
+ publicKeyToEthAddress,
1474
+ isEd25519Chain,
1475
+ getCurveForChain,
1476
+ generateEd25519StealthMetaAddress,
1477
+ generateEd25519StealthAddress,
1478
+ deriveEd25519StealthPrivateKey,
1479
+ checkEd25519StealthAddress,
1480
+ ed25519PublicKeyToSolanaAddress,
1481
+ isValidSolanaAddress,
1482
+ solanaAddressToEd25519PublicKey,
1483
+ ed25519PublicKeyToNearAddress,
1484
+ nearAddressToEd25519PublicKey,
1485
+ isValidNearImplicitAddress,
1486
+ isValidNearAccountId,
1487
+ parseAnnouncement,
1488
+ createAnnouncementMemo,
1489
+ sendPrivateSPLTransfer,
1490
+ estimatePrivateTransferFee,
1491
+ hasTokenAccount,
1492
+ scanForPayments,
1493
+ claimStealthPayment,
1494
+ getStealthBalance
1495
+ };