@sip-protocol/sdk 0.7.1 → 0.7.3

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.
Files changed (50) hide show
  1. package/dist/browser.d.mts +1 -1
  2. package/dist/browser.d.ts +1 -1
  3. package/dist/browser.js +2926 -341
  4. package/dist/browser.mjs +48 -2
  5. package/dist/chunk-2XIVXWHA.mjs +1930 -0
  6. package/dist/chunk-3M3HNQCW.mjs +18253 -0
  7. package/dist/chunk-7RFRWDCW.mjs +1504 -0
  8. package/dist/chunk-F6F73W35.mjs +16166 -0
  9. package/dist/chunk-OFDBEIEK.mjs +16166 -0
  10. package/dist/chunk-SF7YSLF5.mjs +1515 -0
  11. package/dist/chunk-WWUSGOXE.mjs +17129 -0
  12. package/dist/index-8MQz13eJ.d.mts +13746 -0
  13. package/dist/index-B71aXVzk.d.ts +13264 -0
  14. package/dist/index-DIBZHOOQ.d.ts +13746 -0
  15. package/dist/index-pOIIuwfV.d.mts +13264 -0
  16. package/dist/index.d.mts +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +2911 -326
  19. package/dist/index.mjs +48 -2
  20. package/dist/solana-4O4K45VU.mjs +46 -0
  21. package/dist/solana-NDABAZ6P.mjs +56 -0
  22. package/dist/solana-ZYO63LY5.mjs +46 -0
  23. package/package.json +3 -3
  24. package/src/chains/solana/index.ts +24 -0
  25. package/src/chains/solana/providers/generic.ts +160 -0
  26. package/src/chains/solana/providers/helius.ts +249 -0
  27. package/src/chains/solana/providers/index.ts +54 -0
  28. package/src/chains/solana/providers/interface.ts +178 -0
  29. package/src/chains/solana/providers/webhook.ts +519 -0
  30. package/src/chains/solana/scan.ts +88 -8
  31. package/src/chains/solana/types.ts +20 -1
  32. package/src/compliance/index.ts +14 -0
  33. package/src/compliance/range-sas.ts +591 -0
  34. package/src/index.ts +99 -0
  35. package/src/privacy-backends/index.ts +86 -0
  36. package/src/privacy-backends/interface.ts +263 -0
  37. package/src/privacy-backends/privacycash-types.ts +278 -0
  38. package/src/privacy-backends/privacycash.ts +460 -0
  39. package/src/privacy-backends/registry.ts +278 -0
  40. package/src/privacy-backends/router.ts +346 -0
  41. package/src/privacy-backends/sip-native.ts +253 -0
  42. package/src/proofs/noir.ts +1 -1
  43. package/src/surveillance/algorithms/address-reuse.ts +143 -0
  44. package/src/surveillance/algorithms/cluster.ts +247 -0
  45. package/src/surveillance/algorithms/exchange.ts +295 -0
  46. package/src/surveillance/algorithms/temporal.ts +337 -0
  47. package/src/surveillance/analyzer.ts +442 -0
  48. package/src/surveillance/index.ts +64 -0
  49. package/src/surveillance/scoring.ts +372 -0
  50. package/src/surveillance/types.ts +264 -0
@@ -0,0 +1,1930 @@
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 viewTagHex = stealthAddress.viewTag.toString(16).padStart(2, "0");
1160
+ const memoContent = createAnnouncementMemo(
1161
+ ephemeralPubkeyBase58,
1162
+ viewTagHex,
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: viewTagHex,
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
+ import { ed25519 as ed255192 } from "@noble/curves/ed25519";
1242
+ async function scanForPayments(params) {
1243
+ const {
1244
+ connection,
1245
+ viewingPrivateKey,
1246
+ spendingPublicKey,
1247
+ fromSlot,
1248
+ toSlot,
1249
+ limit = 100,
1250
+ provider
1251
+ } = params;
1252
+ const results = [];
1253
+ const memoProgram = new PublicKey2(MEMO_PROGRAM_ID);
1254
+ try {
1255
+ const signatures = await connection.getSignaturesForAddress(
1256
+ memoProgram,
1257
+ {
1258
+ limit,
1259
+ minContextSlot: fromSlot
1260
+ }
1261
+ );
1262
+ const filteredSignatures = toSlot ? signatures.filter((s) => s.slot <= toSlot) : signatures;
1263
+ for (const sigInfo of filteredSignatures) {
1264
+ try {
1265
+ const tx = await connection.getTransaction(sigInfo.signature, {
1266
+ maxSupportedTransactionVersion: 0
1267
+ });
1268
+ if (!tx?.meta?.logMessages) continue;
1269
+ for (const log of tx.meta.logMessages) {
1270
+ if (!log.includes(SIP_MEMO_PREFIX)) continue;
1271
+ const memoMatch = log.match(/Program log: (.+)/);
1272
+ if (!memoMatch) continue;
1273
+ const memoContent = memoMatch[1];
1274
+ const announcement = parseAnnouncement(memoContent);
1275
+ if (!announcement) continue;
1276
+ const ephemeralPubKeyHex = solanaAddressToEd25519PublicKey(
1277
+ announcement.ephemeralPublicKey
1278
+ );
1279
+ const viewTagNumber = parseInt(announcement.viewTag, 16);
1280
+ const stealthAddressToCheck = {
1281
+ address: announcement.stealthAddress ? solanaAddressToEd25519PublicKey(announcement.stealthAddress) : "0x" + "00".repeat(32),
1282
+ // Will be computed
1283
+ ephemeralPublicKey: ephemeralPubKeyHex,
1284
+ viewTag: viewTagNumber
1285
+ };
1286
+ const isOurs = checkEd25519StealthAddress(
1287
+ stealthAddressToCheck,
1288
+ viewingPrivateKey,
1289
+ spendingPublicKey
1290
+ );
1291
+ if (isOurs) {
1292
+ const transferInfo = parseTokenTransfer(tx);
1293
+ if (transferInfo) {
1294
+ let amount = transferInfo.amount;
1295
+ const tokenSymbol = getTokenSymbol(transferInfo.mint);
1296
+ if (provider && announcement.stealthAddress) {
1297
+ try {
1298
+ const balance = await provider.getTokenBalance(
1299
+ announcement.stealthAddress,
1300
+ transferInfo.mint
1301
+ );
1302
+ if (balance > 0n) {
1303
+ amount = balance;
1304
+ }
1305
+ } catch {
1306
+ }
1307
+ }
1308
+ results.push({
1309
+ stealthAddress: announcement.stealthAddress || "",
1310
+ ephemeralPublicKey: announcement.ephemeralPublicKey,
1311
+ amount,
1312
+ mint: transferInfo.mint,
1313
+ tokenSymbol,
1314
+ txSignature: sigInfo.signature,
1315
+ slot: sigInfo.slot,
1316
+ timestamp: sigInfo.blockTime || 0
1317
+ });
1318
+ }
1319
+ }
1320
+ }
1321
+ } catch (err) {
1322
+ console.warn(`Failed to parse tx ${sigInfo.signature}:`, err);
1323
+ }
1324
+ }
1325
+ } catch (err) {
1326
+ console.error("Scan failed:", err);
1327
+ throw new Error(`Failed to scan for payments: ${err}`);
1328
+ }
1329
+ return results;
1330
+ }
1331
+ async function claimStealthPayment(params) {
1332
+ const {
1333
+ connection,
1334
+ stealthAddress,
1335
+ ephemeralPublicKey,
1336
+ viewingPrivateKey,
1337
+ spendingPrivateKey,
1338
+ destinationAddress,
1339
+ mint
1340
+ } = params;
1341
+ const stealthAddressHex = solanaAddressToEd25519PublicKey(stealthAddress);
1342
+ const ephemeralPubKeyHex = solanaAddressToEd25519PublicKey(ephemeralPublicKey);
1343
+ const stealthAddressObj = {
1344
+ address: stealthAddressHex,
1345
+ ephemeralPublicKey: ephemeralPubKeyHex,
1346
+ viewTag: 0
1347
+ // Not needed for derivation
1348
+ };
1349
+ const recovery = deriveEd25519StealthPrivateKey(
1350
+ stealthAddressObj,
1351
+ spendingPrivateKey,
1352
+ viewingPrivateKey
1353
+ );
1354
+ const stealthPrivKeyBytes = hexToBytes2(recovery.privateKey.slice(2));
1355
+ const stealthPubkey = new PublicKey2(stealthAddress);
1356
+ const expectedPubKeyBytes = stealthPubkey.toBytes();
1357
+ const scalarBigInt = bytesToBigIntLE2(stealthPrivKeyBytes);
1358
+ const ED25519_ORDER2 = 2n ** 252n + 27742317777372353535851937790883648493n;
1359
+ let validScalar = scalarBigInt % ED25519_ORDER2;
1360
+ if (validScalar === 0n) validScalar = 1n;
1361
+ const derivedPubKeyBytes = ed255192.ExtendedPoint.BASE.multiply(validScalar).toRawBytes();
1362
+ if (!derivedPubKeyBytes.every((b, i) => b === expectedPubKeyBytes[i])) {
1363
+ throw new Error(
1364
+ "Stealth key derivation failed: derived private key does not produce expected public key. This may indicate incorrect spending/viewing keys or corrupted announcement data."
1365
+ );
1366
+ }
1367
+ const stealthKeypair = Keypair.fromSecretKey(
1368
+ new Uint8Array([...stealthPrivKeyBytes, ...expectedPubKeyBytes])
1369
+ );
1370
+ const stealthATA = await getAssociatedTokenAddress2(
1371
+ mint,
1372
+ stealthPubkey,
1373
+ true
1374
+ );
1375
+ const destinationPubkey = new PublicKey2(destinationAddress);
1376
+ const destinationATA = await getAssociatedTokenAddress2(
1377
+ mint,
1378
+ destinationPubkey
1379
+ );
1380
+ const stealthAccount = await getAccount2(connection, stealthATA);
1381
+ const amount = stealthAccount.amount;
1382
+ const transaction = new Transaction2();
1383
+ transaction.add(
1384
+ createTransferInstruction2(
1385
+ stealthATA,
1386
+ destinationATA,
1387
+ stealthPubkey,
1388
+ amount
1389
+ )
1390
+ );
1391
+ const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
1392
+ transaction.recentBlockhash = blockhash;
1393
+ transaction.lastValidBlockHeight = lastValidBlockHeight;
1394
+ transaction.feePayer = stealthPubkey;
1395
+ transaction.sign(stealthKeypair);
1396
+ const txSignature = await connection.sendRawTransaction(
1397
+ transaction.serialize(),
1398
+ {
1399
+ skipPreflight: false,
1400
+ preflightCommitment: "confirmed"
1401
+ }
1402
+ );
1403
+ await connection.confirmTransaction(
1404
+ {
1405
+ signature: txSignature,
1406
+ blockhash,
1407
+ lastValidBlockHeight
1408
+ },
1409
+ "confirmed"
1410
+ );
1411
+ const cluster = detectCluster2(connection.rpcEndpoint);
1412
+ return {
1413
+ txSignature,
1414
+ destinationAddress,
1415
+ amount,
1416
+ explorerUrl: getExplorerUrl(txSignature, cluster)
1417
+ };
1418
+ }
1419
+ async function getStealthBalance(connection, stealthAddress, mint, provider) {
1420
+ if (provider) {
1421
+ try {
1422
+ return await provider.getTokenBalance(stealthAddress, mint.toBase58());
1423
+ } catch {
1424
+ }
1425
+ }
1426
+ try {
1427
+ const stealthPubkey = new PublicKey2(stealthAddress);
1428
+ const ata = await getAssociatedTokenAddress2(mint, stealthPubkey, true);
1429
+ const account = await getAccount2(connection, ata);
1430
+ return account.amount;
1431
+ } catch {
1432
+ return 0n;
1433
+ }
1434
+ }
1435
+ function parseTokenTransfer(tx) {
1436
+ if (!tx?.meta?.postTokenBalances || !tx.meta.preTokenBalances) {
1437
+ return null;
1438
+ }
1439
+ for (let i = 0; i < tx.meta.postTokenBalances.length; i++) {
1440
+ const post = tx.meta.postTokenBalances[i];
1441
+ const pre = tx.meta.preTokenBalances.find(
1442
+ (p) => p.accountIndex === post.accountIndex
1443
+ );
1444
+ const postAmount = BigInt(post.uiTokenAmount.amount);
1445
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
1446
+ if (postAmount > preAmount) {
1447
+ return {
1448
+ mint: post.mint,
1449
+ amount: postAmount - preAmount
1450
+ };
1451
+ }
1452
+ }
1453
+ return null;
1454
+ }
1455
+ function getTokenSymbol(mint) {
1456
+ for (const [symbol, address] of Object.entries(SOLANA_TOKEN_MINTS)) {
1457
+ if (address === mint) {
1458
+ return symbol;
1459
+ }
1460
+ }
1461
+ return void 0;
1462
+ }
1463
+ function detectCluster2(endpoint) {
1464
+ if (endpoint.includes("devnet")) {
1465
+ return "devnet";
1466
+ }
1467
+ if (endpoint.includes("testnet")) {
1468
+ return "testnet";
1469
+ }
1470
+ if (endpoint.includes("localhost") || endpoint.includes("127.0.0.1")) {
1471
+ return "localnet";
1472
+ }
1473
+ return "mainnet-beta";
1474
+ }
1475
+ function bytesToBigIntLE2(bytes) {
1476
+ let result = 0n;
1477
+ for (let i = bytes.length - 1; i >= 0; i--) {
1478
+ result = result << 8n | BigInt(bytes[i]);
1479
+ }
1480
+ return result;
1481
+ }
1482
+
1483
+ // src/chains/solana/providers/helius.ts
1484
+ var HeliusProvider = class {
1485
+ name = "helius";
1486
+ apiKey;
1487
+ cluster;
1488
+ rpcUrl;
1489
+ restUrl;
1490
+ constructor(config) {
1491
+ if (!config.apiKey) {
1492
+ throw new Error("Helius API key is required. Get one at https://dev.helius.xyz");
1493
+ }
1494
+ this.apiKey = config.apiKey;
1495
+ this.cluster = config.cluster ?? "mainnet-beta";
1496
+ this.rpcUrl = this.cluster === "devnet" ? `https://devnet.helius-rpc.com/?api-key=${this.apiKey}` : `https://mainnet.helius-rpc.com/?api-key=${this.apiKey}`;
1497
+ this.restUrl = this.cluster === "devnet" ? `https://api-devnet.helius.xyz/v0` : `https://api.helius.xyz/v0`;
1498
+ }
1499
+ /**
1500
+ * Get all token assets owned by an address using DAS API
1501
+ *
1502
+ * Uses getAssetsByOwner for comprehensive asset information including
1503
+ * NFTs and fungible tokens with metadata.
1504
+ */
1505
+ async getAssetsByOwner(owner) {
1506
+ const assets = [];
1507
+ let page = 1;
1508
+ const limit = 1e3;
1509
+ let hasMore = true;
1510
+ while (hasMore) {
1511
+ const response = await fetch(this.rpcUrl, {
1512
+ method: "POST",
1513
+ headers: { "Content-Type": "application/json" },
1514
+ body: JSON.stringify({
1515
+ jsonrpc: "2.0",
1516
+ id: `sip-${Date.now()}`,
1517
+ method: "getAssetsByOwner",
1518
+ params: {
1519
+ ownerAddress: owner,
1520
+ page,
1521
+ limit,
1522
+ displayOptions: {
1523
+ showFungible: true,
1524
+ showNativeBalance: false
1525
+ }
1526
+ }
1527
+ })
1528
+ });
1529
+ if (!response.ok) {
1530
+ throw new Error(`Helius API error: ${response.status} ${response.statusText}`);
1531
+ }
1532
+ const data = await response.json();
1533
+ if (data.error) {
1534
+ throw new Error(`Helius RPC error: ${data.error.message} (code: ${data.error.code})`);
1535
+ }
1536
+ if (data.result?.items) {
1537
+ for (const item of data.result.items) {
1538
+ if (item.interface !== "FungibleToken" && item.interface !== "FungibleAsset") {
1539
+ continue;
1540
+ }
1541
+ const tokenInfo = item.token_info;
1542
+ if (!tokenInfo?.balance) continue;
1543
+ const balanceValue = typeof tokenInfo.balance === "string" ? BigInt(tokenInfo.balance) : BigInt(Math.floor(tokenInfo.balance));
1544
+ assets.push({
1545
+ mint: item.id,
1546
+ amount: balanceValue,
1547
+ decimals: tokenInfo.decimals ?? 0,
1548
+ symbol: tokenInfo.symbol ?? item.content?.metadata?.symbol,
1549
+ name: item.content?.metadata?.name,
1550
+ logoUri: item.content?.links?.image
1551
+ });
1552
+ }
1553
+ }
1554
+ hasMore = data.result?.items?.length === limit;
1555
+ page++;
1556
+ if (page > 100) {
1557
+ console.warn("[HeliusProvider] Reached page limit (100), stopping pagination");
1558
+ break;
1559
+ }
1560
+ }
1561
+ return assets;
1562
+ }
1563
+ /**
1564
+ * Get token balance for a specific mint using Balances API
1565
+ *
1566
+ * More efficient than getAssetsByOwner when you only need one token's balance.
1567
+ */
1568
+ async getTokenBalance(owner, mint) {
1569
+ try {
1570
+ const url = `${this.restUrl}/addresses/${owner}/balances?api-key=${this.apiKey}`;
1571
+ const response = await fetch(url);
1572
+ if (!response.ok) {
1573
+ const assets = await this.getAssetsByOwner(owner);
1574
+ const asset = assets.find((a) => a.mint === mint);
1575
+ return asset?.amount ?? 0n;
1576
+ }
1577
+ const data = await response.json();
1578
+ const token = data.tokens?.find((t) => t.mint === mint);
1579
+ return token ? BigInt(token.amount) : 0n;
1580
+ } catch (error) {
1581
+ console.warn("[HeliusProvider] getTokenBalance error, falling back to DAS:", error);
1582
+ const assets = await this.getAssetsByOwner(owner);
1583
+ const asset = assets.find((a) => a.mint === mint);
1584
+ return asset?.amount ?? 0n;
1585
+ }
1586
+ }
1587
+ /**
1588
+ * Check if provider supports real-time subscriptions
1589
+ *
1590
+ * Helius supports webhooks for real-time notifications,
1591
+ * but that requires server-side setup. Client-side subscriptions
1592
+ * are not directly supported.
1593
+ */
1594
+ supportsSubscriptions() {
1595
+ return false;
1596
+ }
1597
+ };
1598
+
1599
+ // src/chains/solana/providers/generic.ts
1600
+ import {
1601
+ Connection,
1602
+ PublicKey as PublicKey3
1603
+ } from "@solana/web3.js";
1604
+ import {
1605
+ TOKEN_PROGRAM_ID as TOKEN_PROGRAM_ID2,
1606
+ getAssociatedTokenAddress as getAssociatedTokenAddress3,
1607
+ getAccount as getAccount3
1608
+ } from "@solana/spl-token";
1609
+ var CLUSTER_ENDPOINTS = {
1610
+ "mainnet-beta": "https://api.mainnet-beta.solana.com",
1611
+ devnet: "https://api.devnet.solana.com",
1612
+ testnet: "https://api.testnet.solana.com"
1613
+ };
1614
+ function validateSolanaAddress(address, paramName) {
1615
+ try {
1616
+ return new PublicKey3(address);
1617
+ } catch {
1618
+ throw new Error(`Invalid Solana address for ${paramName}: ${address}`);
1619
+ }
1620
+ }
1621
+ var GenericProvider = class {
1622
+ name = "generic";
1623
+ connection;
1624
+ constructor(config) {
1625
+ if (config.connection) {
1626
+ this.connection = config.connection;
1627
+ } else {
1628
+ const endpoint = config.endpoint ?? CLUSTER_ENDPOINTS[config.cluster ?? "mainnet-beta"];
1629
+ this.connection = new Connection(endpoint, "confirmed");
1630
+ }
1631
+ }
1632
+ /**
1633
+ * Get all token assets owned by an address using getParsedTokenAccountsByOwner
1634
+ *
1635
+ * Note: This is less efficient than Helius DAS API for large wallets,
1636
+ * but works with any RPC endpoint.
1637
+ */
1638
+ async getAssetsByOwner(owner) {
1639
+ const ownerPubkey = validateSolanaAddress(owner, "owner");
1640
+ const accounts = await this.connection.getParsedTokenAccountsByOwner(
1641
+ ownerPubkey,
1642
+ { programId: TOKEN_PROGRAM_ID2 }
1643
+ );
1644
+ const assets = [];
1645
+ for (const { account } of accounts.value) {
1646
+ const parsed = account.data.parsed;
1647
+ if (parsed.type !== "account") continue;
1648
+ const info = parsed.info;
1649
+ const amount = BigInt(info.tokenAmount.amount);
1650
+ if (amount === 0n) continue;
1651
+ assets.push({
1652
+ mint: info.mint,
1653
+ amount,
1654
+ decimals: info.tokenAmount.decimals,
1655
+ // Generic RPC doesn't provide symbol/name, those need metadata lookup
1656
+ symbol: void 0,
1657
+ name: void 0,
1658
+ logoUri: void 0
1659
+ });
1660
+ }
1661
+ return assets;
1662
+ }
1663
+ /**
1664
+ * Get token balance for a specific mint
1665
+ *
1666
+ * Uses getAccount on the associated token address.
1667
+ */
1668
+ async getTokenBalance(owner, mint) {
1669
+ const ownerPubkey = validateSolanaAddress(owner, "owner");
1670
+ const mintPubkey = validateSolanaAddress(mint, "mint");
1671
+ try {
1672
+ const ata = await getAssociatedTokenAddress3(
1673
+ mintPubkey,
1674
+ ownerPubkey,
1675
+ true
1676
+ // allowOwnerOffCurve for PDAs
1677
+ );
1678
+ const account = await getAccount3(this.connection, ata);
1679
+ return account.amount;
1680
+ } catch {
1681
+ return 0n;
1682
+ }
1683
+ }
1684
+ /**
1685
+ * Check if provider supports real-time subscriptions
1686
+ *
1687
+ * Generic RPC supports WebSocket subscriptions but they're not
1688
+ * efficient for monitoring token transfers. Returns false.
1689
+ */
1690
+ supportsSubscriptions() {
1691
+ return false;
1692
+ }
1693
+ /**
1694
+ * Get the underlying Connection object
1695
+ *
1696
+ * Useful for advanced operations that need direct RPC access.
1697
+ */
1698
+ getConnection() {
1699
+ return this.connection;
1700
+ }
1701
+ };
1702
+
1703
+ // src/chains/solana/providers/interface.ts
1704
+ function createProvider(type, config) {
1705
+ switch (type) {
1706
+ case "helius":
1707
+ return new HeliusProvider(config);
1708
+ case "generic":
1709
+ return new GenericProvider(config);
1710
+ case "quicknode":
1711
+ case "triton":
1712
+ throw new Error(
1713
+ `Provider '${type}' is not yet implemented. Use 'helius' or 'generic' for now. See https://github.com/sip-protocol/sip-protocol/issues/${type === "quicknode" ? "494" : "495"}`
1714
+ );
1715
+ default:
1716
+ throw new Error(`Unknown provider type: ${type}`);
1717
+ }
1718
+ }
1719
+
1720
+ // src/chains/solana/providers/webhook.ts
1721
+ function createWebhookHandler(config) {
1722
+ const { viewingPrivateKey, spendingPublicKey, onPaymentFound, onError } = config;
1723
+ if (!viewingPrivateKey || !viewingPrivateKey.startsWith("0x")) {
1724
+ throw new ValidationError("viewingPrivateKey must be a valid hex string starting with 0x", "viewingPrivateKey");
1725
+ }
1726
+ if (!spendingPublicKey || !spendingPublicKey.startsWith("0x")) {
1727
+ throw new ValidationError("spendingPublicKey must be a valid hex string starting with 0x", "spendingPublicKey");
1728
+ }
1729
+ if (typeof onPaymentFound !== "function") {
1730
+ throw new ValidationError("onPaymentFound callback is required", "onPaymentFound");
1731
+ }
1732
+ return async (payload) => {
1733
+ const transactions = Array.isArray(payload) ? payload : [payload];
1734
+ const results = [];
1735
+ for (const tx of transactions) {
1736
+ try {
1737
+ if (isRawTransaction(tx)) {
1738
+ const result = await processRawTransaction(
1739
+ tx,
1740
+ viewingPrivateKey,
1741
+ spendingPublicKey,
1742
+ onPaymentFound
1743
+ );
1744
+ results.push(result);
1745
+ } else {
1746
+ results.push({
1747
+ found: false,
1748
+ signature: tx.signature
1749
+ });
1750
+ }
1751
+ } catch (error) {
1752
+ onError?.(error, isRawTransaction(tx) ? tx : void 0);
1753
+ results.push({
1754
+ found: false,
1755
+ signature: getSignature(tx)
1756
+ });
1757
+ }
1758
+ }
1759
+ return results;
1760
+ };
1761
+ }
1762
+ async function processRawTransaction(tx, viewingPrivateKey, spendingPublicKey, onPaymentFound) {
1763
+ const signature = tx.transaction?.signatures?.[0] ?? "unknown";
1764
+ if (tx.meta?.err) {
1765
+ return { found: false, signature };
1766
+ }
1767
+ if (!tx.meta?.logMessages) {
1768
+ return { found: false, signature };
1769
+ }
1770
+ for (const log of tx.meta.logMessages) {
1771
+ if (!log.includes(SIP_MEMO_PREFIX)) continue;
1772
+ const memoMatch = log.match(/Program log: (.+)/);
1773
+ if (!memoMatch) continue;
1774
+ const memoContent = memoMatch[1];
1775
+ const announcement = parseAnnouncement(memoContent);
1776
+ if (!announcement) continue;
1777
+ const ephemeralPubKeyHex = solanaAddressToEd25519PublicKey(
1778
+ announcement.ephemeralPublicKey
1779
+ );
1780
+ const viewTagNumber = parseInt(announcement.viewTag, 16);
1781
+ if (!Number.isFinite(viewTagNumber) || viewTagNumber < 0 || viewTagNumber > 255) {
1782
+ continue;
1783
+ }
1784
+ const stealthAddressToCheck = {
1785
+ address: announcement.stealthAddress ? solanaAddressToEd25519PublicKey(announcement.stealthAddress) : "0x" + "00".repeat(32),
1786
+ ephemeralPublicKey: ephemeralPubKeyHex,
1787
+ viewTag: viewTagNumber
1788
+ };
1789
+ let isOurs = false;
1790
+ try {
1791
+ isOurs = checkEd25519StealthAddress(
1792
+ stealthAddressToCheck,
1793
+ viewingPrivateKey,
1794
+ spendingPublicKey
1795
+ );
1796
+ } catch {
1797
+ continue;
1798
+ }
1799
+ if (isOurs) {
1800
+ const transferInfo = parseTokenTransferFromWebhook(tx);
1801
+ const payment = {
1802
+ stealthAddress: announcement.stealthAddress || "",
1803
+ ephemeralPublicKey: announcement.ephemeralPublicKey,
1804
+ amount: transferInfo?.amount ?? 0n,
1805
+ mint: transferInfo?.mint ?? "",
1806
+ tokenSymbol: transferInfo?.mint ? getTokenSymbol2(transferInfo.mint) : void 0,
1807
+ txSignature: signature,
1808
+ slot: tx.slot,
1809
+ timestamp: tx.blockTime
1810
+ };
1811
+ try {
1812
+ await onPaymentFound(payment);
1813
+ } catch {
1814
+ }
1815
+ return { found: true, payment, signature };
1816
+ }
1817
+ }
1818
+ return { found: false, signature };
1819
+ }
1820
+ function parseTokenTransferFromWebhook(tx) {
1821
+ const { preTokenBalances, postTokenBalances } = tx.meta;
1822
+ if (!postTokenBalances || !preTokenBalances) {
1823
+ return null;
1824
+ }
1825
+ for (const post of postTokenBalances) {
1826
+ const pre = preTokenBalances.find(
1827
+ (p) => p.accountIndex === post.accountIndex
1828
+ );
1829
+ const postAmount = BigInt(post.uiTokenAmount.amount);
1830
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
1831
+ if (postAmount > preAmount) {
1832
+ return {
1833
+ mint: post.mint,
1834
+ amount: postAmount - preAmount
1835
+ };
1836
+ }
1837
+ }
1838
+ return null;
1839
+ }
1840
+ function getTokenSymbol2(mint) {
1841
+ for (const [symbol, address] of Object.entries(SOLANA_TOKEN_MINTS)) {
1842
+ if (address === mint) {
1843
+ return symbol;
1844
+ }
1845
+ }
1846
+ return void 0;
1847
+ }
1848
+ function isRawTransaction(tx) {
1849
+ return typeof tx === "object" && tx !== null && "meta" in tx && "transaction" in tx && Array.isArray(tx.transaction?.signatures);
1850
+ }
1851
+ function getSignature(tx) {
1852
+ if ("signature" in tx && typeof tx.signature === "string") {
1853
+ return tx.signature;
1854
+ }
1855
+ if (isRawTransaction(tx)) {
1856
+ return tx.transaction?.signatures?.[0] ?? "unknown";
1857
+ }
1858
+ return "unknown";
1859
+ }
1860
+ async function processWebhookTransaction(transaction, viewingPrivateKey, spendingPublicKey) {
1861
+ const result = await processRawTransaction(
1862
+ transaction,
1863
+ viewingPrivateKey,
1864
+ spendingPublicKey,
1865
+ () => {
1866
+ }
1867
+ // No-op callback
1868
+ );
1869
+ return result.found ? result.payment ?? null : null;
1870
+ }
1871
+
1872
+ export {
1873
+ isValidChainId,
1874
+ isValidPrivacyLevel,
1875
+ isValidHex,
1876
+ isValidHexLength,
1877
+ isValidAmount,
1878
+ isNonNegativeAmount,
1879
+ isValidSlippage,
1880
+ isValidStealthMetaAddress,
1881
+ isValidCompressedPublicKey,
1882
+ isValidEd25519PublicKey,
1883
+ isValidPrivateKey,
1884
+ validateAsset,
1885
+ validateIntentInput,
1886
+ validateIntentOutput,
1887
+ validateCreateIntentParams,
1888
+ validateViewingKey,
1889
+ isValidScalar,
1890
+ validateScalar,
1891
+ getChainAddressType,
1892
+ isAddressValidForChain,
1893
+ secureWipe,
1894
+ withSecureBuffer,
1895
+ withSecureBufferSync,
1896
+ secureWipeAll,
1897
+ generateStealthMetaAddress,
1898
+ generateStealthAddress,
1899
+ deriveStealthPrivateKey,
1900
+ checkStealthAddress,
1901
+ encodeStealthMetaAddress,
1902
+ decodeStealthMetaAddress,
1903
+ publicKeyToEthAddress,
1904
+ isEd25519Chain,
1905
+ getCurveForChain,
1906
+ generateEd25519StealthMetaAddress,
1907
+ generateEd25519StealthAddress,
1908
+ deriveEd25519StealthPrivateKey,
1909
+ checkEd25519StealthAddress,
1910
+ ed25519PublicKeyToSolanaAddress,
1911
+ isValidSolanaAddress,
1912
+ solanaAddressToEd25519PublicKey,
1913
+ ed25519PublicKeyToNearAddress,
1914
+ nearAddressToEd25519PublicKey,
1915
+ isValidNearImplicitAddress,
1916
+ isValidNearAccountId,
1917
+ parseAnnouncement,
1918
+ createAnnouncementMemo,
1919
+ sendPrivateSPLTransfer,
1920
+ estimatePrivateTransferFee,
1921
+ hasTokenAccount,
1922
+ scanForPayments,
1923
+ claimStealthPayment,
1924
+ getStealthBalance,
1925
+ HeliusProvider,
1926
+ GenericProvider,
1927
+ createProvider,
1928
+ createWebhookHandler,
1929
+ processWebhookTransaction
1930
+ };