stellar-shade 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,817 @@
1
+ import { randomBytes } from '@noble/hashes/utils';
2
+ import { bytesToNumberLE, numberToBytesLE } from '@noble/curves/abstract/utils';
3
+ import { sha256 } from '@noble/hashes/sha256';
4
+ import { ed25519 } from '@noble/curves/ed25519';
5
+ import { sha512 } from '@noble/hashes/sha512';
6
+ import { generateMnemonic as generateMnemonic$1, validateMnemonic, mnemonicToSeedSync } from '@scure/bip39';
7
+ import { wordlist } from '@scure/bip39/wordlists/english';
8
+ import * as StellarSdk from '@stellar/stellar-sdk';
9
+ import { Keypair, Contract, TransactionBuilder, nativeToScVal, StrKey, Networks, Asset } from '@stellar/stellar-sdk';
10
+
11
+ // ../crypto/src/errors.ts
12
+ var PointAtInfinity = class extends Error {
13
+ constructor(operation) {
14
+ super(`Point at infinity encountered in ${operation}`);
15
+ this.name = "PointAtInfinity";
16
+ }
17
+ };
18
+ var InvalidPublicKey = class extends Error {
19
+ constructor(message = "Invalid public key: not on curve") {
20
+ super(message);
21
+ this.name = "InvalidPublicKey";
22
+ }
23
+ };
24
+ var InvalidScalar = class extends Error {
25
+ constructor(message = "Invalid scalar value") {
26
+ super(message);
27
+ this.name = "InvalidScalar";
28
+ }
29
+ };
30
+ var InvalidMetaAddress = class extends Error {
31
+ constructor(message = "Invalid meta-address format") {
32
+ super(message);
33
+ this.name = "InvalidMetaAddress";
34
+ }
35
+ };
36
+ var L = 2n ** 252n + 27742317777372353535851937790883648493n;
37
+ function validatePoint(point) {
38
+ if (point.length !== 32) {
39
+ throw new InvalidPublicKey("Invalid point length");
40
+ }
41
+ try {
42
+ ed25519.ExtendedPoint.fromHex(point);
43
+ return true;
44
+ } catch (e) {
45
+ if (e instanceof InvalidPublicKey) throw e;
46
+ throw new InvalidPublicKey("Point not on curve");
47
+ }
48
+ }
49
+ function pointAdd(p1, p2) {
50
+ if (p1.length !== 32) throw new InvalidPublicKey("Invalid p1 length");
51
+ if (p2.length !== 32) throw new InvalidPublicKey("Invalid p2 length");
52
+ validatePoint(p1);
53
+ validatePoint(p2);
54
+ const point1 = ed25519.ExtendedPoint.fromHex(p1);
55
+ const point2 = ed25519.ExtendedPoint.fromHex(p2);
56
+ const result = point1.add(point2);
57
+ if (result.equals(ed25519.ExtendedPoint.ZERO)) {
58
+ throw new PointAtInfinity("pointAdd");
59
+ }
60
+ return result.toRawBytes();
61
+ }
62
+ function scalarMultBase(scalar, allowZero = false) {
63
+ if (scalar.length !== 32) throw new InvalidScalar("Invalid scalar length");
64
+ const s = bytesToNumberLE(scalar) % L;
65
+ if (s === 0n) {
66
+ if (!allowZero) {
67
+ throw new InvalidScalar("Zero scalar not allowed");
68
+ }
69
+ return ed25519.ExtendedPoint.ZERO.toRawBytes();
70
+ }
71
+ const result = ed25519.ExtendedPoint.BASE.multiply(s);
72
+ return result.toRawBytes();
73
+ }
74
+ function scalarMult(scalar, point, allowZero = false) {
75
+ if (scalar.length !== 32) throw new InvalidScalar("Invalid scalar length");
76
+ if (point.length !== 32) throw new InvalidPublicKey("Invalid point length");
77
+ validatePoint(point);
78
+ const s = bytesToNumberLE(scalar) % L;
79
+ if (s === 0n) {
80
+ if (!allowZero) {
81
+ throw new InvalidScalar("Zero scalar not allowed");
82
+ }
83
+ return ed25519.ExtendedPoint.ZERO.toRawBytes();
84
+ }
85
+ const p = ed25519.ExtendedPoint.fromHex(point);
86
+ const result = p.multiply(s);
87
+ if (result.equals(ed25519.ExtendedPoint.ZERO)) {
88
+ throw new PointAtInfinity("scalarMult");
89
+ }
90
+ return result.toRawBytes();
91
+ }
92
+ function scalarAdd(s1, s2) {
93
+ if (s1.length !== 32) throw new InvalidScalar("Invalid s1 length");
94
+ if (s2.length !== 32) throw new InvalidScalar("Invalid s2 length");
95
+ const a = bytesToNumberLE(s1) % L;
96
+ const b = bytesToNumberLE(s2) % L;
97
+ const result = (a + b) % L;
98
+ return numberToBytesLE(result, 32);
99
+ }
100
+
101
+ // ../crypto/src/keys.ts
102
+ function generateMetaAddress() {
103
+ const spendPrivKey = generateRandomScalar();
104
+ const viewPrivKey = generateRandomScalar();
105
+ const spendPubKey = scalarMultBase(spendPrivKey);
106
+ const viewPubKey = scalarMultBase(viewPrivKey);
107
+ return {
108
+ spendPrivKey,
109
+ viewPrivKey,
110
+ metaAddress: {
111
+ spendPubKey,
112
+ viewPubKey
113
+ }
114
+ };
115
+ }
116
+ function generateRandomScalar() {
117
+ const bytes = randomBytes(32);
118
+ const scalar = bytesToNumberLE(bytes) % L;
119
+ return numberToBytesLE(scalar, 32);
120
+ }
121
+ function encodeMetaAddress(meta) {
122
+ if (meta.spendPubKey.length !== 32) {
123
+ throw new InvalidMetaAddress("Invalid spend public key length");
124
+ }
125
+ if (meta.viewPubKey.length !== 32) {
126
+ throw new InvalidMetaAddress("Invalid view public key length");
127
+ }
128
+ try {
129
+ validatePoint(meta.spendPubKey);
130
+ validatePoint(meta.viewPubKey);
131
+ } catch (e) {
132
+ if (e instanceof InvalidPublicKey) {
133
+ throw new InvalidMetaAddress(`Invalid public key: ${e.message}`);
134
+ }
135
+ throw e;
136
+ }
137
+ const payload = new Uint8Array(64);
138
+ payload.set(meta.spendPubKey, 0);
139
+ payload.set(meta.viewPubKey, 32);
140
+ const hash = sha256(payload);
141
+ const checksum = hash.slice(28, 32);
142
+ const combined = new Uint8Array(68);
143
+ combined.set(payload, 0);
144
+ combined.set(checksum, 64);
145
+ const hex = Array.from(combined).map((b) => b.toString(16).padStart(2, "0")).join("");
146
+ return `st:stellar:${hex}`;
147
+ }
148
+ function decodeMetaAddress(encoded) {
149
+ if (!encoded.startsWith("st:stellar:")) {
150
+ throw new InvalidMetaAddress("Invalid meta-address prefix");
151
+ }
152
+ const hex = encoded.slice(11);
153
+ if (hex.length !== 136) {
154
+ throw new InvalidMetaAddress("Invalid meta-address length");
155
+ }
156
+ const combined = new Uint8Array(68);
157
+ for (let i = 0; i < 68; i++) {
158
+ const byte = parseInt(hex.substr(i * 2, 2), 16);
159
+ if (isNaN(byte)) {
160
+ throw new InvalidMetaAddress("Invalid hex encoding");
161
+ }
162
+ combined[i] = byte;
163
+ }
164
+ const payload = combined.slice(0, 64);
165
+ const checksum = combined.slice(64, 68);
166
+ const hash = sha256(payload);
167
+ const expectedChecksum = hash.slice(28, 32);
168
+ let checksumValid = true;
169
+ for (let i = 0; i < 4; i++) {
170
+ if (checksum[i] !== expectedChecksum[i]) {
171
+ checksumValid = false;
172
+ break;
173
+ }
174
+ }
175
+ if (!checksumValid) {
176
+ throw new InvalidMetaAddress("Invalid checksum");
177
+ }
178
+ const spendPubKey = payload.slice(0, 32);
179
+ const viewPubKey = payload.slice(32, 64);
180
+ try {
181
+ validatePoint(spendPubKey);
182
+ validatePoint(viewPubKey);
183
+ } catch (e) {
184
+ if (e instanceof InvalidPublicKey) {
185
+ throw new InvalidMetaAddress(`Invalid public key in meta-address: ${e.message}`);
186
+ }
187
+ throw e;
188
+ }
189
+ return {
190
+ spendPubKey,
191
+ viewPubKey
192
+ };
193
+ }
194
+ function hashToScalar(data) {
195
+ const hash = sha256(data);
196
+ const scalar = bytesToNumberLE(hash) % L;
197
+ if (scalar === 0n) {
198
+ throw new InvalidScalar("Hash produced zero scalar");
199
+ }
200
+ return numberToBytesLE(scalar, 32);
201
+ }
202
+ function viewTag(sharedSecret) {
203
+ if (sharedSecret.length !== 32) {
204
+ throw new Error("Invalid shared secret length");
205
+ }
206
+ const hash = sha256(sharedSecret);
207
+ return hash[0];
208
+ }
209
+
210
+ // ../crypto/src/stellar-keys.ts
211
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
212
+ var VERSION_BYTE_PUBLIC_KEY = 6 << 3;
213
+ function crc16XModem(data) {
214
+ let crc = 0;
215
+ for (let i = 0; i < data.length; i++) {
216
+ crc ^= data[i] << 8;
217
+ for (let j = 0; j < 8; j++) {
218
+ if (crc & 32768) {
219
+ crc = crc << 1 ^ 4129;
220
+ } else {
221
+ crc = crc << 1;
222
+ }
223
+ }
224
+ }
225
+ return crc & 65535;
226
+ }
227
+ function base32Encode(data) {
228
+ let bits = 0;
229
+ let value = 0;
230
+ let output = "";
231
+ for (let i = 0; i < data.length; i++) {
232
+ value = value << 8 | data[i];
233
+ bits += 8;
234
+ while (bits >= 5) {
235
+ output += ALPHABET[value >>> bits - 5 & 31];
236
+ bits -= 5;
237
+ }
238
+ }
239
+ if (bits > 0) {
240
+ output += ALPHABET[value << 5 - bits & 31];
241
+ }
242
+ return output;
243
+ }
244
+ function encodePublicKey(pubKey) {
245
+ if (pubKey.length !== 32) {
246
+ throw new Error("Public key must be 32 bytes");
247
+ }
248
+ const versionedPayload = new Uint8Array(33);
249
+ versionedPayload[0] = VERSION_BYTE_PUBLIC_KEY;
250
+ versionedPayload.set(pubKey, 1);
251
+ const checksum = crc16XModem(versionedPayload);
252
+ const checksumBytes = new Uint8Array(2);
253
+ checksumBytes[0] = checksum & 255;
254
+ checksumBytes[1] = checksum >>> 8 & 255;
255
+ const fullPayload = new Uint8Array(35);
256
+ fullPayload.set(versionedPayload, 0);
257
+ fullPayload.set(checksumBytes, 33);
258
+ return base32Encode(fullPayload);
259
+ }
260
+
261
+ // ../crypto/src/scan.ts
262
+ function checkViewTag(viewPrivKey, ephemeralPubKey, expectedTag) {
263
+ if (viewPrivKey.length !== 32) {
264
+ throw new Error("Invalid view private key length");
265
+ }
266
+ if (ephemeralPubKey.length !== 32) {
267
+ throw new Error("Invalid ephemeral public key length");
268
+ }
269
+ if (expectedTag < 0 || expectedTag > 255) {
270
+ throw new Error("Invalid view tag");
271
+ }
272
+ const S = scalarMult(viewPrivKey, ephemeralPubKey);
273
+ const computedTag = viewTag(S);
274
+ if (computedTag === expectedTag) {
275
+ return { matches: true, sharedSecret: S };
276
+ }
277
+ return { matches: false };
278
+ }
279
+ function scanAnnouncements(viewPrivKey, spendPubKey, announcements) {
280
+ if (viewPrivKey.length !== 32) {
281
+ throw new Error("Invalid view private key length");
282
+ }
283
+ if (spendPubKey.length !== 32) {
284
+ throw new Error("Invalid spend public key length");
285
+ }
286
+ const results = [];
287
+ const tagMatches = [];
288
+ for (const announcement of announcements) {
289
+ const tagResult = checkViewTag(viewPrivKey, announcement.ephemeralPubKey, announcement.viewTag);
290
+ if (tagResult.matches && tagResult.sharedSecret) {
291
+ tagMatches.push({ announcement, sharedSecret: tagResult.sharedSecret });
292
+ }
293
+ }
294
+ for (const { announcement, sharedSecret } of tagMatches) {
295
+ const s = hashToScalar(sharedSecret);
296
+ const sG = scalarMultBase(s);
297
+ const P = pointAdd(spendPubKey, sG);
298
+ const computedAddress = encodePublicKey(P);
299
+ if (computedAddress === announcement.stealthAddress) {
300
+ results.push({
301
+ publicKey: P,
302
+ address: computedAddress
303
+ });
304
+ }
305
+ }
306
+ return results;
307
+ }
308
+
309
+ // ../crypto/src/recover.ts
310
+ function recoverStealthPrivateKey(spendPrivKey, viewPrivKey, ephemeralPubKey) {
311
+ if (spendPrivKey.length !== 32) {
312
+ throw new Error("Invalid spend private key length");
313
+ }
314
+ if (viewPrivKey.length !== 32) {
315
+ throw new Error("Invalid view private key length");
316
+ }
317
+ if (ephemeralPubKey.length !== 32) {
318
+ throw new Error("Invalid ephemeral public key length");
319
+ }
320
+ const S = scalarMult(viewPrivKey, ephemeralPubKey);
321
+ const s = hashToScalar(S);
322
+ const stealthPrivKey = scalarAdd(spendPrivKey, s);
323
+ return stealthPrivKey;
324
+ }
325
+ function signWithStealthKey(message, privateScalar) {
326
+ if (privateScalar.length !== 32) {
327
+ throw new Error("Invalid stealth private key length");
328
+ }
329
+ if (message.length === 0) {
330
+ throw new Error("Message cannot be empty");
331
+ }
332
+ const a = bytesToNumberLE(privateScalar) % L;
333
+ const pubKeyBytes = scalarMultBase(privateScalar);
334
+ const nonceInput = new Uint8Array(32 + message.length);
335
+ nonceInput.set(privateScalar, 0);
336
+ nonceInput.set(message, 32);
337
+ const r = bytesToNumberLE(sha512(nonceInput)) % L;
338
+ const R = ed25519.ExtendedPoint.BASE.multiply(r);
339
+ const Rbytes = R.toRawBytes();
340
+ const challengeInput = new Uint8Array(32 + 32 + message.length);
341
+ challengeInput.set(Rbytes, 0);
342
+ challengeInput.set(pubKeyBytes, 32);
343
+ challengeInput.set(message, 64);
344
+ const k = bytesToNumberLE(sha512(challengeInput)) % L;
345
+ const S = (r + k * a) % L;
346
+ const sig = new Uint8Array(64);
347
+ sig.set(Rbytes, 0);
348
+ sig.set(numberToBytesLE(S, 32), 32);
349
+ return sig;
350
+ }
351
+ function deriveStealthAddressWithSecret(spendPubKey, viewPubKey, ephemeralPrivKey) {
352
+ const R = scalarMultBase(ephemeralPrivKey);
353
+ const S = scalarMult(ephemeralPrivKey, viewPubKey);
354
+ const s = hashToScalar(S);
355
+ const sG = scalarMultBase(s);
356
+ const P = pointAdd(spendPubKey, sG);
357
+ const tag = viewTag(S);
358
+ return {
359
+ stealthPubKey: P,
360
+ stealthAddress: encodePublicKey(P),
361
+ ephemeralPubKey: R,
362
+ viewTag: tag,
363
+ ephemeralPrivKey,
364
+ sharedSecret: S
365
+ };
366
+ }
367
+ var textEncoder = new TextEncoder();
368
+ function generateMnemonic() {
369
+ return generateMnemonic$1(wordlist);
370
+ }
371
+ function mnemonicToStealthKeys(mnemonic, passphrase) {
372
+ if (!validateMnemonic(mnemonic, wordlist)) {
373
+ throw new Error("Invalid mnemonic phrase");
374
+ }
375
+ const seed = mnemonicToSeedSync(mnemonic, "");
376
+ const spendTag = textEncoder.encode("stealth-spend");
377
+ const viewTag2 = textEncoder.encode("stealth-view");
378
+ const spendInput = new Uint8Array(spendTag.length + seed.length);
379
+ spendInput.set(spendTag, 0);
380
+ spendInput.set(seed, spendTag.length);
381
+ const viewInput = new Uint8Array(viewTag2.length + seed.length);
382
+ viewInput.set(viewTag2, 0);
383
+ viewInput.set(seed, viewTag2.length);
384
+ const spendPrivKey = hashToScalar(spendInput);
385
+ const viewPrivKey = hashToScalar(viewInput);
386
+ seed.fill(0);
387
+ spendInput.fill(0);
388
+ viewInput.fill(0);
389
+ const spendPubKey = scalarMultBase(spendPrivKey);
390
+ const viewPubKey = scalarMultBase(viewPrivKey);
391
+ return {
392
+ spendPrivKey,
393
+ viewPrivKey,
394
+ metaAddress: { spendPubKey, viewPubKey }
395
+ };
396
+ }
397
+ function getNetworkConfig(network) {
398
+ const networkPassphrase = network === "local" ? Networks.STANDALONE : Networks.TESTNET;
399
+ const rpcUrl = network === "local" ? "http://localhost:8000/soroban/rpc" : "https://soroban-testnet.stellar.org";
400
+ const server = new StellarSdk.rpc.Server(rpcUrl, {
401
+ allowHttp: network === "local"
402
+ });
403
+ return { networkPassphrase, rpcUrl, server };
404
+ }
405
+ function createSimulationTx(operation, networkPassphrase) {
406
+ return new TransactionBuilder(
407
+ new StellarSdk.Account("GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF", "0"),
408
+ { fee: "100", networkPassphrase }
409
+ ).addOperation(operation).setTimeout(30).build();
410
+ }
411
+ async function simulateReadOnly(contractId, method, args, server, networkPassphrase) {
412
+ const contract = new Contract(contractId);
413
+ const op = contract.call(method, ...args);
414
+ const sim = await server.simulateTransaction(
415
+ createSimulationTx(op, networkPassphrase)
416
+ );
417
+ if (StellarSdk.rpc.Api.isSimulationSuccess(sim) && sim.result?.retval) {
418
+ return StellarSdk.scValToNative(sim.result.retval);
419
+ }
420
+ return null;
421
+ }
422
+ function resolveTokenAddress(assetArg, networkPassphrase) {
423
+ if (!assetArg || assetArg === "native" || assetArg === "XLM") {
424
+ return Asset.native().contractId(networkPassphrase);
425
+ }
426
+ const parts = assetArg.split(":");
427
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
428
+ throw new Error('Invalid asset format. Use CODE:ISSUER or "native"');
429
+ }
430
+ return new Asset(parts[0], parts[1]).contractId(networkPassphrase);
431
+ }
432
+ async function waitForTransaction(server, hash) {
433
+ let attempts = 0;
434
+ while (attempts < 30) {
435
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
436
+ try {
437
+ const result = await server.getTransaction(hash);
438
+ if (result.status === "SUCCESS") return;
439
+ if (result.status === "FAILED") throw new Error("Transaction failed on-chain");
440
+ } catch (e) {
441
+ const msg = e instanceof Error ? e.message : String(e);
442
+ if (msg === "Transaction failed on-chain") throw e;
443
+ }
444
+ attempts++;
445
+ }
446
+ throw new Error("Transaction confirmation timed out");
447
+ }
448
+ async function fetchAnnouncements(contractId, server, networkPassphrase) {
449
+ const result = await simulateReadOnly(
450
+ contractId,
451
+ "get_announcements",
452
+ [nativeToScVal(0, { type: "u64" }), nativeToScVal(1e3, { type: "u64" })],
453
+ server,
454
+ networkPassphrase
455
+ );
456
+ if (!result || !Array.isArray(result)) return [];
457
+ return result.map((ann) => {
458
+ const a = ann;
459
+ const stealthPk = new Uint8Array(a.stealth_pk);
460
+ return {
461
+ ephemeralPubKey: new Uint8Array(a.ephemeral_pk),
462
+ viewTag: a.view_tag,
463
+ stealthPubKey: stealthPk,
464
+ stealthAddress: StrKey.encodeEd25519PublicKey(Buffer.from(stealthPk)),
465
+ token: a.token?.toString?.() || "unknown",
466
+ amount: BigInt(a.amount || 0)
467
+ };
468
+ });
469
+ }
470
+ async function queryBalance(contractId, stealthPk, tokenAddress, server, networkPassphrase) {
471
+ const result = await simulateReadOnly(
472
+ contractId,
473
+ "get_balance",
474
+ [nativeToScVal(Buffer.from(stealthPk)), new StellarSdk.Address(tokenAddress).toScVal()],
475
+ server,
476
+ networkPassphrase
477
+ );
478
+ if (result !== null) return BigInt(result);
479
+ return 0n;
480
+ }
481
+ async function queryNonce(contractId, stealthPk, server, networkPassphrase) {
482
+ const result = await simulateReadOnly(
483
+ contractId,
484
+ "get_nonce",
485
+ [nativeToScVal(Buffer.from(stealthPk))],
486
+ server,
487
+ networkPassphrase
488
+ );
489
+ if (result !== null) return BigInt(result);
490
+ return 0n;
491
+ }
492
+ function i128ToBigEndian(value) {
493
+ const buf = new Uint8Array(16);
494
+ const dv = new DataView(buf.buffer);
495
+ dv.setBigInt64(0, value >> 64n);
496
+ dv.setBigUint64(8, value & 0xFFFFFFFFFFFFFFFFn);
497
+ return buf;
498
+ }
499
+ function u64ToBigEndian(value) {
500
+ const buf = new Uint8Array(8);
501
+ const dv = new DataView(buf.buffer);
502
+ dv.setBigUint64(0, value);
503
+ return buf;
504
+ }
505
+ function buildWithdrawMessage(stealthPk, tokenAddress, amount, destination, nonce) {
506
+ const tokenBytes = Buffer.from(tokenAddress, "utf-8");
507
+ const destBytes = Buffer.from(destination, "utf-8");
508
+ if (tokenBytes.length !== 56) throw new Error(`Token address must be 56 bytes StrKey, got ${tokenBytes.length}`);
509
+ if (destBytes.length !== 56) throw new Error(`Destination must be 56 bytes StrKey, got ${destBytes.length}`);
510
+ const amountBytes = i128ToBigEndian(amount);
511
+ const nonceBytes = u64ToBigEndian(nonce);
512
+ const msg = new Uint8Array(32 + 56 + 16 + 56 + 8);
513
+ let offset = 0;
514
+ msg.set(stealthPk, offset);
515
+ offset += 32;
516
+ msg.set(tokenBytes, offset);
517
+ offset += 56;
518
+ msg.set(amountBytes, offset);
519
+ offset += 16;
520
+ msg.set(destBytes, offset);
521
+ offset += 56;
522
+ msg.set(nonceBytes, offset);
523
+ return sha256(msg);
524
+ }
525
+
526
+ // src/client.ts
527
+ var DEFAULT_CONTRACTS = {
528
+ local: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGABAX"
529
+ };
530
+ var StealthClient = class {
531
+ contractId;
532
+ networkPassphrase;
533
+ server;
534
+ constructor(config) {
535
+ this.contractId = config.contractId || DEFAULT_CONTRACTS[config.network] || "";
536
+ const netConfig = getNetworkConfig(config.network);
537
+ this.networkPassphrase = netConfig.networkPassphrase;
538
+ this.server = netConfig.server;
539
+ }
540
+ /**
541
+ * Generate a new random stealth key pair.
542
+ * No network connection needed.
543
+ */
544
+ static keygen() {
545
+ const keys = generateMetaAddress();
546
+ const metaAddress = encodeMetaAddress(keys.metaAddress);
547
+ return {
548
+ metaAddress,
549
+ spendPubKey: Buffer.from(keys.metaAddress.spendPubKey).toString("hex"),
550
+ spendPrivKey: Buffer.from(keys.spendPrivKey).toString("hex"),
551
+ viewPubKey: Buffer.from(keys.metaAddress.viewPubKey).toString("hex"),
552
+ viewPrivKey: Buffer.from(keys.viewPrivKey).toString("hex")
553
+ };
554
+ }
555
+ /**
556
+ * Generate stealth keys from a BIP-39 mnemonic.
557
+ * Returns the mnemonic alongside the keys for backup.
558
+ * No network connection needed.
559
+ */
560
+ static fromMnemonic(mnemonic) {
561
+ const phrase = mnemonic || generateMnemonic();
562
+ const keys = mnemonicToStealthKeys(phrase);
563
+ const metaAddress = encodeMetaAddress(keys.metaAddress);
564
+ return {
565
+ mnemonic: phrase,
566
+ metaAddress,
567
+ spendPubKey: Buffer.from(keys.metaAddress.spendPubKey).toString("hex"),
568
+ spendPrivKey: Buffer.from(keys.spendPrivKey).toString("hex"),
569
+ viewPubKey: Buffer.from(keys.metaAddress.viewPubKey).toString("hex"),
570
+ viewPrivKey: Buffer.from(keys.viewPrivKey).toString("hex")
571
+ };
572
+ }
573
+ /**
574
+ * Send tokens to a stealth address.
575
+ *
576
+ * Derives a one-time stealth address from the recipient's meta-address,
577
+ * deposits tokens into the pool contract, and records the announcement.
578
+ *
579
+ * @param metaAddress - Recipient's meta-address (st:stellar:... format)
580
+ * @param amount - Amount in whole units (e.g. 100 = 100 XLM)
581
+ * @param senderSecret - Sender's Stellar secret key
582
+ * @param opts - Optional: asset to send
583
+ */
584
+ async send(metaAddress, amount, senderSecret, opts) {
585
+ if (amount <= 0) throw new Error("Amount must be positive");
586
+ const { spendPubKey, viewPubKey } = decodeMetaAddress(metaAddress);
587
+ const senderKeypair = Keypair.fromSecret(senderSecret);
588
+ const tokenAddress = resolveTokenAddress(opts?.asset, this.networkPassphrase);
589
+ const stroops = BigInt(Math.round(amount * 1e7));
590
+ const ephemeralPrivKey = new Uint8Array(randomBytes(32));
591
+ const stealth = deriveStealthAddressWithSecret(
592
+ spendPubKey,
593
+ viewPubKey,
594
+ ephemeralPrivKey
595
+ );
596
+ const contract = new Contract(this.contractId);
597
+ const account = await this.server.getAccount(senderKeypair.publicKey());
598
+ const depositTx = new TransactionBuilder(account, {
599
+ fee: "100",
600
+ networkPassphrase: this.networkPassphrase
601
+ }).addOperation(
602
+ contract.call(
603
+ "deposit",
604
+ new StellarSdk.Address(senderKeypair.publicKey()).toScVal(),
605
+ new StellarSdk.Address(tokenAddress).toScVal(),
606
+ nativeToScVal(stroops, { type: "i128" }),
607
+ nativeToScVal(Buffer.from(stealth.stealthPubKey)),
608
+ nativeToScVal(Buffer.from(stealth.ephemeralPubKey)),
609
+ nativeToScVal(stealth.viewTag, { type: "u32" })
610
+ )
611
+ ).setTimeout(30).build();
612
+ const prepared = await this.server.prepareTransaction(depositTx);
613
+ prepared.sign(senderKeypair);
614
+ const result = await this.server.sendTransaction(prepared);
615
+ if (result.status === "ERROR") {
616
+ throw new Error("Transaction submission failed");
617
+ }
618
+ if (result.status === "PENDING") {
619
+ await waitForTransaction(this.server, result.hash);
620
+ }
621
+ return {
622
+ stealthAddress: stealth.stealthAddress,
623
+ txHash: result.hash
624
+ };
625
+ }
626
+ /**
627
+ * Scan for stealth payments you received.
628
+ *
629
+ * Uses the view key to detect which announcements are yours,
630
+ * then queries the contract for current balances.
631
+ *
632
+ * @param keys - Your stealth keys (needs viewPrivKey + spendPubKey)
633
+ */
634
+ async scan(keys) {
635
+ const viewPrivKey = Buffer.from(keys.viewPrivKey, "hex");
636
+ const spendPubKey = Buffer.from(keys.spendPubKey, "hex");
637
+ const announcements = await fetchAnnouncements(
638
+ this.contractId,
639
+ this.server,
640
+ this.networkPassphrase
641
+ );
642
+ if (announcements.length === 0) return [];
643
+ const matches = scanAnnouncements(
644
+ viewPrivKey,
645
+ spendPubKey,
646
+ announcements.map((a) => ({
647
+ ephemeralPubKey: a.ephemeralPubKey,
648
+ viewTag: a.viewTag,
649
+ stealthAddress: a.stealthAddress
650
+ }))
651
+ );
652
+ const payments = [];
653
+ for (const match of matches) {
654
+ if (!match) continue;
655
+ const ann = announcements.find((a) => a.stealthAddress === match.address);
656
+ if (!ann) continue;
657
+ const balance = await queryBalance(
658
+ this.contractId,
659
+ ann.stealthPubKey,
660
+ ann.token,
661
+ this.server,
662
+ this.networkPassphrase
663
+ );
664
+ if (balance <= 0n) continue;
665
+ payments.push({
666
+ stealthAddress: ann.stealthAddress,
667
+ ephemeralPubKey: Buffer.from(ann.ephemeralPubKey).toString("hex"),
668
+ token: ann.token,
669
+ amount: Number(balance) / 1e7
670
+ });
671
+ }
672
+ return payments;
673
+ }
674
+ /**
675
+ * Get balances for all your stealth addresses in the pool.
676
+ *
677
+ * @param keys - Your stealth keys (needs viewPrivKey + spendPubKey)
678
+ */
679
+ async balance(keys) {
680
+ const payments = await this.scan(keys);
681
+ return payments.map((p) => ({
682
+ stealthAddress: p.stealthAddress,
683
+ token: p.token,
684
+ amount: p.amount
685
+ }));
686
+ }
687
+ /**
688
+ * Withdraw tokens from the stealth pool.
689
+ *
690
+ * Recovers the stealth private key, signs a withdraw message,
691
+ * and submits the transaction. Use `opts.relay` for privacy-preserving
692
+ * withdrawal via a relayer.
693
+ *
694
+ * @param stealthAddress - The stealth address to withdraw from
695
+ * @param destination - Destination Stellar address (G...)
696
+ * @param opts - Withdraw options (keys, feePayer, optional relay/asset/amount)
697
+ */
698
+ async withdraw(stealthAddress, destination, opts) {
699
+ if (!StrKey.isValidEd25519PublicKey(stealthAddress)) {
700
+ throw new Error("Invalid stealth address");
701
+ }
702
+ if (!StrKey.isValidEd25519PublicKey(destination)) {
703
+ throw new Error("Invalid destination address");
704
+ }
705
+ const viewPrivKey = Buffer.from(opts.keys.viewPrivKey, "hex");
706
+ const spendPrivKey = Buffer.from(opts.keys.spendPrivKey, "hex");
707
+ const spendPubKey = Buffer.from(opts.keys.spendPubKey, "hex");
708
+ const tokenAddress = resolveTokenAddress(opts.asset, this.networkPassphrase);
709
+ const announcements = await fetchAnnouncements(
710
+ this.contractId,
711
+ this.server,
712
+ this.networkPassphrase
713
+ );
714
+ const allMatches = scanAnnouncements(
715
+ viewPrivKey,
716
+ spendPubKey,
717
+ announcements.map((a) => ({
718
+ ephemeralPubKey: a.ephemeralPubKey,
719
+ viewTag: a.viewTag,
720
+ stealthAddress: a.stealthAddress
721
+ }))
722
+ );
723
+ const matchedAnn = announcements.find((a) => {
724
+ if (a.stealthAddress !== stealthAddress) return false;
725
+ return allMatches.some((m) => m?.address === stealthAddress);
726
+ });
727
+ if (!matchedAnn) {
728
+ throw new Error("Could not find announcement for this stealth address");
729
+ }
730
+ const stealthPrivKey = recoverStealthPrivateKey(
731
+ spendPrivKey,
732
+ viewPrivKey,
733
+ matchedAnn.ephemeralPubKey
734
+ );
735
+ const balance = await queryBalance(
736
+ this.contractId,
737
+ matchedAnn.stealthPubKey,
738
+ tokenAddress,
739
+ this.server,
740
+ this.networkPassphrase
741
+ );
742
+ if (balance <= 0n) throw new Error("Stealth address has no balance in the pool");
743
+ let withdrawAmount;
744
+ if (opts.amount !== void 0) {
745
+ withdrawAmount = BigInt(Math.round(opts.amount * 1e7));
746
+ if (withdrawAmount > balance) {
747
+ throw new Error(`Requested ${opts.amount} but balance is ${Number(balance) / 1e7}`);
748
+ }
749
+ } else {
750
+ withdrawAmount = balance;
751
+ }
752
+ const currentNonce = await queryNonce(
753
+ this.contractId,
754
+ matchedAnn.stealthPubKey,
755
+ this.server,
756
+ this.networkPassphrase
757
+ );
758
+ const nonce = currentNonce + 1n;
759
+ const messageHash = buildWithdrawMessage(
760
+ matchedAnn.stealthPubKey,
761
+ tokenAddress,
762
+ withdrawAmount,
763
+ destination,
764
+ nonce
765
+ );
766
+ const signature = signWithStealthKey(messageHash, stealthPrivKey);
767
+ const feePayerKeypair = Keypair.fromSecret(opts.feePayer);
768
+ const contract = new Contract(this.contractId);
769
+ const feePayerAccount = await this.server.getAccount(feePayerKeypair.publicKey());
770
+ const withdrawTx = new TransactionBuilder(feePayerAccount, {
771
+ fee: "100",
772
+ networkPassphrase: this.networkPassphrase
773
+ }).addOperation(
774
+ contract.call(
775
+ "withdraw",
776
+ nativeToScVal(Buffer.from(matchedAnn.stealthPubKey)),
777
+ new StellarSdk.Address(tokenAddress).toScVal(),
778
+ nativeToScVal(withdrawAmount, { type: "i128" }),
779
+ new StellarSdk.Address(destination).toScVal(),
780
+ nativeToScVal(nonce, { type: "u64" }),
781
+ nativeToScVal(Buffer.from(signature))
782
+ )
783
+ ).setTimeout(30).build();
784
+ const prepared = await this.server.prepareTransaction(withdrawTx);
785
+ prepared.sign(feePayerKeypair);
786
+ let txHash;
787
+ if (opts.relay) {
788
+ const url = opts.relay.endsWith("/relay") ? opts.relay : `${opts.relay}/relay`;
789
+ const res = await fetch(url, {
790
+ method: "POST",
791
+ headers: { "Content-Type": "application/json" },
792
+ body: JSON.stringify({ xdr: prepared.toEnvelope().toXDR("base64") })
793
+ });
794
+ if (!res.ok) {
795
+ const err = await res.json();
796
+ throw new Error(`Relay error: ${err.error || "unknown"}`);
797
+ }
798
+ const data = await res.json();
799
+ txHash = data.txHash;
800
+ } else {
801
+ const result = await this.server.sendTransaction(prepared);
802
+ if (result.status === "ERROR") throw new Error("Transaction submission failed");
803
+ if (result.status === "PENDING") {
804
+ await waitForTransaction(this.server, result.hash);
805
+ }
806
+ txHash = result.hash;
807
+ }
808
+ return {
809
+ txHash,
810
+ amount: Number(withdrawAmount) / 1e7
811
+ };
812
+ }
813
+ };
814
+
815
+ export { StealthClient };
816
+ //# sourceMappingURL=index.js.map
817
+ //# sourceMappingURL=index.js.map