@auditable/privacy-pool-zk-sdk 0.7.0 → 0.9.1-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3,32 +3,27 @@
3
3
 
4
4
  var fs = require('fs');
5
5
  var stellarSdk = require('@stellar/stellar-sdk');
6
- var snarkjs = require('snarkjs');
6
+ var circomlibjs = require('circomlibjs');
7
7
 
8
8
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
9
9
  function _interopNamespaceDefault(e) {
10
- var n = Object.create(null);
11
- if (e) {
12
- Object.keys(e).forEach(function (k) {
13
- if (k !== 'default') {
14
- var d = Object.getOwnPropertyDescriptor(e, k);
15
- Object.defineProperty(n, k, d.get ? d : {
16
- enumerable: true,
17
- get: function () { return e[k]; }
18
- });
19
- }
20
- });
21
- }
22
- n.default = e;
23
- return Object.freeze(n);
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
24
  }
25
25
 
26
26
  var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
27
- var snarkjs__namespace = /*#__PURE__*/_interopNamespaceDefault(snarkjs);
28
-
29
- function getDefaultExportFromCjs (x) {
30
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
31
- }
32
27
 
33
28
  var dist = {};
34
29
 
@@ -205,7 +200,7 @@ dist.bech32m = getLibraryFromEncoding('bech32m');
205
200
  const STEALTH_ADDRESS_HRP = 'stpl1';
206
201
  /** BIP-173 default 90 is too small for 64-byte payload (32+32 field coords) after 8→5 bit conversion. */
207
202
  const BECH32_STEALTH_LIMIT = 1023;
208
- function normalizeHex$2(hex) {
203
+ function normalizeHex$3(hex) {
209
204
  const s = hex.trim().replace(/^0x/i, '');
210
205
  if (!/^[0-9a-fA-F]*$/.test(s)) {
211
206
  throw new Error('stealth-address: hex must contain only 0-9, a-f');
@@ -213,7 +208,7 @@ function normalizeHex$2(hex) {
213
208
  return s.length % 2 === 0 ? s : `0${s}`;
214
209
  }
215
210
  function hexToBytes$2(hex) {
216
- const norm = normalizeHex$2(hex);
211
+ const norm = normalizeHex$3(hex);
217
212
  const out = new Uint8Array(norm.length / 2);
218
213
  for (let i = 0; i < out.length; i++) {
219
214
  out[i] = parseInt(norm.slice(i * 2, i * 2 + 2), 16);
@@ -227,7 +222,7 @@ function bytesToHex$1(bytes) {
227
222
  }
228
223
  return s;
229
224
  }
230
- function concatBytes(a, b) {
225
+ function concatBytes$1(a, b) {
231
226
  const out = new Uint8Array(a.length + b.length);
232
227
  out.set(a, 0);
233
228
  out.set(b, a.length);
@@ -243,7 +238,7 @@ function encodeStealthAddress(decoded) {
243
238
  if (xb.length !== yb.length) {
244
239
  throw new Error('stealth-address: x and y must encode the same number of bytes for a reversible address');
245
240
  }
246
- const payload = concatBytes(xb, yb);
241
+ const payload = concatBytes$1(xb, yb);
247
242
  const words = bech32.toWords(payload);
248
243
  return bech32.encode(STEALTH_ADDRESS_HRP, words, BECH32_STEALTH_LIMIT);
249
244
  }
@@ -268,375 +263,57 @@ function decodeStealthAddress(address) {
268
263
 
269
264
  /** Default nonce when deriving the wallet sign-in message. */
270
265
  const DEFAULT_STEALTH_SIGN_NONCE = 'main address';
271
- /**
272
- * Plaintext for Stellar wallet message signing (stealth address derivation).
273
- * Sign the exact bytes of this string (UTF-8) with the stellar account key.
274
- */
275
- function buildStealthAddressSignMessage(address, nonce = DEFAULT_STEALTH_SIGN_NONCE) {
276
- return `Privacy layer app needs you to sign the message for stealth address derivation with address:\n${address}\n and nonce:\n${nonce}.\n\n This will not authorize any transaction.`;
277
- }
278
-
279
- /** Bech32 HRP for {@link DecodedEphemeralKey} only (`x ‖ y`, 64 bytes). */
280
- const DECODED_EPHEMERAL_HRP = 'epk1';
281
- /** Bech32 HRP for {@link DecodedDepositorSharedSecretPreimage} (96 bytes). */
282
- const DEPOSITOR_SHARED_SECRET_PREIMAGE_HRP = 'epk_dep_pre1';
283
- const FIELD_BYTES = 32;
284
- const EPK_POINT_PAYLOAD = FIELD_BYTES * 2;
285
- const EPK_DEP_PRE_PAYLOAD = FIELD_BYTES * 3;
286
- const BECH32_LONG_LIMIT = 1023;
287
- function normalizeHex$1(hex) {
288
- const s = hex.trim().replace(/^0x/i, '');
289
- if (!/^[0-9a-fA-F]*$/.test(s)) {
290
- throw new Error('ephemeral-key: hex must contain only 0-9, a-f');
291
- }
292
- return s.length % 2 === 0 ? s : `0${s}`;
293
- }
294
- function hexToBytes$1(hex) {
295
- const norm = normalizeHex$1(hex);
296
- const out = new Uint8Array(norm.length / 2);
297
- for (let i = 0; i < out.length; i++) {
298
- out[i] = parseInt(norm.slice(i * 2, i * 2 + 2), 16);
299
- }
300
- return out;
301
- }
302
- function bytesToHex(bytes) {
303
- let s = '';
304
- for (let i = 0; i < bytes.length; i++) {
305
- s += bytes[i].toString(16).padStart(2, '0');
306
- }
307
- return s;
308
- }
309
- function concat2(a, b) {
310
- const out = new Uint8Array(a.length + b.length);
311
- out.set(a, 0);
312
- out.set(b, a.length);
313
- return out;
314
- }
315
- function concat3(a, b, c) {
316
- const out = new Uint8Array(a.length + b.length + c.length);
317
- out.set(a, 0);
318
- out.set(b, a.length);
319
- out.set(c, a.length + b.length);
320
- return out;
321
- }
322
- function require32(label, bytes) {
323
- if (bytes.length !== FIELD_BYTES) {
324
- throw new Error(`ephemeral-key: ${label} must encode exactly ${FIELD_BYTES} bytes, got ${bytes.length}`);
325
- }
326
- }
327
- /**
328
- * 32-byte big-endian integer as hex (64 chars), **< 2^253**, so BabyJub ECDH matches
329
- * `circuits/encryption.circom` `Num2Bits(253)` and `libs/cryptography` `scalar_mul_253`.
330
- */
331
- function generateRandomScalarHex32() {
332
- const g = globalThis.crypto;
333
- if (!g?.getRandomValues) {
334
- throw new Error('ephemeral-key: crypto.getRandomValues is required');
335
- }
336
- const max = 1n << 253n;
337
- for (let attempt = 0; attempt < 65536; attempt++) {
338
- const b = new Uint8Array(32);
339
- g.getRandomValues(b);
340
- const hex = bytesToHex(b);
341
- if (BigInt(`0x${hex}`) < max) {
342
- return hex;
343
- }
344
- }
345
- throw new Error('ephemeral-key: failed to sample scalar < 2^253');
346
- }
347
- /** Bech32 `epk1`: encodes `x ‖ y` (64 bytes). */
348
- function encodeDecodedEphemeralKey(decoded) {
349
- const xb = hexToBytes$1(decoded.x);
350
- const yb = hexToBytes$1(decoded.y);
351
- require32('x', xb);
352
- require32('y', yb);
353
- const payload = concat2(xb, yb);
354
- const words = bech32.toWords(payload);
355
- return bech32.encode(DECODED_EPHEMERAL_HRP, words, BECH32_LONG_LIMIT);
356
- }
357
- function decodeDecodedEphemeralKey(encoded) {
358
- const { prefix, words } = bech32.decode(encoded, BECH32_LONG_LIMIT);
359
- if (prefix !== DECODED_EPHEMERAL_HRP) {
360
- throw new Error(`ephemeral-key: expected HRP ${DECODED_EPHEMERAL_HRP}, got ${JSON.stringify(prefix)}`);
361
- }
362
- const bytes = new Uint8Array(bech32.fromWords(words));
363
- if (bytes.length !== EPK_POINT_PAYLOAD) {
364
- throw new Error(`ephemeral-key: epk1 payload must be ${EPK_POINT_PAYLOAD} bytes, got ${bytes.length}`);
365
- }
366
- return {
367
- x: bytesToHex(bytes.subarray(0, FIELD_BYTES)),
368
- y: bytesToHex(bytes.subarray(FIELD_BYTES)),
369
- };
370
- }
371
- /** Bech32 `epk_dep_pre1`: `randomNonceScalar ‖ recipient.x ‖ recipient.y`. */
372
- function encodeDepositorSharedSecretPreimage(decoded) {
373
- const sb = hexToBytes$1(decoded.randomNonceScalar);
374
- const xb = hexToBytes$1(decoded.recipientStealthAddress.x);
375
- const yb = hexToBytes$1(decoded.recipientStealthAddress.y);
376
- require32('randomNonceScalar', sb);
377
- require32('recipientStealthAddress.x', xb);
378
- require32('recipientStealthAddress.y', yb);
379
- const payload = concat3(sb, xb, yb);
380
- const words = bech32.toWords(payload);
381
- return bech32.encode(DEPOSITOR_SHARED_SECRET_PREIMAGE_HRP, words, BECH32_LONG_LIMIT);
266
+ /** Domain tag mixed into the spend-scalar digest (H6). */
267
+ const SPEND_SCALAR_DOMAIN_TAG = 'privacy-pool-spend-scalar-v1';
268
+ /** Schema version bound into the spend-scalar message and digest. */
269
+ const OWNER_BOUND_NOTE_SCHEMA_VERSION = 1;
270
+ function resolveSpendScalarSchemaVersion(domain) {
271
+ return domain.schemaVersion ?? OWNER_BOUND_NOTE_SCHEMA_VERSION;
382
272
  }
383
- function decodeDepositorSharedSecretPreimage(encoded) {
384
- const { prefix, words } = bech32.decode(encoded, BECH32_LONG_LIMIT);
385
- if (prefix !== DEPOSITOR_SHARED_SECRET_PREIMAGE_HRP) {
386
- throw new Error(`ephemeral-key: expected HRP ${DEPOSITOR_SHARED_SECRET_PREIMAGE_HRP}, got ${JSON.stringify(prefix)}`);
387
- }
388
- const bytes = new Uint8Array(bech32.fromWords(words));
389
- if (bytes.length !== EPK_DEP_PRE_PAYLOAD) {
390
- throw new Error(`ephemeral-key: epk_dep_pre1 payload must be ${EPK_DEP_PRE_PAYLOAD} bytes, got ${bytes.length}`);
391
- }
392
- return {
393
- randomNonceScalar: bytesToHex(bytes.subarray(0, FIELD_BYTES)),
394
- recipientStealthAddress: {
395
- x: bytesToHex(bytes.subarray(FIELD_BYTES, FIELD_BYTES * 2)),
396
- y: bytesToHex(bytes.subarray(FIELD_BYTES * 2)),
397
- },
398
- };
399
- }
400
-
401
- const DEFAULT_APPLICATION_ID = '101';
402
- /** BabyJub audit public key (decimal Fr) used in BDD / local demo when env is unset. */
403
- const DEMO_AUDIT_PUBLIC_KEY = [
404
- '21605515851820432880964235241069234202284600780825340516808373216881770219365',
405
- '18856460861531942120859708048677603751294231190189224157283439874962410808705',
406
- ];
407
- function isActiveWithdraw(slot) {
408
- return slot.value !== '0';
409
- }
410
- function isActiveDeposit(slot) {
411
- return slot.value !== '0';
412
- }
413
- function resolveWithdrawSlot(slot) {
414
- if (slot === 'dummy') {
415
- return null;
416
- }
417
- return slot;
418
- }
419
- function resolveDepositSlot(slot) {
420
- if (slot === 'dummy') {
421
- return null;
422
- }
423
- return slot;
424
- }
425
- function buildUniformAuditParams(applicationId = DEFAULT_APPLICATION_ID, auditPublicKey) {
426
- return {
427
- applicationId,
428
- noteAuditPublicKeys: [
429
- auditPublicKey,
430
- auditPublicKey,
431
- auditPublicKey,
432
- auditPublicKey,
433
- ],
434
- auditEphemeralScalars: [
435
- randomFrDecimal253(),
436
- randomFrDecimal253(),
437
- randomFrDecimal253(),
438
- randomFrDecimal253(),
439
- ],
440
- };
441
- }
442
- function resolveSlotApplicationIds(audit, withdrawSlots, depositSlots) {
443
- const w0 = resolveWithdrawSlot(withdrawSlots[0]);
444
- const w1 = resolveWithdrawSlot(withdrawSlots[1]);
445
- const d0 = resolveDepositSlot(depositSlots[0]);
446
- const d1 = resolveDepositSlot(depositSlots[1]);
447
- return {
448
- inputApplicationIds: [
449
- w0 && isActiveWithdraw(w0) ? audit.applicationId : '0',
450
- w1 && isActiveWithdraw(w1) ? audit.applicationId : '0',
451
- ],
452
- outputApplicationIds: [
453
- d0 && isActiveDeposit(d0) ? audit.applicationId : '0',
454
- d1 && isActiveDeposit(d1) ? audit.applicationId : '0',
455
- ],
456
- };
457
- }
458
- function resolveTransactionAuditParams(applicationId, auditPublicKey) {
459
- return buildUniformAuditParams(applicationId, auditPublicKey ?? DEMO_AUDIT_PUBLIC_KEY);
460
- }
461
-
462
- /** Matches `Transaction(20, 2, 2, publicNInputs, publicNOutputs, 4, 9)` in `circuits/main.circom`. */
463
- const TRANSACTION_TREE_DEPTH = 20;
464
- /** BN254 scalar field modulus (ark `Fr`, circom signals). */
465
- const BN254_SCALAR_MOD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
466
273
  /**
467
- * BabyJub ECDH in `circuits/encryption.circom` uses `Num2Bits(253)`; scalars must be < 2^253
468
- * (matches `libs/cryptography` `scalar_mul_253`).
274
+ * Plaintext for Stellar wallet message signing (spend-key derivation).
275
+ * Sign the exact bytes of this string (UTF-8) with the stellar account key.
276
+ *
277
+ * The derived scalar is the owner-bound spend key for notes in this environment.
469
278
  */
470
- const BN254_BABYJUB_SCALAR_MAX_EXCLUSIVE = 1n << 253n;
471
- function normalizeHex(hex) {
472
- const s = hex.trim().replace(/^0x/i, '');
473
- if (!/^[0-9a-fA-F]*$/.test(s)) {
474
- throw new Error('withdrawal-transaction-input: invalid hex');
475
- }
476
- return s.length % 2 === 0 ? s : `0${s}`;
477
- }
478
- /** 32-byte field coordinate (hex, no 0x) → decimal string mod BN254 scalar field. */
479
- function coordHexToDecimal(hex) {
480
- const h = normalizeHex(hex);
481
- if (h.length > 64) {
482
- throw new Error('withdrawal-transaction-input: coordinate hex too long');
483
- }
484
- const v = BigInt(`0x${h}`);
485
- return (v % BN254_SCALAR_MOD).toString(10);
486
- }
279
+ function buildStealthAddressSignMessage(address, domain, nonce = DEFAULT_STEALTH_SIGN_NONCE) {
280
+ const schemaVersion = resolveSpendScalarSchemaVersion(domain);
281
+ return [
282
+ 'Arcane privacy layer: derive your private-note spend key.',
283
+ '',
284
+ `Wallet: ${address}`,
285
+ `Network: ${domain.networkPassphrase}`,
286
+ `Pool: ${domain.poolContract}`,
287
+ `Registry: ${domain.registryContract}`,
288
+ `Schema: owner-bound-note-v${schemaVersion}`,
289
+ `Nonce: ${nonce}`,
290
+ '',
291
+ 'This signature derives the key that authorizes spending your private notes in this environment.',
292
+ ].join('\n');
293
+ }
294
+
295
+ const DOM_ENC = 1346719299n;
296
+ const DOM_AUDIT_TAG = 1346458695n;
297
+ const DOM_OUTPUT_NOTE_TAG = 1347310663n;
298
+ const BABYJUB_SUBGROUP_ORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
487
299
  /**
488
- * 32-byte big-endian scalar hex → decimal for circom `ephemeralKeyScalar` / ECDH `priv`.
489
- * Integer must be < 2^253 (not reduced mod r — values ≥ 2^253 are rejected).
300
+ * Map any integer into `(0, l)` so it satisfies circom `LessThan(252)` vs
301
+ * `BABYJUB_SUBGROUP_ORDER` and `BabyPbk`. Values already in range are unchanged.
490
302
  */
491
- function scalarHexToFrDecimal(hex) {
492
- const h = normalizeHex(hex);
493
- if (h.length > 64) {
494
- throw new Error('withdrawal-transaction-input: scalar hex too long');
303
+ function canonicalBabyJubScalarFromInteger(value) {
304
+ if (value > 0n && value < BABYJUB_SUBGROUP_ORDER) {
305
+ return value;
495
306
  }
496
- const v = BigInt(`0x${h.padStart(64, '0').slice(-64)}`);
497
- if (v >= BN254_BABYJUB_SCALAR_MAX_EXCLUSIVE) {
498
- throw new Error('depositor ephemeral scalar must be < 2^253 (BabyJub Num2Bits); resample with random-scalar');
499
- }
500
- return v.toString(10);
501
- }
502
- /**
503
- * Stellar G-address Ed25519 payload (32 bytes as 64 hex, optional 0x) → two circom public decimals
504
- * (`withdrawAddressHi` / `withdrawAddressLo`). No mod-r; each half fits in 128 bits.
505
- */
506
- function ed25519PubkeyPayloadHexToWithdrawFrDecimals(hex) {
507
- const h = normalizeHex(hex).padStart(64, '0').slice(-64);
508
- const hi = BigInt(`0x${h.slice(0, 32)}`);
509
- const lo = BigInt(`0x${h.slice(32, 64)}`);
510
- return { hi: hi.toString(10), lo: lo.toString(10) };
511
- }
512
- /**
513
- * Stellar contract id (`C…`) → two circom field decimals for `asset[0]`, `asset[1]` (same 32-byte split as accounts).
514
- */
515
- function stellarContractAddressToAssetFrDecimals(address) {
516
- const raw = stellarSdk.StrKey.decodeContract(address);
517
- const hex = Buffer.from(raw).toString('hex');
518
- const { hi, lo } = ed25519PubkeyPayloadHexToWithdrawFrDecimals(hex);
519
- return [hi, lo];
520
- }
521
- /** Uniform random `Fr` as decimal (32 random bytes, mod r). For Poseidon-only inputs (e.g. nullifiers). */
522
- function randomFrDecimal() {
523
- const hex = generateRandomScalarHex32();
524
- const v = BigInt(`0x${normalizeHex(hex)}`);
525
- return (v % BN254_SCALAR_MOD).toString(10);
526
- }
527
- /** Random scalar < 2^253 for BabyJub ECDH / `Num2Bits(253)` (uses {@link generateRandomScalarHex32}). */
528
- function randomFrDecimal253() {
529
- const hex = generateRandomScalarHex32();
530
- return BigInt(`0x${normalizeHex(hex)}`).toString(10);
531
- }
532
- function zerosTreeSiblings() {
533
- return Array(TRANSACTION_TREE_DEPTH).fill('0');
534
- }
535
- function dummyWithdraw(wasm) {
536
- const nullifier = randomFrDecimal();
537
- const secretHex = generateRandomScalarHex32();
538
- const secret = (BigInt(`0x${normalizeHex(secretHex)}`) % BN254_SCALAR_MOD).toString(10);
539
- const pt = wasm.ecdhEphemeralPublicKeyFromScalarHex(secretHex);
540
- return {
541
- value: '0',
542
- nullifier,
543
- secret,
544
- asset: ['0', '0'],
545
- applicationId: '0',
546
- ephemeralKeys: [coordHexToDecimal(pt.x), coordHexToDecimal(pt.y)],
547
- stateSiblings: zerosTreeSiblings(),
548
- stateIndex: '0',
549
- };
550
- }
551
- function dummyDeposit(wasm) {
552
- const nullifier = randomFrDecimal();
553
- const ephemeralKeyScalar = randomFrDecimal253();
554
- const skHex = generateRandomScalarHex32();
555
- const pt = wasm.ecdhEphemeralPublicKeyFromScalarHex(skHex);
556
- return {
557
- value: '0',
558
- nullifier,
559
- ephemeralKeyScalar,
560
- asset: ['0', '0'],
561
- applicationId: '0',
562
- recipientPublicKeys: [coordHexToDecimal(pt.x), coordHexToDecimal(pt.y)],
563
- };
564
- }
565
- function resolveWithdraw(slot, wasm) {
566
- return slot === 'dummy' ? dummyWithdraw(wasm) : slot;
567
- }
568
- function resolveDeposit(slot, wasm) {
569
- return slot === 'dummy' ? dummyDeposit(wasm) : slot;
570
- }
571
- function buildTransactionWitnessInput(publicParams, publicLegs, withdrawSlots, depositSlots, audit, wasm) {
572
- const w0 = resolveWithdraw(withdrawSlots[0], wasm);
573
- const w1 = resolveWithdraw(withdrawSlots[1], wasm);
574
- const d0 = resolveDeposit(depositSlots[0], wasm);
575
- const d1 = resolveDeposit(depositSlots[1], wasm);
576
- const appIds = resolveSlotApplicationIds(audit, withdrawSlots, depositSlots);
577
- return {
578
- stateRoot: publicParams.stateRoot,
579
- withdrawAddressHi: publicParams.withdrawAddressHi,
580
- withdrawAddressLo: publicParams.withdrawAddressLo,
581
- privKeyScalar: publicParams.privKeyScalar,
582
- withdrawnValues: [w0.value, w1.value],
583
- withdrawnNullifiers: [w0.nullifier, w1.nullifier],
584
- withdrawnSecrets: [w0.secret, w1.secret],
585
- withdrawnAssets: [w0.asset, w1.asset],
586
- ephemeralKeys: [w0.ephemeralKeys, w1.ephemeralKeys],
587
- stateSiblings: [w0.stateSiblings, w1.stateSiblings],
588
- stateIndex: [w0.stateIndex, w1.stateIndex],
589
- depositedValues: [d0.value, d1.value],
590
- depositedNullifiers: [d0.nullifier, d1.nullifier],
591
- depositedAssets: [d0.asset, d1.asset],
592
- depositedEphemeralKeyScalars: [d0.ephemeralKeyScalar, d1.ephemeralKeyScalar],
593
- depositedRecipientPublicKeys: [d0.recipientPublicKeys, d1.recipientPublicKeys],
594
- inputApplicationIds: appIds.inputApplicationIds,
595
- outputApplicationIds: appIds.outputApplicationIds,
596
- auditEphemeralScalars: audit.auditEphemeralScalars,
597
- noteAuditPublicKeys: audit.noteAuditPublicKeys,
598
- publicWithdrawnAssets: publicLegs.publicWithdrawnAssets,
599
- publicDepositedAssets: publicLegs.publicDepositedAssets,
600
- publicDeposits: publicLegs.publicDeposits,
601
- publicWithdrawals: publicLegs.publicWithdrawals,
602
- };
603
- }
604
- /** `stpl1…` stealth address → `[x, y]` as decimal field strings for `depositedRecipientPublicKeys`. */
605
- function recipientPublicKeysDecimalFromStealthAddress(stealthAddress) {
606
- const { x, y } = decodeStealthAddress(stealthAddress);
607
- return [coordHexToDecimal(x), coordHexToDecimal(y)];
608
- }
609
- /** First withdraw leg: Merkle witness + depositor ECDH point coordinates (hex). */
610
- function withdrawObjectFromMerkleWitness(witness, depositorEphemeralHex, applicationId) {
611
- return {
612
- value: witness.value,
613
- nullifier: witness.nullifier,
614
- secret: witness.secret,
615
- asset: witness.withdrawnAsset,
616
- applicationId,
617
- ephemeralKeys: [
618
- coordHexToDecimal(depositorEphemeralHex.x),
619
- coordHexToDecimal(depositorEphemeralHex.y),
620
- ],
621
- stateSiblings: witness.stateSiblings,
622
- stateIndex: witness.stateIndex,
623
- };
307
+ return (value % (BABYJUB_SUBGROUP_ORDER - 1n)) + 1n;
624
308
  }
625
309
 
626
- function hexToBytes(hex) {
310
+ function hexToBytes$1(hex) {
627
311
  const out = new Uint8Array(hex.length / 2);
628
312
  for (let i = 0; i < out.length; i++) {
629
313
  out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
630
314
  }
631
315
  return out;
632
316
  }
633
- function bytesToHexLower(bytes) {
634
- let s = '';
635
- for (let i = 0; i < bytes.length; i++) {
636
- s += bytes[i].toString(16).padStart(2, '0');
637
- }
638
- return s;
639
- }
640
317
  /** Base64 → bytes via `atob` (browsers; Node 16+). No Node `Buffer`. */
641
318
  function decodeBase64ToBytes(s) {
642
319
  const t = s.replace(/\s/g, '');
@@ -662,7 +339,7 @@ function parseStellarEd25519SignatureRaw(input) {
662
339
  const trimmed = input.trim();
663
340
  const no0x = trimmed.replace(/^0x/i, '');
664
341
  if (/^[0-9a-fA-F]+$/.test(no0x) && no0x.length === 128) {
665
- return hexToBytes(no0x.toLowerCase());
342
+ return hexToBytes$1(no0x.toLowerCase());
666
343
  }
667
344
  const fromB64 = decodeBase64ToBytes(trimmed);
668
345
  if (fromB64.length !== 64) {
@@ -680,29 +357,66 @@ async function sha256(data) {
680
357
  copy.set(data);
681
358
  return new Uint8Array(await subtle.digest('SHA-256', copy));
682
359
  }
360
+ function appendU32BE(parts, value) {
361
+ const buf = new Uint8Array(4);
362
+ new DataView(buf.buffer).setUint32(0, value >>> 0, false);
363
+ parts.push(buf);
364
+ }
365
+ function appendUtf8Prefixed(parts, text) {
366
+ const bytes = new TextEncoder().encode(text);
367
+ appendU32BE(parts, bytes.length);
368
+ parts.push(bytes);
369
+ }
370
+ function concatBytes(parts) {
371
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
372
+ const out = new Uint8Array(total);
373
+ let offset = 0;
374
+ for (const part of parts) {
375
+ out.set(part, offset);
376
+ offset += part.length;
377
+ }
378
+ return out;
379
+ }
683
380
  /**
684
- * Stellar Ed25519 signature (hex or base64) → SHA-256(signature bytes) → scalar → WASM ECDH (no UTF-8 seed hash).
381
+ * Domain-separated spend-scalar digest: SHA-256(
382
+ * SPEND_SCALAR_DOMAIN_TAG || network || pool || registry || schemaVersion || signature
383
+ * ).
685
384
  */
686
- async function stealthAddressFromStellarSignature(ecdhFromScalarHex, encodeStealth, signature) {
385
+ async function spendScalarDigestFromStellarSignature(signature, domain) {
687
386
  const raw = parseStellarEd25519SignatureRaw(signature);
688
- const scalar = await sha256(raw);
689
- const scalarHex = bytesToHexLower(scalar);
690
- const decoded = ecdhFromScalarHex(scalarHex);
387
+ const parts = [new TextEncoder().encode(SPEND_SCALAR_DOMAIN_TAG)];
388
+ appendUtf8Prefixed(parts, domain.networkPassphrase);
389
+ appendUtf8Prefixed(parts, domain.poolContract);
390
+ appendUtf8Prefixed(parts, domain.registryContract);
391
+ appendU32BE(parts, resolveSpendScalarSchemaVersion(domain));
392
+ parts.push(raw);
393
+ return sha256(concatBytes(parts));
394
+ }
395
+ function digestToCanonicalScalar(digest) {
396
+ let v = 0n;
397
+ for (let i = 0; i < digest.length; i++) {
398
+ v = (v << 8n) + BigInt(digest[i]);
399
+ }
400
+ return canonicalBabyJubScalarFromInteger(v);
401
+ }
402
+ function canonicalScalarHex(scalar) {
403
+ return scalar.toString(16).padStart(64, '0');
404
+ }
405
+ /**
406
+ * Stellar Ed25519 signature (hex or base64) → domain-separated SHA-256 → scalar → WASM ECDH.
407
+ */
408
+ async function stealthAddressFromStellarSignature(ecdhFromScalarHex, encodeStealth, signature, domain) {
409
+ const digest = await spendScalarDigestFromStellarSignature(signature, domain);
410
+ const decoded = ecdhFromScalarHex(canonicalScalarHex(digestToCanonicalScalar(digest)));
691
411
  return encodeStealth(decoded);
692
412
  }
693
413
  /**
694
- * SHA-256(signature) as 32-byte BE integer, truncated to **253 bits** (same effective scalar as
695
- * `libs/cryptography` `scalar_mul_253` / circom `Num2Bits(253)`), decimal for `privKeyScalar`.
414
+ * Domain-separated SHA-256(signature, network, pool, registry, schema) reduced into
415
+ * `(0, BabyJub subgroup order)` so `privKeyScalar` matches `BabyPbk` and circom `LessThan(l)`.
696
416
  */
697
- async function privKeyScalarDecimalFromStellarSignature(signature) {
698
- const raw = parseStellarEd25519SignatureRaw(signature);
699
- const h = await sha256(raw);
700
- let v = 0n;
701
- for (let i = 0; i < h.length; i++) {
702
- v = (v << 8n) + BigInt(h[i]);
703
- }
704
- const mask = BN254_BABYJUB_SCALAR_MAX_EXCLUSIVE - 1n;
705
- return (v & mask).toString(10);
417
+ async function privKeyScalarDecimalFromStellarSignature(signature, domain) {
418
+ const digest = await spendScalarDigestFromStellarSignature(signature, domain);
419
+ return digestToCanonicalScalar(digest).toString(10);
706
420
  }
707
421
 
708
422
  /* @ts-self-types="./client_sdk_wasm.d.ts" */
@@ -743,35 +457,69 @@ function buildWithdrawMerkleWitness(coin_json, state_json) {
743
457
  }
744
458
 
745
459
  /**
746
- * Calculate nullifier hash from nullifier decimal string.
460
+ * Calculate owner-bound nullifier hash from nullifier and spend-scalar decimal strings.
747
461
  * Returns hex string (0x...)
748
462
  * @param {string} nullifier_decimal
463
+ * @param {string} priv_key_scalar_decimal
749
464
  * @returns {string}
750
465
  */
751
- function calculateNullifierHash(nullifier_decimal) {
752
- let deferred3_0;
753
- let deferred3_1;
466
+ function calculateNullifierHash(nullifier_decimal, priv_key_scalar_decimal) {
467
+ let deferred4_0;
468
+ let deferred4_1;
754
469
  try {
755
470
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
756
471
  const ptr0 = passStringToWasm0(nullifier_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
757
472
  const len0 = WASM_VECTOR_LEN;
758
- wasm.calculateNullifierHash(retptr, ptr0, len0);
473
+ const ptr1 = passStringToWasm0(priv_key_scalar_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
474
+ const len1 = WASM_VECTOR_LEN;
475
+ wasm.calculateNullifierHash(retptr, ptr0, len0, ptr1, len1);
759
476
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
760
477
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
761
478
  var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
762
479
  var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
763
- var ptr2 = r0;
764
- var len2 = r1;
480
+ var ptr3 = r0;
481
+ var len3 = r1;
765
482
  if (r3) {
766
- ptr2 = 0; len2 = 0;
483
+ ptr3 = 0; len3 = 0;
767
484
  throw takeObject(r2);
768
485
  }
769
- deferred3_0 = ptr2;
770
- deferred3_1 = len2;
771
- return getStringFromWasm0(ptr2, len2);
486
+ deferred4_0 = ptr3;
487
+ deferred4_1 = len3;
488
+ return getStringFromWasm0(ptr3, len3);
489
+ } finally {
490
+ wasm.__wbindgen_add_to_stack_pointer(16);
491
+ wasm.__wbindgen_export4(deferred4_0, deferred4_1, 1);
492
+ }
493
+ }
494
+
495
+ /**
496
+ * `R = Poseidon255(2)(hi, lo)`; `scalar` is the raw Poseidon hash when it is
497
+ * already in `(0, BabyJub subgroup order)`. Out-of-range hashes fail so the
498
+ * caller can rejection-sample a new nonce.
499
+ * @param {string} nonce_decimal
500
+ * @param {string} recipient_hi_decimal
501
+ * @param {string} recipient_lo_decimal
502
+ * @returns {any}
503
+ */
504
+ function derivedEscrowKey(nonce_decimal, recipient_hi_decimal, recipient_lo_decimal) {
505
+ try {
506
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
507
+ const ptr0 = passStringToWasm0(nonce_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
508
+ const len0 = WASM_VECTOR_LEN;
509
+ const ptr1 = passStringToWasm0(recipient_hi_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
510
+ const len1 = WASM_VECTOR_LEN;
511
+ const ptr2 = passStringToWasm0(recipient_lo_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
512
+ const len2 = WASM_VECTOR_LEN;
513
+ wasm.derivedEscrowKey(retptr, ptr0, len0, ptr1, len1, ptr2, len2);
514
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
515
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
516
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
517
+ if (r2) {
518
+ throw takeObject(r1);
519
+ }
520
+ return takeObject(r0);
772
521
  } finally {
773
522
  wasm.__wbindgen_add_to_stack_pointer(16);
774
- wasm.__wbindgen_export4(deferred3_0, deferred3_1, 1);
775
523
  }
776
524
  }
777
525
 
@@ -852,26 +600,28 @@ function ecdhSharedKey(priv_hex, pub_x_hex, pub_y_hex) {
852
600
  }
853
601
 
854
602
  /**
855
- * Generate a new coin with random nullifier, secret, and shared-secret field elements.
856
- * `amount` is stroops (u64); JS passes `bigint`.
603
+ * Generate a new coin with random nullifier, secret, and owner public-key field elements.
604
+ * `amount_decimal` is an arbitrary-precision decimal field element (never JS `number`).
857
605
  * `asset_hi_decimal` / `asset_lo_decimal` are decimal Fr strings for the Stellar asset contract id (two limbs).
858
606
  * Returns JSON: { coin: { value, nullifier, secret, commitment, asset_hi, asset_lo }, commitment_hex, precommitement_hex }
859
- * @param {bigint} amount
607
+ * @param {string} amount_decimal
860
608
  * @param {string} asset_hi_decimal
861
609
  * @param {string} asset_lo_decimal
862
610
  * @param {string} application_id_decimal
863
611
  * @returns {any}
864
612
  */
865
- function generateCoin(amount, asset_hi_decimal, asset_lo_decimal, application_id_decimal) {
613
+ function generateCoin(amount_decimal, asset_hi_decimal, asset_lo_decimal, application_id_decimal) {
866
614
  try {
867
615
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
868
- const ptr0 = passStringToWasm0(asset_hi_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
616
+ const ptr0 = passStringToWasm0(amount_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
869
617
  const len0 = WASM_VECTOR_LEN;
870
- const ptr1 = passStringToWasm0(asset_lo_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
618
+ const ptr1 = passStringToWasm0(asset_hi_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
871
619
  const len1 = WASM_VECTOR_LEN;
872
- const ptr2 = passStringToWasm0(application_id_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
620
+ const ptr2 = passStringToWasm0(asset_lo_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
873
621
  const len2 = WASM_VECTOR_LEN;
874
- wasm.generateCoin(retptr, amount, ptr0, len0, ptr1, len1, ptr2, len2);
622
+ const ptr3 = passStringToWasm0(application_id_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
623
+ const len3 = WASM_VECTOR_LEN;
624
+ wasm.generateCoin(retptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
875
625
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
876
626
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
877
627
  var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
@@ -885,32 +635,34 @@ function generateCoin(amount, asset_hi_decimal, asset_lo_decimal, application_id
885
635
  }
886
636
 
887
637
  /**
888
- * `Poseidon₁(scalar)` secret + fixed ECDH shared coords (hex), matching an aligned deposit witness.
638
+ * `Poseidon₁(scalar)` secret + recipient owner public key (hex), matching an aligned deposit witness.
889
639
  * @param {string} scalar_hex
890
- * @param {string} shared_x_hex
891
- * @param {string} shared_y_hex
892
- * @param {bigint} amount
640
+ * @param {string} owner_x_hex
641
+ * @param {string} owner_y_hex
642
+ * @param {string} amount_decimal
893
643
  * @param {string} asset_hi_decimal
894
644
  * @param {string} asset_lo_decimal
895
645
  * @param {string} application_id_decimal
896
646
  * @returns {any}
897
647
  */
898
- function generateCoinForDepositWithSharedHex(scalar_hex, shared_x_hex, shared_y_hex, amount, asset_hi_decimal, asset_lo_decimal, application_id_decimal) {
648
+ function generateCoinForDepositWithOwnerPubHex(scalar_hex, owner_x_hex, owner_y_hex, amount_decimal, asset_hi_decimal, asset_lo_decimal, application_id_decimal) {
899
649
  try {
900
650
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
901
651
  const ptr0 = passStringToWasm0(scalar_hex, wasm.__wbindgen_export, wasm.__wbindgen_export2);
902
652
  const len0 = WASM_VECTOR_LEN;
903
- const ptr1 = passStringToWasm0(shared_x_hex, wasm.__wbindgen_export, wasm.__wbindgen_export2);
653
+ const ptr1 = passStringToWasm0(owner_x_hex, wasm.__wbindgen_export, wasm.__wbindgen_export2);
904
654
  const len1 = WASM_VECTOR_LEN;
905
- const ptr2 = passStringToWasm0(shared_y_hex, wasm.__wbindgen_export, wasm.__wbindgen_export2);
655
+ const ptr2 = passStringToWasm0(owner_y_hex, wasm.__wbindgen_export, wasm.__wbindgen_export2);
906
656
  const len2 = WASM_VECTOR_LEN;
907
- const ptr3 = passStringToWasm0(asset_hi_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
657
+ const ptr3 = passStringToWasm0(amount_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
908
658
  const len3 = WASM_VECTOR_LEN;
909
- const ptr4 = passStringToWasm0(asset_lo_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
659
+ const ptr4 = passStringToWasm0(asset_hi_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
910
660
  const len4 = WASM_VECTOR_LEN;
911
- const ptr5 = passStringToWasm0(application_id_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
661
+ const ptr5 = passStringToWasm0(asset_lo_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
912
662
  const len5 = WASM_VECTOR_LEN;
913
- wasm.generateCoinForDepositWithSharedHex(retptr, ptr0, len0, ptr1, len1, ptr2, len2, amount, ptr3, len3, ptr4, len4, ptr5, len5);
663
+ const ptr6 = passStringToWasm0(application_id_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
664
+ const len6 = WASM_VECTOR_LEN;
665
+ wasm.generateCoinForDepositWithOwnerPubHex(retptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, ptr5, len5, ptr6, len6);
914
666
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
915
667
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
916
668
  var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
@@ -926,24 +678,26 @@ function generateCoinForDepositWithSharedHex(scalar_hex, shared_x_hex, shared_y_
926
678
  /**
927
679
  * `secret` in coin = `Poseidon255(1)(scalar)` per `deposit.circom`; scalar is 32-byte hex (64 chars, optional `0x`).
928
680
  * @param {string} scalar_hex
929
- * @param {bigint} amount
681
+ * @param {string} amount_decimal
930
682
  * @param {string} asset_hi_decimal
931
683
  * @param {string} asset_lo_decimal
932
684
  * @param {string} application_id_decimal
933
685
  * @returns {any}
934
686
  */
935
- function generateCoinFromDepositEphemeralScalarHex(scalar_hex, amount, asset_hi_decimal, asset_lo_decimal, application_id_decimal) {
687
+ function generateCoinFromDepositEphemeralScalarHex(scalar_hex, amount_decimal, asset_hi_decimal, asset_lo_decimal, application_id_decimal) {
936
688
  try {
937
689
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
938
690
  const ptr0 = passStringToWasm0(scalar_hex, wasm.__wbindgen_export, wasm.__wbindgen_export2);
939
691
  const len0 = WASM_VECTOR_LEN;
940
- const ptr1 = passStringToWasm0(asset_hi_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
692
+ const ptr1 = passStringToWasm0(amount_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
941
693
  const len1 = WASM_VECTOR_LEN;
942
- const ptr2 = passStringToWasm0(asset_lo_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
694
+ const ptr2 = passStringToWasm0(asset_hi_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
943
695
  const len2 = WASM_VECTOR_LEN;
944
- const ptr3 = passStringToWasm0(application_id_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
696
+ const ptr3 = passStringToWasm0(asset_lo_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
945
697
  const len3 = WASM_VECTOR_LEN;
946
- wasm.generateCoinFromDepositEphemeralScalarHex(retptr, ptr0, len0, amount, ptr1, len1, ptr2, len2, ptr3, len3);
698
+ const ptr4 = passStringToWasm0(application_id_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
699
+ const len4 = WASM_VECTOR_LEN;
700
+ wasm.generateCoinFromDepositEphemeralScalarHex(retptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4);
947
701
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
948
702
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
949
703
  var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
@@ -957,29 +711,31 @@ function generateCoinFromDepositEphemeralScalarHex(scalar_hex, amount, asset_hi_
957
711
  }
958
712
 
959
713
  /**
960
- * Same as `generateCoin`, but commitment uses the given ECDH shared key (64-char hex coords from `ecdhSharedKey`); shared coords are not stored in `coin` JSON.
961
- * @param {string} shared_x_hex
962
- * @param {string} shared_y_hex
963
- * @param {bigint} amount
714
+ * Same as `generateCoin`, but commitment uses the given owner public key (64-char hex coords).
715
+ * @param {string} owner_x_hex
716
+ * @param {string} owner_y_hex
717
+ * @param {string} amount_decimal
964
718
  * @param {string} asset_hi_decimal
965
719
  * @param {string} asset_lo_decimal
966
720
  * @param {string} application_id_decimal
967
721
  * @returns {any}
968
722
  */
969
- function generateCoinWithSharedSecretHex(shared_x_hex, shared_y_hex, amount, asset_hi_decimal, asset_lo_decimal, application_id_decimal) {
723
+ function generateCoinWithOwnerPubHex(owner_x_hex, owner_y_hex, amount_decimal, asset_hi_decimal, asset_lo_decimal, application_id_decimal) {
970
724
  try {
971
725
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
972
- const ptr0 = passStringToWasm0(shared_x_hex, wasm.__wbindgen_export, wasm.__wbindgen_export2);
726
+ const ptr0 = passStringToWasm0(owner_x_hex, wasm.__wbindgen_export, wasm.__wbindgen_export2);
973
727
  const len0 = WASM_VECTOR_LEN;
974
- const ptr1 = passStringToWasm0(shared_y_hex, wasm.__wbindgen_export, wasm.__wbindgen_export2);
728
+ const ptr1 = passStringToWasm0(owner_y_hex, wasm.__wbindgen_export, wasm.__wbindgen_export2);
975
729
  const len1 = WASM_VECTOR_LEN;
976
- const ptr2 = passStringToWasm0(asset_hi_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
730
+ const ptr2 = passStringToWasm0(amount_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
977
731
  const len2 = WASM_VECTOR_LEN;
978
- const ptr3 = passStringToWasm0(asset_lo_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
732
+ const ptr3 = passStringToWasm0(asset_hi_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
979
733
  const len3 = WASM_VECTOR_LEN;
980
- const ptr4 = passStringToWasm0(application_id_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
734
+ const ptr4 = passStringToWasm0(asset_lo_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
981
735
  const len4 = WASM_VECTOR_LEN;
982
- wasm.generateCoinWithSharedSecretHex(retptr, ptr0, len0, ptr1, len1, amount, ptr2, len2, ptr3, len3, ptr4, len4);
736
+ const ptr5 = passStringToWasm0(application_id_decimal, wasm.__wbindgen_export, wasm.__wbindgen_export2);
737
+ const len5 = WASM_VECTOR_LEN;
738
+ wasm.generateCoinWithOwnerPubHex(retptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, ptr5, len5);
983
739
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
984
740
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
985
741
  var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
@@ -1016,6 +772,38 @@ function proofToHex(proof_json) {
1016
772
  }
1017
773
  }
1018
774
 
775
+ /**
776
+ * Graph witness + arkworks Groth16 prove. Returns `{ proof_hex, public_hex }`.
777
+ * @param {Uint8Array} graph
778
+ * @param {Uint8Array} proving_key
779
+ * @param {Uint8Array} r1cs
780
+ * @param {string} inputs_json
781
+ * @returns {any}
782
+ */
783
+ function proveGroth16(graph, proving_key, r1cs, inputs_json) {
784
+ try {
785
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
786
+ const ptr0 = passArray8ToWasm0(graph, wasm.__wbindgen_export);
787
+ const len0 = WASM_VECTOR_LEN;
788
+ const ptr1 = passArray8ToWasm0(proving_key, wasm.__wbindgen_export);
789
+ const len1 = WASM_VECTOR_LEN;
790
+ const ptr2 = passArray8ToWasm0(r1cs, wasm.__wbindgen_export);
791
+ const len2 = WASM_VECTOR_LEN;
792
+ const ptr3 = passStringToWasm0(inputs_json, wasm.__wbindgen_export, wasm.__wbindgen_export2);
793
+ const len3 = WASM_VECTOR_LEN;
794
+ wasm.proveGroth16(retptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
795
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
796
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
797
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
798
+ if (r2) {
799
+ throw takeObject(r1);
800
+ }
801
+ return takeObject(r0);
802
+ } finally {
803
+ wasm.__wbindgen_add_to_stack_pointer(16);
804
+ }
805
+ }
806
+
1019
807
  /**
1020
808
  * Convert snarkjs public signals JSON to hex bytes for Soroban contract.
1021
809
  * @param {string} public_json
@@ -1226,6 +1014,13 @@ function isLikeNone(x) {
1226
1014
  return x === undefined || x === null;
1227
1015
  }
1228
1016
 
1017
+ function passArray8ToWasm0(arg, malloc) {
1018
+ const ptr = malloc(arg.length * 1, 1) >>> 0;
1019
+ getUint8ArrayMemory0().set(arg, ptr / 1);
1020
+ WASM_VECTOR_LEN = arg.length;
1021
+ return ptr;
1022
+ }
1023
+
1229
1024
  function passStringToWasm0(arg, malloc, realloc) {
1230
1025
  if (realloc === undefined) {
1231
1026
  const buf = cachedTextEncoder.encode(arg);
@@ -1388,28 +1183,30 @@ async function __wbg_init(module_or_path) {
1388
1183
  }
1389
1184
 
1390
1185
  var wasmBindings = /*#__PURE__*/Object.freeze({
1391
- __proto__: null,
1392
- buildWithdrawMerkleWitness: buildWithdrawMerkleWitness,
1393
- calculateNullifierHash: calculateNullifierHash,
1394
- default: __wbg_init,
1395
- ecdhEphemeralPublicKey: ecdhEphemeralPublicKey,
1396
- ecdhEphemeralPublicKeyFromScalarHex: ecdhEphemeralPublicKeyFromScalarHex,
1397
- ecdhSharedKey: ecdhSharedKey,
1398
- generateCoin: generateCoin,
1399
- generateCoinForDepositWithSharedHex: generateCoinForDepositWithSharedHex,
1400
- generateCoinFromDepositEphemeralScalarHex: generateCoinFromDepositEphemeralScalarHex,
1401
- generateCoinWithSharedSecretHex: generateCoinWithSharedSecretHex,
1402
- initSync: initSync,
1403
- proofToHex: proofToHex,
1404
- publicToHex: publicToHex
1186
+ __proto__: null,
1187
+ buildWithdrawMerkleWitness: buildWithdrawMerkleWitness,
1188
+ calculateNullifierHash: calculateNullifierHash,
1189
+ default: __wbg_init,
1190
+ derivedEscrowKey: derivedEscrowKey,
1191
+ ecdhEphemeralPublicKey: ecdhEphemeralPublicKey,
1192
+ ecdhEphemeralPublicKeyFromScalarHex: ecdhEphemeralPublicKeyFromScalarHex,
1193
+ ecdhSharedKey: ecdhSharedKey,
1194
+ generateCoin: generateCoin,
1195
+ generateCoinForDepositWithOwnerPubHex: generateCoinForDepositWithOwnerPubHex,
1196
+ generateCoinFromDepositEphemeralScalarHex: generateCoinFromDepositEphemeralScalarHex,
1197
+ generateCoinWithOwnerPubHex: generateCoinWithOwnerPubHex,
1198
+ initSync: initSync,
1199
+ proofToHex: proofToHex,
1200
+ proveGroth16: proveGroth16,
1201
+ publicToHex: publicToHex
1405
1202
  });
1406
1203
 
1407
- const isNode$2 = typeof process !== 'undefined' && !!process.versions?.node;
1204
+ const isNode$1 = typeof process !== 'undefined' && !!process.versions?.node;
1408
1205
  let wasmModule = null;
1409
1206
  async function loadWasm(wasmBinary) {
1410
1207
  if (wasmModule)
1411
1208
  return wasmModule;
1412
- if (isNode$2) {
1209
+ if (isNode$1) {
1413
1210
  if (wasmBinary) {
1414
1211
  initSync({ module: wasmBinary });
1415
1212
  }
@@ -1436,511 +1233,1020 @@ async function loadWasm(wasmBinary) {
1436
1233
  return wasmModule;
1437
1234
  }
1438
1235
 
1439
- var witness_calculator = async function builder(code, options) {
1440
-
1441
- options = options || {};
1442
-
1443
- let wasmModule;
1444
- try {
1445
- wasmModule = await WebAssembly.compile(code);
1446
- } catch (err) {
1447
- console.log(err);
1448
- console.log("\nTry to run circom --c in order to generate c++ code instead\n");
1449
- throw new Error(err);
1450
- }
1451
-
1452
- let wc;
1453
-
1454
- let errStr = "";
1455
- let msgStr = "";
1456
-
1457
- const instance = await WebAssembly.instantiate(wasmModule, {
1458
- runtime: {
1459
- exceptionHandler : function(code) {
1460
- let err;
1461
- if (code == 1) {
1462
- err = "Signal not found.\n";
1463
- } else if (code == 2) {
1464
- err = "Too many signals set.\n";
1465
- } else if (code == 3) {
1466
- err = "Signal already set.\n";
1467
- } else if (code == 4) {
1468
- err = "Assert Failed.\n";
1469
- } else if (code == 5) {
1470
- err = "Not enough memory.\n";
1471
- } else if (code == 6) {
1472
- err = "Input signal array access exceeds the size.\n";
1473
- } else if (code == 7) {
1474
- err = "Out of bounds array access.\n";
1475
- } else {
1476
- err = "Unknown error.\n";
1477
- }
1478
- throw new Error(err + errStr);
1479
- },
1480
- printErrorMessage : function() {
1481
- errStr += getMessage() + "\n";
1482
- // console.error(getMessage());
1483
- },
1484
- writeBufferMessage : function() {
1485
- const msg = getMessage();
1486
- // Any calls to `log()` will always end with a `\n`, so that's when we print and reset
1487
- if (msg === "\n") {
1488
- console.log(msgStr);
1489
- msgStr = "";
1490
- } else {
1491
- // If we've buffered other content, put a space in between the items
1492
- if (msgStr !== "") {
1493
- msgStr += " ";
1494
- }
1495
- // Then append the message to the message we are creating
1496
- msgStr += msg;
1497
- }
1498
- },
1499
- showSharedRWMemory : function() {
1500
- printSharedRWMemory ();
1501
- }
1502
-
1503
- }
1504
- });
1505
-
1506
- const sanityCheck =
1507
- options;
1508
- // options &&
1509
- // (
1510
- // options.sanityCheck ||
1511
- // options.logGetSignal ||
1512
- // options.logSetSignal ||
1513
- // options.logStartComponent ||
1514
- // options.logFinishComponent
1515
- // );
1516
-
1517
-
1518
- wc = new WitnessCalculator(instance, sanityCheck);
1519
- return wc;
1520
-
1521
- function getMessage() {
1522
- var message = "";
1523
- var c = instance.exports.getMessageChar();
1524
- while ( c != 0 ) {
1525
- message += String.fromCharCode(c);
1526
- c = instance.exports.getMessageChar();
1527
- }
1528
- return message;
1529
- }
1530
-
1531
- function printSharedRWMemory () {
1532
- const shared_rw_memory_size = instance.exports.getFieldNumLen32();
1533
- const arr = new Uint32Array(shared_rw_memory_size);
1534
- for (let j=0; j<shared_rw_memory_size; j++) {
1535
- arr[shared_rw_memory_size-1-j] = instance.exports.readSharedRWMemory(j);
1536
- }
1537
-
1538
- // If we've buffered other content, put a space in between the items
1539
- if (msgStr !== "") {
1540
- msgStr += " ";
1541
- }
1542
- // Then append the value to the message we are creating
1543
- msgStr += (fromArray32(arr).toString());
1544
- }
1545
-
1236
+ /** Off-chain ZK layout table. Must stay byte-identical to `libs/zk::ZkLayoutParams`. */
1237
+ const BINDING_ZK_NONCE = 3n;
1238
+ const SIX_BY_SIX_BINDING_ZK_NONCE = 7n;
1239
+ const V2_PUBLIC_INPUT_PREFIX_LEN = 7;
1240
+ const NOTE_AUDIT_LEN$1 = 12;
1241
+ const NOTE_OUTPUT_LEN = 6;
1242
+ var BundledZkCircuit;
1243
+ (function (BundledZkCircuit) {
1244
+ BundledZkCircuit["TwoByTwo"] = "2x2";
1245
+ BundledZkCircuit["SixBySix"] = "6x6";
1246
+ })(BundledZkCircuit || (BundledZkCircuit = {}));
1247
+ function isDevZkCircuitConfig(config) {
1248
+ return !('circuit' in config);
1249
+ }
1250
+ const DEFAULT_ZK_CONFIG_NONCE = BINDING_ZK_NONCE;
1251
+ const DEFAULT_ZK_CIRCUITS = {
1252
+ [BINDING_ZK_NONCE.toString()]: {
1253
+ nIns: 2,
1254
+ nOuts: 2,
1255
+ publicNInputs: 1,
1256
+ publicNOutputs: 1,
1257
+ circuit: BundledZkCircuit.TwoByTwo,
1258
+ },
1259
+ [SIX_BY_SIX_BINDING_ZK_NONCE.toString()]: {
1260
+ nIns: 6,
1261
+ nOuts: 6,
1262
+ publicNInputs: 1,
1263
+ publicNOutputs: 1,
1264
+ circuit: BundledZkCircuit.SixBySix,
1265
+ },
1546
1266
  };
1547
-
1548
- class WitnessCalculator {
1549
- constructor(instance, sanityCheck) {
1550
- this.instance = instance;
1551
-
1552
- this.version = this.instance.exports.getVersion();
1553
- this.n32 = this.instance.exports.getFieldNumLen32();
1554
-
1555
- this.instance.exports.getRawPrime();
1556
- const arr = new Uint32Array(this.n32);
1557
- for (let i=0; i<this.n32; i++) {
1558
- arr[this.n32-1-i] = this.instance.exports.readSharedRWMemory(i);
1559
- }
1560
- this.prime = fromArray32(arr);
1561
-
1562
- this.witnessSize = this.instance.exports.getWitnessSize();
1563
-
1564
- this.sanityCheck = sanityCheck;
1565
- }
1566
-
1567
- circom_version() {
1568
- return this.instance.exports.getVersion();
1569
- }
1570
-
1571
- async _doCalculateWitness(input_orig, sanityCheck) {
1572
- //input is assumed to be a map from signals to arrays of bigints
1573
- this.instance.exports.init((this.sanityCheck || sanityCheck) ? 1 : 0);
1574
- let prefix = "";
1575
- var input = new Object();
1576
- //console.log("Input: ", input_orig);
1577
- qualify_input(prefix,input_orig,input);
1578
- //console.log("Input after: ",input);
1579
- const keys = Object.keys(input);
1580
- var input_counter = 0;
1581
- keys.forEach( (k) => {
1582
- const h = fnvHash(k);
1583
- const hMSB = parseInt(h.slice(0,8), 16);
1584
- const hLSB = parseInt(h.slice(8,16), 16);
1585
- const fArr = flatArray(input[k]);
1586
- let signalSize = this.instance.exports.getInputSignalSize(hMSB, hLSB);
1587
- if (signalSize < 0){
1588
- throw new Error(`Signal ${k} not found\n`);
1589
- }
1590
- if (fArr.length < signalSize) {
1591
- throw new Error(`Not enough values for input signal ${k}\n`);
1592
- }
1593
- if (fArr.length > signalSize) {
1594
- throw new Error(`Too many values for input signal ${k}\n`);
1595
- }
1596
- for (let i=0; i<fArr.length; i++) {
1597
- const arrFr = toArray32(normalize(fArr[i],this.prime),this.n32);
1598
- for (let j=0; j<this.n32; j++) {
1599
- this.instance.exports.writeSharedRWMemory(j,arrFr[this.n32-1-j]);
1600
- }
1601
- try {
1602
- this.instance.exports.setInputSignal(hMSB, hLSB,i);
1603
- input_counter++;
1604
- } catch (err) {
1605
- // console.log(`After adding signal ${i} of ${k}`)
1606
- throw new Error(err);
1607
- }
1608
- }
1609
-
1610
- });
1611
- if (input_counter < this.instance.exports.getInputSize()) {
1612
- throw new Error(`Not all inputs have been set. Only ${input_counter} out of ${this.instance.exports.getInputSize()}`);
1613
- }
1267
+ function layoutFromCircuitConfig(config) {
1268
+ return zkLayoutFromShapeBinding(config.nIns, config.nOuts, config.publicNInputs, config.publicNOutputs, config.nIns + config.nOuts, NOTE_AUDIT_LEN$1, NOTE_OUTPUT_LEN);
1269
+ }
1270
+ function resolveZkCircuitConfig(zkCircuits, zkConfigNonce) {
1271
+ const nonce = zkConfigNonce ?? DEFAULT_ZK_CONFIG_NONCE;
1272
+ const circuits = zkCircuits ?? DEFAULT_ZK_CIRCUITS;
1273
+ const config = circuits[nonce.toString()];
1274
+ if (!config) {
1275
+ throw new Error(`no zk circuit configured for nonce ${nonce.toString()}`);
1614
1276
  }
1615
-
1616
- async calculateWitness(input, sanityCheck) {
1617
-
1618
- const w = [];
1619
- await this._doCalculateWitness(input, sanityCheck);
1620
-
1621
- for (let i=0; i<this.witnessSize; i++) {
1622
- this.instance.exports.getWitness(i);
1623
- const arr = new Uint32Array(this.n32);
1624
- for (let j=0; j<this.n32; j++) {
1625
- arr[this.n32-1-j] = this.instance.exports.readSharedRWMemory(j);
1626
- }
1627
- w.push(fromArray32(arr));
1628
- }
1629
-
1630
- return w;
1277
+ return { nonce, config };
1278
+ }
1279
+ function bundledCircuitFileNames(circuit) {
1280
+ if (circuit === BundledZkCircuit.SixBySix) {
1281
+ return {
1282
+ graphFileName: 'main_6x6.graph.bin',
1283
+ r1csFileName: 'main_6x6.r1cs.gz',
1284
+ provingKeyFileName: 'main_6x6_proving_key.bin',
1285
+ verificationKeyFileName: 'main_6x6_verification_key.json',
1286
+ zkeyFileName: 'main_6x6_final.zkey',
1287
+ };
1631
1288
  }
1632
-
1633
-
1634
- async calculateBinWitness(input, sanityCheck) {
1635
-
1636
- const buff32 = new Uint32Array(this.witnessSize*this.n32);
1637
- const buff = new Uint8Array( buff32.buffer);
1638
- await this._doCalculateWitness(input, sanityCheck);
1639
-
1640
- for (let i=0; i<this.witnessSize; i++) {
1641
- this.instance.exports.getWitness(i);
1642
- const pos = i*this.n32;
1643
- for (let j=0; j<this.n32; j++) {
1644
- buff32[pos+j] = this.instance.exports.readSharedRWMemory(j);
1645
- }
1646
- }
1289
+ return {
1290
+ graphFileName: 'main.graph.bin',
1291
+ r1csFileName: 'main.r1cs.gz',
1292
+ provingKeyFileName: 'main_proving_key.bin',
1293
+ verificationKeyFileName: 'main_verification_key.json',
1294
+ zkeyFileName: 'main_final.zkey',
1295
+ };
1296
+ }
1297
+ function zkLayoutFromShapeBinding(nIns, nOuts, publicNInputs, publicNOutputs, nAuditSlots, noteAuditLen, noteOutputLen) {
1298
+ const auditOffset = nIns + nOuts + nOuts * 2;
1299
+ const outputNoteOffset = auditOffset + 2 + nAuditSlots;
1300
+ return {
1301
+ nIns,
1302
+ nOuts,
1303
+ publicNInputs,
1304
+ publicNOutputs,
1305
+ nAuditSlots,
1306
+ noteAuditLen,
1307
+ noteOutputLen,
1308
+ publicInputPrefixLen: V2_PUBLIC_INPUT_PREFIX_LEN,
1309
+ auditOffset,
1310
+ outputNoteOffset,
1311
+ };
1312
+ }
1313
+ function ciphertextFieldCount(layout) {
1314
+ return layout.nAuditSlots * layout.noteAuditLen + layout.nOuts * layout.noteOutputLen;
1315
+ }
1647
1316
 
1648
- return buff;
1317
+ const ZK_ARTIFACT_GITHUB_REPO = "Polynom-Labs/stellar-privacy-layer-contracts";
1318
+ const ZK_SDK_PACKAGE_VERSION = "0.9.1-rc.0";
1319
+ function defaultZkArtifactBaseUrl(version = ZK_SDK_PACKAGE_VERSION) {
1320
+ const tag = version.startsWith("v") ? version.slice(1) : version;
1321
+ return `https://github.com/${ZK_ARTIFACT_GITHUB_REPO}/releases/download/v${tag}`;
1322
+ }
1323
+ function trimArtifactBaseUrl(value) {
1324
+ let trimmed = value.trim();
1325
+ while (trimmed.endsWith("/")) {
1326
+ trimmed = trimmed.slice(0, -1);
1649
1327
  }
1650
-
1651
-
1652
- async calculateWTNSBin(input, sanityCheck) {
1653
-
1654
- const buff32 = new Uint32Array(this.witnessSize*this.n32+this.n32+11);
1655
- const buff = new Uint8Array( buff32.buffer);
1656
- await this._doCalculateWitness(input, sanityCheck);
1657
-
1658
- //"wtns"
1659
- buff[0] = "w".charCodeAt(0);
1660
- buff[1] = "t".charCodeAt(0);
1661
- buff[2] = "n".charCodeAt(0);
1662
- buff[3] = "s".charCodeAt(0);
1663
-
1664
- //version 2
1665
- buff32[1] = 2;
1666
-
1667
- //number of sections: 2
1668
- buff32[2] = 2;
1669
-
1670
- //id section 1
1671
- buff32[3] = 1;
1672
-
1673
- const n8 = this.n32*4;
1674
- //id section 1 length in 64bytes
1675
- const idSection1length = 8 + n8;
1676
- const idSection1lengthHex = idSection1length.toString(16);
1677
- buff32[4] = parseInt(idSection1lengthHex.slice(0,8), 16);
1678
- buff32[5] = parseInt(idSection1lengthHex.slice(8,16), 16);
1679
-
1680
- //this.n32
1681
- buff32[6] = n8;
1328
+ return trimmed;
1329
+ }
1682
1330
 
1683
- //prime number
1684
- this.instance.exports.getRawPrime();
1331
+ const isNode = typeof process !== 'undefined' && !!process.versions?.node;
1332
+ function toUint8Array(src) {
1333
+ if (src instanceof Uint8Array) {
1334
+ return src;
1335
+ }
1336
+ if (src instanceof ArrayBuffer) {
1337
+ return new Uint8Array(src);
1338
+ }
1339
+ return new Uint8Array(src.buffer, src.byteOffset, src.byteLength);
1340
+ }
1341
+ function nodeEnvArtifactBaseUrl() {
1342
+ if (!isNode) {
1343
+ return undefined;
1344
+ }
1345
+ const fromEnv = process.env.ZK_ARTIFACT_BASE_URL?.trim();
1346
+ return fromEnv ? trimArtifactBaseUrl(fromEnv) : undefined;
1347
+ }
1348
+ function resolveZkArtifactBaseUrl(explicit) {
1349
+ const trimmed = explicit?.trim();
1350
+ if (trimmed) {
1351
+ return trimArtifactBaseUrl(trimmed);
1352
+ }
1353
+ return nodeEnvArtifactBaseUrl() ?? defaultZkArtifactBaseUrl();
1354
+ }
1355
+ function isHttpUrl(value) {
1356
+ return /^https?:\/\//i.test(value) || value.startsWith('file:');
1357
+ }
1358
+ async function readNodeAssetFile(filePath) {
1359
+ const fs = await import('fs');
1360
+ if (!fs.existsSync(filePath)) {
1361
+ throw new Error(`Circuit artifact not found at ${filePath}. Set zkArtifactBaseUrl or ZK_ARTIFACT_BASE_URL.`);
1362
+ }
1363
+ return fs.readFileSync(filePath);
1364
+ }
1365
+ async function fetchArtifact(url) {
1366
+ const response = await fetch(url);
1367
+ if (!response.ok) {
1368
+ throw new Error(`Failed to fetch ${url}: ${String(response.status)}`);
1369
+ }
1370
+ return new Uint8Array(await response.arrayBuffer());
1371
+ }
1372
+ function remoteArtifactHref(base, fileName) {
1373
+ const withSlash = `${trimArtifactBaseUrl(base)}/`;
1374
+ if (isHttpUrl(withSlash)) {
1375
+ return new URL(fileName, withSlash).href;
1376
+ }
1377
+ if (typeof window !== 'undefined') {
1378
+ return new URL(fileName, new URL(withSlash, window.location.href)).href;
1379
+ }
1380
+ return `${withSlash}${fileName}`;
1381
+ }
1382
+ async function loadNamedArtifact(base, fileName) {
1383
+ if (isNode && !isHttpUrl(base)) {
1384
+ const nodePath = await import('path');
1385
+ return readNodeAssetFile(nodePath.resolve(base, fileName));
1386
+ }
1387
+ return fetchArtifact(remoteArtifactHref(base, fileName));
1388
+ }
1389
+ async function loadBundledCircuitArtifacts(circuit, artifactBaseUrl) {
1390
+ const base = resolveZkArtifactBaseUrl(artifactBaseUrl);
1391
+ const names = bundledCircuitFileNames(circuit);
1392
+ const [graph, r1cs, provingKey] = await Promise.all([
1393
+ loadNamedArtifact(base, names.graphFileName),
1394
+ loadNamedArtifact(base, names.r1csFileName),
1395
+ loadNamedArtifact(base, names.provingKeyFileName),
1396
+ ]);
1397
+ return { graph, r1cs, provingKey };
1398
+ }
1399
+ async function resolveCircuitArtifacts(config, artifactBaseUrl) {
1400
+ if (isDevZkCircuitConfig(config)) {
1401
+ return {
1402
+ graph: toUint8Array(config.circuitGraph),
1403
+ r1cs: toUint8Array(config.r1cs),
1404
+ provingKey: toUint8Array(config.provingKey),
1405
+ };
1406
+ }
1407
+ return loadBundledCircuitArtifacts(config.circuit, artifactBaseUrl);
1408
+ }
1685
1409
 
1686
- var pos = 7;
1687
- for (let j=0; j<this.n32; j++) {
1688
- buff32[pos+j] = this.instance.exports.readSharedRWMemory(j);
1689
- }
1690
- pos += this.n32;
1691
-
1692
- // witness size
1693
- buff32[pos] = this.witnessSize;
1694
- pos++;
1695
-
1696
- //id section 2
1697
- buff32[pos] = 2;
1698
- pos++;
1699
-
1700
- // section 2 length
1701
- const idSection2length = n8*this.witnessSize;
1702
- const idSection2lengthHex = idSection2length.toString(16);
1703
- buff32[pos] = parseInt(idSection2lengthHex.slice(0,8), 16);
1704
- buff32[pos+1] = parseInt(idSection2lengthHex.slice(8,16), 16);
1705
-
1706
- pos += 2;
1707
- for (let i=0; i<this.witnessSize; i++) {
1708
- this.instance.exports.getWitness(i);
1709
- for (let j=0; j<this.n32; j++) {
1710
- buff32[pos+j] = this.instance.exports.readSharedRWMemory(j);
1711
- }
1712
- pos += this.n32;
1410
+ /** Bech32 HRP for {@link DecodedEphemeralKey} only (`x ‖ y`, 64 bytes). */
1411
+ const DECODED_EPHEMERAL_HRP = 'epk1';
1412
+ /** Bech32 HRP for {@link DecodedDepositorSharedSecretPreimage} (96 bytes). */
1413
+ const DEPOSITOR_SHARED_SECRET_PREIMAGE_HRP = 'epk_dep_pre1';
1414
+ const FIELD_BYTES = 32;
1415
+ const EPK_POINT_PAYLOAD = FIELD_BYTES * 2;
1416
+ const EPK_DEP_PRE_PAYLOAD = FIELD_BYTES * 3;
1417
+ const BECH32_LONG_LIMIT = 1023;
1418
+ function normalizeHex$2(hex) {
1419
+ const s = hex.trim().replace(/^0x/i, '');
1420
+ if (!/^[0-9a-fA-F]*$/.test(s)) {
1421
+ throw new Error('ephemeral-key: hex must contain only 0-9, a-f');
1422
+ }
1423
+ return s.length % 2 === 0 ? s : `0${s}`;
1424
+ }
1425
+ function hexToBytes(hex) {
1426
+ const norm = normalizeHex$2(hex);
1427
+ const out = new Uint8Array(norm.length / 2);
1428
+ for (let i = 0; i < out.length; i++) {
1429
+ out[i] = parseInt(norm.slice(i * 2, i * 2 + 2), 16);
1430
+ }
1431
+ return out;
1432
+ }
1433
+ function bytesToHex(bytes) {
1434
+ let s = '';
1435
+ for (let i = 0; i < bytes.length; i++) {
1436
+ s += bytes[i].toString(16).padStart(2, '0');
1437
+ }
1438
+ return s;
1439
+ }
1440
+ function concat2(a, b) {
1441
+ const out = new Uint8Array(a.length + b.length);
1442
+ out.set(a, 0);
1443
+ out.set(b, a.length);
1444
+ return out;
1445
+ }
1446
+ function concat3(a, b, c) {
1447
+ const out = new Uint8Array(a.length + b.length + c.length);
1448
+ out.set(a, 0);
1449
+ out.set(b, a.length);
1450
+ out.set(c, a.length + b.length);
1451
+ return out;
1452
+ }
1453
+ function require32(label, bytes) {
1454
+ if (bytes.length !== FIELD_BYTES) {
1455
+ throw new Error(`ephemeral-key: ${label} must encode exactly ${FIELD_BYTES} bytes, got ${bytes.length}`);
1456
+ }
1457
+ }
1458
+ /**
1459
+ * 32-byte big-endian integer as hex (64 chars), **< 2^253**, so BabyJub ECDH matches
1460
+ * `circuits/encryption.circom` `Num2Bits(253)` and `libs/cryptography` `scalar_mul_253`.
1461
+ */
1462
+ function generateRandomScalarHex32() {
1463
+ const g = globalThis.crypto;
1464
+ if (!g?.getRandomValues) {
1465
+ throw new Error('ephemeral-key: crypto.getRandomValues is required');
1466
+ }
1467
+ const max = 1n << 253n;
1468
+ for (let attempt = 0; attempt < 65536; attempt++) {
1469
+ const b = new Uint8Array(32);
1470
+ g.getRandomValues(b);
1471
+ const hex = bytesToHex(b);
1472
+ if (BigInt(`0x${hex}`) < max) {
1473
+ return hex;
1713
1474
  }
1714
-
1715
- return buff;
1716
1475
  }
1717
-
1476
+ throw new Error('ephemeral-key: failed to sample scalar < 2^253');
1718
1477
  }
1719
-
1720
-
1721
- function qualify_input_list(prefix,input,input1){
1722
- if (Array.isArray(input)) {
1723
- for (let i = 0; i<input.length; i++) {
1724
- let new_prefix = prefix + "[" + i + "]";
1725
- qualify_input_list(new_prefix,input[i],input1);
1726
- }
1727
- } else {
1728
- qualify_input(prefix,input,input1);
1729
- }
1730
- }
1731
-
1732
- function qualify_input(prefix,input,input1) {
1733
- if (Array.isArray(input)) {
1734
- let a = flatArray(input);
1735
- if (a.length > 0) {
1736
- let t = typeof a[0];
1737
- for (let i = 1; i<a.length; i++) {
1738
- if (typeof a[i] != t){
1739
- throw new Error(`Types are not the same in the key ${prefix}`);
1740
- }
1741
- }
1742
- if (t == "object") {
1743
- qualify_input_list(prefix,input,input1);
1744
- } else {
1745
- input1[prefix] = input;
1746
- }
1747
- } else {
1748
- input1[prefix] = input;
1749
- }
1750
- } else if (typeof input == "object") {
1751
- const keys = Object.keys(input);
1752
- keys.forEach( (k) => {
1753
- let new_prefix = prefix == ""? k : prefix + "." + k;
1754
- qualify_input(new_prefix,input[k],input1);
1755
- });
1756
- } else {
1757
- input1[prefix] = input;
1478
+ /** Bech32 `epk1`: encodes `x ‖ y` (64 bytes). */
1479
+ function encodeDecodedEphemeralKey(decoded) {
1480
+ const xb = hexToBytes(decoded.x);
1481
+ const yb = hexToBytes(decoded.y);
1482
+ require32('x', xb);
1483
+ require32('y', yb);
1484
+ const payload = concat2(xb, yb);
1485
+ const words = bech32.toWords(payload);
1486
+ return bech32.encode(DECODED_EPHEMERAL_HRP, words, BECH32_LONG_LIMIT);
1487
+ }
1488
+ function decodeDecodedEphemeralKey(encoded) {
1489
+ const { prefix, words } = bech32.decode(encoded, BECH32_LONG_LIMIT);
1490
+ if (prefix !== DECODED_EPHEMERAL_HRP) {
1491
+ throw new Error(`ephemeral-key: expected HRP ${DECODED_EPHEMERAL_HRP}, got ${JSON.stringify(prefix)}`);
1492
+ }
1493
+ const bytes = new Uint8Array(bech32.fromWords(words));
1494
+ if (bytes.length !== EPK_POINT_PAYLOAD) {
1495
+ throw new Error(`ephemeral-key: epk1 payload must be ${EPK_POINT_PAYLOAD} bytes, got ${bytes.length}`);
1758
1496
  }
1497
+ return {
1498
+ x: bytesToHex(bytes.subarray(0, FIELD_BYTES)),
1499
+ y: bytesToHex(bytes.subarray(FIELD_BYTES)),
1500
+ };
1759
1501
  }
1760
-
1761
- function toArray32(rem,size) {
1762
- const res = []; //new Uint32Array(size); //has no unshift
1763
- const radix = BigInt(0x100000000);
1764
- while (rem) {
1765
- res.unshift( Number(rem % radix));
1766
- rem = rem / radix;
1502
+ /** Bech32 `epk_dep_pre1`: `randomNonceScalar ‖ recipient.x ‖ recipient.y`. */
1503
+ function encodeDepositorSharedSecretPreimage(decoded) {
1504
+ const sb = hexToBytes(decoded.randomNonceScalar);
1505
+ const xb = hexToBytes(decoded.recipientStealthAddress.x);
1506
+ const yb = hexToBytes(decoded.recipientStealthAddress.y);
1507
+ require32('randomNonceScalar', sb);
1508
+ require32('recipientStealthAddress.x', xb);
1509
+ require32('recipientStealthAddress.y', yb);
1510
+ const payload = concat3(sb, xb, yb);
1511
+ const words = bech32.toWords(payload);
1512
+ return bech32.encode(DEPOSITOR_SHARED_SECRET_PREIMAGE_HRP, words, BECH32_LONG_LIMIT);
1513
+ }
1514
+ function decodeDepositorSharedSecretPreimage(encoded) {
1515
+ const { prefix, words } = bech32.decode(encoded, BECH32_LONG_LIMIT);
1516
+ if (prefix !== DEPOSITOR_SHARED_SECRET_PREIMAGE_HRP) {
1517
+ throw new Error(`ephemeral-key: expected HRP ${DEPOSITOR_SHARED_SECRET_PREIMAGE_HRP}, got ${JSON.stringify(prefix)}`);
1518
+ }
1519
+ const bytes = new Uint8Array(bech32.fromWords(words));
1520
+ if (bytes.length !== EPK_DEP_PRE_PAYLOAD) {
1521
+ throw new Error(`ephemeral-key: epk_dep_pre1 payload must be ${EPK_DEP_PRE_PAYLOAD} bytes, got ${bytes.length}`);
1767
1522
  }
1768
- if (size) {
1769
- var i = size - res.length;
1770
- while (i>0) {
1771
- res.unshift(0);
1772
- i--;
1773
- }
1523
+ return {
1524
+ randomNonceScalar: bytesToHex(bytes.subarray(0, FIELD_BYTES)),
1525
+ recipientStealthAddress: {
1526
+ x: bytesToHex(bytes.subarray(FIELD_BYTES, FIELD_BYTES * 2)),
1527
+ y: bytesToHex(bytes.subarray(FIELD_BYTES * 2)),
1528
+ },
1529
+ };
1530
+ }
1531
+
1532
+ /**
1533
+ * Depositor: `randomNonceScalar * recipientStealthPoint` (same as circom `ECDH` with depositor scalar).
1534
+ */
1535
+ function sharedSecretFromDepositorPreimage(ecdhShared, preimage) {
1536
+ const out = ecdhShared(preimage.randomNonceScalar, preimage.recipientStealthAddress.x, preimage.recipientStealthAddress.y);
1537
+ return { x: out.x, y: out.y };
1538
+ }
1539
+ /**
1540
+ * Recipient: `recipientScalar * ephemeralKey` (same shared point as depositor path when keys match).
1541
+ */
1542
+ function sharedSecretFromRecipientPreimage(ecdhShared, preimage) {
1543
+ const out = ecdhShared(preimage.recipientScalar, preimage.ephemeralKey.x, preimage.ephemeralKey.y);
1544
+ return { x: out.x, y: out.y };
1545
+ }
1546
+
1547
+ const TRANSACTION_N_AUDIT_SLOTS = 4;
1548
+ const DEFAULT_APPLICATION_ID = '101';
1549
+ /** BabyJub audit public key (decimal Fr) used in BDD / local demo when env is unset. */
1550
+ const DEMO_AUDIT_PUBLIC_KEY = [
1551
+ '21605515851820432880964235241069234202284600780825340516808373216881770219365',
1552
+ '18856460861531942120859708048677603751294231190189224157283439874962410808705',
1553
+ ];
1554
+ function isActiveWithdraw(slot) {
1555
+ return slot.value !== '0';
1556
+ }
1557
+ function isActiveDeposit(slot) {
1558
+ return slot.value !== '0';
1559
+ }
1560
+ function resolveWithdrawSlot(slot) {
1561
+ if (slot === 'dummy') {
1562
+ return null;
1774
1563
  }
1775
- return res;
1564
+ return slot;
1776
1565
  }
1777
-
1778
- function fromArray32(arr) { //returns a BigInt
1779
- var res = BigInt(0);
1780
- const radix = BigInt(0x100000000);
1781
- for (let i = 0; i<arr.length; i++) {
1782
- res = res*radix + BigInt(arr[i]);
1566
+ function resolveDepositSlot(slot) {
1567
+ if (slot === 'dummy') {
1568
+ return null;
1783
1569
  }
1784
- return res;
1570
+ return slot;
1785
1571
  }
1786
-
1787
- function flatArray(a) {
1788
- var res = [];
1789
- fillArray(res, a);
1790
- return res;
1791
-
1792
- function fillArray(res, a) {
1793
- if (Array.isArray(a)) {
1794
- for (let i=0; i<a.length; i++) {
1795
- fillArray(res, a[i]);
1796
- }
1797
- } else {
1798
- res.push(a);
1799
- }
1800
- }
1572
+ function buildUniformAuditParams(applicationId = DEFAULT_APPLICATION_ID, auditPublicKey, _nAuditSlots = TRANSACTION_N_AUDIT_SLOTS) {
1573
+ return {
1574
+ applicationId,
1575
+ noteAuditPublicKey: auditPublicKey,
1576
+ auditEphemeralScalar: randomFrDecimal253(),
1577
+ };
1801
1578
  }
1802
-
1803
- function normalize(n, prime) {
1804
- let res = BigInt(n) % prime;
1805
- if (res < 0) res += prime;
1806
- return res
1579
+ function resolveSlotApplicationIds(audit, withdrawSlots, depositSlots) {
1580
+ return {
1581
+ inputApplicationIds: withdrawSlots.map((slot) => {
1582
+ const resolved = resolveWithdrawSlot(slot);
1583
+ return resolved && isActiveWithdraw(resolved) ? audit.applicationId : '0';
1584
+ }),
1585
+ outputApplicationIds: depositSlots.map((slot) => {
1586
+ const resolved = resolveDepositSlot(slot);
1587
+ return resolved && isActiveDeposit(resolved) ? audit.applicationId : '0';
1588
+ }),
1589
+ };
1807
1590
  }
1808
-
1809
- function fnvHash(str) {
1810
- const uint64_max = BigInt(2) ** BigInt(64);
1811
- let hash = BigInt("0xCBF29CE484222325");
1812
- for (var i = 0; i < str.length; i++) {
1813
- hash ^= BigInt(str[i].charCodeAt());
1814
- hash *= BigInt(0x100000001B3);
1815
- hash %= uint64_max;
1591
+ /**
1592
+ * Prefer an explicit key, then `NOTE_AUDIT_PUBLIC_KEY_X`/`_Y` env (decimal Fr),
1593
+ * then the built-in demo key. Used by CLI/`demo.sh` for per-app audit pubkeys.
1594
+ */
1595
+ function resolveAuditPublicKeyFromEnv(auditPublicKey) {
1596
+ if (auditPublicKey) {
1597
+ return auditPublicKey;
1816
1598
  }
1817
- let shash = hash.toString(16);
1818
- let n = 16 - shash.length;
1819
- shash = '0'.repeat(n).concat(shash);
1820
- return shash;
1599
+ const x = process.env.NOTE_AUDIT_PUBLIC_KEY_X?.trim();
1600
+ const y = process.env.NOTE_AUDIT_PUBLIC_KEY_Y?.trim();
1601
+ if (x && y) {
1602
+ return [x, y];
1603
+ }
1604
+ return DEMO_AUDIT_PUBLIC_KEY;
1605
+ }
1606
+ function resolveTransactionAuditParams(applicationId, auditPublicKey, nAuditSlots = TRANSACTION_N_AUDIT_SLOTS) {
1607
+ return buildUniformAuditParams(applicationId, resolveAuditPublicKeyFromEnv(auditPublicKey), nAuditSlots);
1821
1608
  }
1822
1609
 
1823
- var witnessCalculatorModule = /*@__PURE__*/getDefaultExportFromCjs(witness_calculator);
1824
-
1825
- const isNode$1 = typeof process !== 'undefined' && !!process.versions?.node;
1826
- async function generateWitness(input, circuitWasm) {
1827
- let wasmBuffer;
1828
- let wc;
1829
- if (isNode$1) {
1830
- const fs = await import('fs');
1831
- const nodePath = await import('path');
1832
- const { fileURLToPath } = await import('url');
1833
- const dir = nodePath.dirname(fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.js', document.baseURI).href))));
1834
- const assetsDir = nodePath.resolve(dir, '..', 'assets');
1835
- wc = { default: witnessCalculatorModule };
1836
- if (circuitWasm) {
1837
- wasmBuffer = circuitWasm;
1610
+ /** Matches `Transaction(20, 2, 2, publicNInputs, publicNOutputs, 4, 12, 6)` in `circuits/main.circom`. */
1611
+ const TRANSACTION_TREE_DEPTH = 20;
1612
+ const TRANSACTION_N_INS = 2;
1613
+ const TRANSACTION_N_OUTS = 2;
1614
+ /** Must match `Transaction(..., publicNInputs, publicNOutputs)` and contract `get_public_slot_config`. */
1615
+ const TRANSACTION_PUBLIC_N_INPUTS = 1;
1616
+ const TRANSACTION_PUBLIC_N_OUTPUTS = 1;
1617
+ /** BN254 scalar field modulus (ark `Fr`, circom signals). */
1618
+ const BN254_SCALAR_MOD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
1619
+ /**
1620
+ * BabyJub ECDH in `circuits/encryption.circom` uses `Num2Bits(253)`; scalars must be < 2^253
1621
+ * (matches `libs/cryptography` `scalar_mul_253`).
1622
+ */
1623
+ const BN254_BABYJUB_SCALAR_MAX_EXCLUSIVE = 1n << 253n;
1624
+ function padPublicLegs(legs, publicNInputs, publicNOutputs) {
1625
+ const padPairs = (values, expected, label) => {
1626
+ if (values.length > expected) {
1627
+ throw new Error(`withdrawal-transaction-input: expected at most ${expected} ${label}`);
1838
1628
  }
1839
- else {
1840
- const wasmPath = nodePath.resolve(assetsDir, 'main.wasm');
1841
- if (!fs.existsSync(wasmPath)) {
1842
- throw new Error(`Circuit WASM not found at ${wasmPath}. Run build.sh first.`);
1843
- }
1844
- wasmBuffer = fs.readFileSync(wasmPath);
1629
+ return [
1630
+ ...values,
1631
+ ...Array.from({ length: expected - values.length }, () => ['0', '0']),
1632
+ ];
1633
+ };
1634
+ const padAmounts = (values, expected, label) => {
1635
+ if (values.length > expected) {
1636
+ throw new Error(`withdrawal-transaction-input: expected at most ${expected} ${label}`);
1845
1637
  }
1638
+ return [...values, ...Array.from({ length: expected - values.length }, () => '0')];
1639
+ };
1640
+ return {
1641
+ publicWithdrawnAssets: padPairs(legs.publicWithdrawnAssets, publicNOutputs, 'public withdrawn assets'),
1642
+ publicDepositedAssets: padPairs(legs.publicDepositedAssets, publicNInputs, 'public deposited assets'),
1643
+ publicDeposits: padAmounts(legs.publicDeposits, publicNInputs, 'public deposits'),
1644
+ publicWithdrawals: padAmounts(legs.publicWithdrawals, publicNOutputs, 'public withdrawals'),
1645
+ };
1646
+ }
1647
+ function normalizeHex$1(hex) {
1648
+ const s = hex.trim().replace(/^0x/i, '');
1649
+ if (!/^[0-9a-fA-F]*$/.test(s)) {
1650
+ throw new Error('withdrawal-transaction-input: invalid hex');
1846
1651
  }
1847
- else {
1848
- if (!circuitWasm) {
1849
- throw new Error('In browser, you must pass circuitWasm to PrivacyPoolSDK.init(). ' +
1850
- 'Load the circuit WASM file via fetch() and pass the ArrayBuffer.');
1851
- }
1852
- wasmBuffer = circuitWasm;
1853
- wc = { default: witnessCalculatorModule };
1652
+ return s.length % 2 === 0 ? s : `0${s}`;
1653
+ }
1654
+ /** 32-byte field coordinate (hex, no 0x) → decimal string mod BN254 scalar field. */
1655
+ function coordHexToDecimal(hex) {
1656
+ const h = normalizeHex$1(hex);
1657
+ if (h.length > 64) {
1658
+ throw new Error('withdrawal-transaction-input: coordinate hex too long');
1854
1659
  }
1855
- const calculator = await wc.default(wasmBuffer);
1856
- const wtns = await calculator.calculateWTNSBin(input, 0);
1857
- return wtns;
1660
+ const v = BigInt(`0x${h}`);
1661
+ return (v % BN254_SCALAR_MOD).toString(10);
1858
1662
  }
1859
-
1860
- // @ts-ignore - snarkjs types
1861
- const isNode = typeof process !== 'undefined' && !!process.versions?.node;
1862
1663
  /**
1863
- * Normalize BufferSource to Uint8Array. Required for @iden3/binfileutils/fastfile:
1864
- * they read via data.buffer and data.byteOffset, which ArrayBuffer does not have.
1664
+ * 32-byte big-endian scalar hex → decimal for circom `ephemeralKeyScalar` / ECDH `priv`.
1665
+ * Integer must be < 2^253 (not reduced mod r — values ≥ 2^253 are rejected).
1865
1666
  */
1866
- function toUint8Array(src) {
1867
- if (src instanceof Uint8Array)
1868
- return src;
1869
- if (src instanceof ArrayBuffer)
1870
- return new Uint8Array(src);
1871
- return new Uint8Array(src.buffer, src.byteOffset, src.byteLength);
1872
- }
1873
- async function generateProof(wtns, zkey) {
1874
- let zkeyData;
1875
- if (zkey) {
1876
- zkeyData = toUint8Array(zkey);
1877
- }
1878
- else if (isNode) {
1879
- const fs = await import('fs');
1880
- const nodePath = await import('path');
1881
- const { fileURLToPath } = await import('url');
1882
- const dir = nodePath.dirname(fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.js', document.baseURI).href))));
1883
- const zkeyPath = nodePath.resolve(dir, '..', 'assets', 'main_final.zkey');
1884
- if (!fs.existsSync(zkeyPath)) {
1885
- throw new Error(`Proving key not found at ${zkeyPath}. Run build.sh first.`);
1886
- }
1887
- const zkeyBuffer = fs.readFileSync(zkeyPath);
1888
- zkeyData = toUint8Array(zkeyBuffer);
1667
+ function scalarHexToFrDecimal(hex) {
1668
+ const h = normalizeHex$1(hex);
1669
+ if (h.length > 64) {
1670
+ throw new Error('withdrawal-transaction-input: scalar hex too long');
1889
1671
  }
1890
- else {
1891
- throw new Error('In browser, you must pass zkey to PrivacyPoolSDK.init(). ' +
1892
- 'Load the zkey file via fetch() and pass the ArrayBuffer.');
1672
+ const v = BigInt(`0x${h.padStart(64, '0').slice(-64)}`);
1673
+ if (v >= BN254_BABYJUB_SCALAR_MAX_EXCLUSIVE) {
1674
+ throw new Error('depositor ephemeral scalar must be < 2^253 (BabyJub Num2Bits); resample with random-scalar');
1893
1675
  }
1894
- const wtnsData = toUint8Array(wtns);
1895
- const { proof, publicSignals } = await snarkjs__namespace.groth16.prove({ type: 'mem', data: zkeyData }, { type: 'mem', data: wtnsData });
1896
- return { proof, publicSignals };
1676
+ return v.toString(10);
1897
1677
  }
1898
-
1899
1678
  /**
1900
- * Depositor: `randomNonceScalar * recipientStealthPoint` (same as circom `ECDH` with depositor scalar).
1679
+ * Stellar G-address Ed25519 payload (32 bytes as 64 hex, optional 0x) → two circom public decimals
1680
+ * (`withdrawAddressHi` / `withdrawAddressLo`). No mod-r; each half fits in 128 bits.
1901
1681
  */
1902
- function sharedSecretFromDepositorPreimage(ecdhShared, preimage) {
1903
- const out = ecdhShared(preimage.randomNonceScalar, preimage.recipientStealthAddress.x, preimage.recipientStealthAddress.y);
1904
- return { x: out.x, y: out.y };
1682
+ function ed25519PubkeyPayloadHexToWithdrawFrDecimals(hex) {
1683
+ const h = normalizeHex$1(hex).padStart(64, '0').slice(-64);
1684
+ const hi = BigInt(`0x${h.slice(0, 32)}`);
1685
+ const lo = BigInt(`0x${h.slice(32, 64)}`);
1686
+ return { hi: hi.toString(10), lo: lo.toString(10) };
1905
1687
  }
1906
1688
  /**
1907
- * Recipient: `recipientScalar * ephemeralKey` (same shared point as depositor path when keys match).
1689
+ * Stellar contract id (`C…`) → two circom field decimals for `asset[0]`, `asset[1]` (same 32-byte split as accounts).
1908
1690
  */
1909
- function sharedSecretFromRecipientPreimage(ecdhShared, preimage) {
1910
- const out = ecdhShared(preimage.recipientScalar, preimage.ephemeralKey.x, preimage.ephemeralKey.y);
1911
- return { x: out.x, y: out.y };
1691
+ function stellarContractAddressToAssetFrDecimals(address) {
1692
+ const raw = stellarSdk.StrKey.decodeContract(address);
1693
+ const hex = Buffer.from(raw).toString('hex');
1694
+ const { hi, lo } = ed25519PubkeyPayloadHexToWithdrawFrDecimals(hex);
1695
+ return [hi, lo];
1696
+ }
1697
+ /** Uniform random `Fr` as decimal (32 random bytes, mod r). For Poseidon-only inputs (e.g. nullifiers). */
1698
+ function randomFrDecimal() {
1699
+ const hex = generateRandomScalarHex32();
1700
+ const v = BigInt(`0x${normalizeHex$1(hex)}`);
1701
+ return (v % BN254_SCALAR_MOD).toString(10);
1702
+ }
1703
+ /** Random scalar < 2^253 for BabyJub ECDH / `Num2Bits(253)` (uses {@link generateRandomScalarHex32}). */
1704
+ function randomFrDecimal253() {
1705
+ const hex = generateRandomScalarHex32();
1706
+ return BigInt(`0x${normalizeHex$1(hex)}`).toString(10);
1707
+ }
1708
+ /** Spend-scalar hex in `(0, BabyJub subgroup order)` so dummy withdraws satisfy `LessThan(l)`. */
1709
+ function randomCanonicalBabyJubScalarHex() {
1710
+ const hex = generateRandomScalarHex32();
1711
+ const reduced = canonicalBabyJubScalarFromInteger(BigInt(`0x${normalizeHex$1(hex)}`));
1712
+ return reduced.toString(16).padStart(64, '0');
1713
+ }
1714
+ function zerosTreeSiblings() {
1715
+ return Array(TRANSACTION_TREE_DEPTH).fill('0');
1716
+ }
1717
+ function dummyWithdraw(wasm) {
1718
+ const nullifier = randomFrDecimal();
1719
+ const secretHex = generateRandomScalarHex32();
1720
+ const secret = (BigInt(`0x${normalizeHex$1(secretHex)}`) % BN254_SCALAR_MOD).toString(10);
1721
+ const scalarHex = randomCanonicalBabyJubScalarHex();
1722
+ const privKeyScalar = scalarHexToFrDecimal(scalarHex);
1723
+ const pt = wasm.ecdhEphemeralPublicKeyFromScalarHex(scalarHex);
1724
+ return {
1725
+ value: '0',
1726
+ nullifier,
1727
+ secret,
1728
+ asset: ['0', '0'],
1729
+ applicationId: '0',
1730
+ ownerPub: [coordHexToDecimal(pt.x), coordHexToDecimal(pt.y)],
1731
+ privKeyScalar,
1732
+ paddingRandom: randomFrDecimal(),
1733
+ escrowNonce: '0',
1734
+ recipientStellar: ['0', '0'],
1735
+ stateSiblings: zerosTreeSiblings(),
1736
+ stateIndex: '0',
1737
+ };
1738
+ }
1739
+ function dummyDeposit(wasm) {
1740
+ const nullifier = randomFrDecimal();
1741
+ const ephemeralKeyScalar = randomFrDecimal253();
1742
+ const skHex = generateRandomScalarHex32();
1743
+ const pt = wasm.ecdhEphemeralPublicKeyFromScalarHex(skHex);
1744
+ return {
1745
+ value: '0',
1746
+ nullifier,
1747
+ ephemeralKeyScalar,
1748
+ asset: ['0', '0'],
1749
+ applicationId: '0',
1750
+ recipientPublicKeys: [coordHexToDecimal(pt.x), coordHexToDecimal(pt.y)],
1751
+ escrowNonce: '0',
1752
+ recipientStellar: ['0', '0'],
1753
+ };
1754
+ }
1755
+ function resolveWithdraw(slot, wasm) {
1756
+ return slot === 'dummy' ? dummyWithdraw(wasm) : slot;
1757
+ }
1758
+ function resolveDeposit(slot, wasm) {
1759
+ return slot === 'dummy' ? dummyDeposit(wasm) : slot;
1760
+ }
1761
+ function padWithdrawSlots(slots, nIns) {
1762
+ if (slots.length > nIns) {
1763
+ throw new Error(`withdrawal-transaction-input: expected at most ${nIns} withdraw slots`);
1764
+ }
1765
+ return [...slots, ...Array.from({ length: nIns - slots.length }, () => 'dummy')];
1766
+ }
1767
+ function padDepositSlots(slots, nOuts) {
1768
+ if (slots.length > nOuts) {
1769
+ throw new Error(`withdrawal-transaction-input: expected at most ${nOuts} deposit slots`);
1770
+ }
1771
+ return [...slots, ...Array.from({ length: nOuts - slots.length }, () => 'dummy')];
1772
+ }
1773
+ function buildTransactionWitnessInput(publicParams, publicLegs, withdrawSlots, depositSlots, audit, wasm, dims = {}) {
1774
+ const nIns = dims.nIns ?? TRANSACTION_N_INS;
1775
+ const nOuts = dims.nOuts ?? TRANSACTION_N_OUTS;
1776
+ const publicNInputs = dims.publicNInputs ?? TRANSACTION_PUBLIC_N_INPUTS;
1777
+ const publicNOutputs = dims.publicNOutputs ?? TRANSACTION_PUBLIC_N_OUTPUTS;
1778
+ const paddedWithdraws = padWithdrawSlots(withdrawSlots, nIns);
1779
+ const paddedDeposits = padDepositSlots(depositSlots, nOuts);
1780
+ const paddedLegs = padPublicLegs(publicLegs, publicNInputs, publicNOutputs);
1781
+ const withdraws = paddedWithdraws.map((slot) => resolveWithdraw(slot, wasm));
1782
+ const deposits = paddedDeposits.map((slot) => resolveDeposit(slot, wasm));
1783
+ const appIds = resolveSlotApplicationIds(audit, paddedWithdraws, paddedDeposits);
1784
+ return {
1785
+ stateRoot: publicParams.stateRoot,
1786
+ withdrawAddressHi: publicParams.withdrawAddressHi,
1787
+ withdrawAddressLo: publicParams.withdrawAddressLo,
1788
+ escrowRecipientHi: publicParams.escrowRecipientHi ?? '0',
1789
+ escrowRecipientLo: publicParams.escrowRecipientLo ?? '0',
1790
+ sweepOutputOwnerPubX: publicParams.sweepOutputOwnerPubX ?? '0',
1791
+ sweepOutputOwnerPubY: publicParams.sweepOutputOwnerPubY ?? '0',
1792
+ privKeyScalars: withdraws.map((w) => w.privKeyScalar),
1793
+ ownerPubs: withdraws.map((w) => w.ownerPub),
1794
+ paddingRandoms: withdraws.map((w) => w.paddingRandom),
1795
+ withdrawnValues: withdraws.map((w) => w.value),
1796
+ withdrawnNullifiers: withdraws.map((w) => w.nullifier),
1797
+ withdrawnSecrets: withdraws.map((w) => w.secret),
1798
+ withdrawnAssets: withdraws.map((w) => w.asset),
1799
+ withdrawnEscrowNonces: withdraws.map((w) => w.escrowNonce),
1800
+ inputRecipientStellar: withdraws.map((w) => w.recipientStellar),
1801
+ stateSiblings: withdraws.map((w) => w.stateSiblings),
1802
+ stateIndex: withdraws.map((w) => w.stateIndex),
1803
+ depositedValues: deposits.map((d) => d.value),
1804
+ depositedNullifiers: deposits.map((d) => d.nullifier),
1805
+ depositedAssets: deposits.map((d) => d.asset),
1806
+ depositedEphemeralKeyScalars: deposits.map((d) => d.ephemeralKeyScalar),
1807
+ depositedRecipientPublicKeys: deposits.map((d) => d.recipientPublicKeys),
1808
+ depositedEscrowNonces: deposits.map((d) => d.escrowNonce ?? '0'),
1809
+ outputRecipientStellar: deposits.map((d) => d.recipientStellar ?? ['0', '0']),
1810
+ inputApplicationIds: appIds.inputApplicationIds,
1811
+ outputApplicationIds: appIds.outputApplicationIds,
1812
+ auditEphemeralScalar: audit.auditEphemeralScalar,
1813
+ noteAuditPublicKey: audit.noteAuditPublicKey,
1814
+ publicWithdrawnAssets: paddedLegs.publicWithdrawnAssets,
1815
+ publicDepositedAssets: paddedLegs.publicDepositedAssets,
1816
+ publicDeposits: paddedLegs.publicDeposits,
1817
+ publicWithdrawals: paddedLegs.publicWithdrawals,
1818
+ };
1819
+ }
1820
+ /** `stpl1…` stealth address → `[x, y]` as decimal field strings for `depositedRecipientPublicKeys`. */
1821
+ function recipientPublicKeysDecimalFromStealthAddress(stealthAddress) {
1822
+ const { x, y } = decodeStealthAddress(stealthAddress);
1823
+ return [coordHexToDecimal(x), coordHexToDecimal(y)];
1824
+ }
1825
+ /** First withdraw leg: Merkle witness + owner public key (hex) and matching scalar. */
1826
+ function withdrawObjectFromMerkleWitness(witness, ownerPubHex, applicationId, privKeyScalar) {
1827
+ return {
1828
+ value: witness.value,
1829
+ nullifier: witness.nullifier,
1830
+ secret: witness.secret,
1831
+ asset: witness.withdrawnAsset,
1832
+ applicationId,
1833
+ ownerPub: [coordHexToDecimal(ownerPubHex.x), coordHexToDecimal(ownerPubHex.y)],
1834
+ privKeyScalar,
1835
+ paddingRandom: randomFrDecimal(),
1836
+ escrowNonce: '0',
1837
+ recipientStellar: ['0', '0'],
1838
+ stateSiblings: witness.stateSiblings,
1839
+ stateIndex: witness.stateIndex,
1840
+ };
1841
+ }
1842
+
1843
+ const FR_SIZE$1 = 32;
1844
+ const CIPHERTEXT_BLOB_HEADER_BYTES = 4;
1845
+ const FR_MODULUS$1 = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
1846
+ function moduleFr$1(value) {
1847
+ const reduced = value % FR_MODULUS$1;
1848
+ return reduced >= 0n ? reduced : reduced + FR_MODULUS$1;
1849
+ }
1850
+ function fieldToBytes(value) {
1851
+ let remaining = moduleFr$1(value);
1852
+ const buf = Buffer.alloc(FR_SIZE$1);
1853
+ for (let index = FR_SIZE$1 - 1; index >= 0; index -= 1) {
1854
+ buf[index] = Number(remaining & 0xffn);
1855
+ remaining >>= 8n;
1856
+ }
1857
+ return buf;
1858
+ }
1859
+ function decimalOrHexToBigint(value) {
1860
+ const trimmed = value.trim();
1861
+ if (/^0x/i.test(trimmed) || /^[0-9a-f]{64}$/i.test(trimmed)) {
1862
+ return BigInt(`0x${trimmed.replace(/^0x/i, '')}`);
1863
+ }
1864
+ return BigInt(trimmed);
1865
+ }
1866
+ function encodeCiphertextBlob(fields) {
1867
+ const prefix = Buffer.alloc(CIPHERTEXT_BLOB_HEADER_BYTES);
1868
+ prefix.writeUInt32BE(fields.length);
1869
+ return Buffer.concat([
1870
+ prefix,
1871
+ ...fields.map((field) => fieldToBytes(decimalOrHexToBigint(field))),
1872
+ ]);
1873
+ }
1874
+ function encodeTransactionCiphertextBlob(parameters) {
1875
+ const expected = ciphertextFieldCount(parameters.layout);
1876
+ const fields = [];
1877
+ for (const slot of parameters.auditCiphertexts) {
1878
+ fields.push(...slot);
1879
+ }
1880
+ for (const note of parameters.outputNoteCiphertexts) {
1881
+ fields.push(...note);
1882
+ }
1883
+ if (fields.length !== expected) {
1884
+ throw new Error(`ciphertext blob expected ${String(expected)} fields, got ${String(fields.length)}`);
1885
+ }
1886
+ return encodeCiphertextBlob(fields);
1887
+ }
1888
+
1889
+ const NOTE_AUDIT_LEN = 12;
1890
+ const STREAM_DOMAIN = 1n;
1891
+ const FR_SIZE = 32;
1892
+ const FR_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
1893
+ const ONE = 1n;
1894
+ const ZERO = 0n;
1895
+ let primitivesPromise;
1896
+ function primitives() {
1897
+ primitivesPromise ?? (primitivesPromise = Promise.all([
1898
+ circomlibjs.buildBabyjub(),
1899
+ circomlibjs.buildMimc7(),
1900
+ circomlibjs.buildPoseidon(),
1901
+ ]).then(([babyJub, mimc7, poseidon]) => ({ babyJub, mimc7, poseidon })));
1902
+ return primitivesPromise;
1903
+ }
1904
+ function normalizeHex(value) {
1905
+ return value.trim().replace(/^0x/i, '').padStart(FR_SIZE * 2, '0');
1906
+ }
1907
+ function bigintFromHex(value) {
1908
+ return BigInt(`0x${normalizeHex(value)}`);
1909
+ }
1910
+ function bigintFromDecimal(value) {
1911
+ const parsed = BigInt(value);
1912
+ return parsed % FR_MODULUS;
1913
+ }
1914
+ function bigintToFrHex(value) {
1915
+ let v = moduleFr(value);
1916
+ const buf = Buffer.alloc(FR_SIZE);
1917
+ for (let index = FR_SIZE - 1; index >= 0; index -= 1) {
1918
+ buf[index] = Number(v & 0xffn);
1919
+ v >>= 8n;
1920
+ }
1921
+ return buf.toString('hex');
1922
+ }
1923
+ function frDecimalToHex(value) {
1924
+ return bigintToFrHex(bigintFromDecimal(value));
1925
+ }
1926
+ function frDecimalFromBigint(value) {
1927
+ const reduced = value >= ZERO ? value % FR_MODULUS : (value % FR_MODULUS) + FR_MODULUS;
1928
+ return reduced.toString(10);
1929
+ }
1930
+ function fieldBigint(field, value) {
1931
+ return BigInt(field.toString(value));
1932
+ }
1933
+ function moduleFr(value) {
1934
+ const reduced = value % FR_MODULUS;
1935
+ return reduced >= ZERO ? reduced : reduced + FR_MODULUS;
1936
+ }
1937
+ function mimcHash(parameters) {
1938
+ return fieldBigint(parameters.mimc7.F, parameters.mimc7.hash(parameters.mimc7.F.e(parameters.message), parameters.mimc7.F.e(parameters.key)));
1939
+ }
1940
+ function poseidonHash(poseidon, inputs) {
1941
+ return fieldBigint(poseidon.F, poseidon(inputs.map((input) => poseidon.F.e(input))));
1942
+ }
1943
+ function deriveEncCipherKey(poseidon, sharedX) {
1944
+ return poseidonHash(poseidon, [DOM_ENC, sharedX]);
1945
+ }
1946
+ function sharedKeyFromDepositor(parameters) {
1947
+ const scalarHex = frDecimalToHex(parameters.ephemeralKeyScalarDecimal);
1948
+ const shared = parameters.ecdhShared(scalarHex, frDecimalToHex(parameters.recipientPublicKey[0]), frDecimalToHex(parameters.recipientPublicKey[1]));
1949
+ return [bigintFromHex(shared.x), bigintFromHex(shared.y)];
1950
+ }
1951
+ function poseidonMacTag(parameters) {
1952
+ return poseidonHash(parameters.poseidon, [
1953
+ parameters.domain,
1954
+ parameters.key,
1955
+ parameters.nonce,
1956
+ ...parameters.message,
1957
+ ]);
1958
+ }
1959
+ function outputNoteTag(parameters) {
1960
+ return poseidonMacTag({
1961
+ poseidon: parameters.poseidon,
1962
+ domain: DOM_OUTPUT_NOTE_TAG,
1963
+ key: parameters.key,
1964
+ nonce: parameters.nonce,
1965
+ message: parameters.plaintext,
1966
+ });
1967
+ }
1968
+ function encryptFields(parameters) {
1969
+ return parameters.plaintext.map((field, index) => {
1970
+ const seed = poseidonHash(parameters.poseidon, [
1971
+ parameters.nonce,
1972
+ STREAM_DOMAIN,
1973
+ BigInt(index),
1974
+ ]);
1975
+ const stream = mimcHash({
1976
+ mimc7: parameters.mimc7,
1977
+ message: seed,
1978
+ key: parameters.key,
1979
+ });
1980
+ return moduleFr(field + stream);
1981
+ });
1982
+ }
1983
+ function plaintextBigints(plaintext) {
1984
+ return [
1985
+ bigintFromDecimal(plaintext.value),
1986
+ bigintFromDecimal(plaintext.assetHi),
1987
+ bigintFromDecimal(plaintext.assetLo),
1988
+ bigintFromDecimal(plaintext.nullifier),
1989
+ bigintFromDecimal(plaintext.secret),
1990
+ bigintFromDecimal(plaintext.applicationId),
1991
+ ];
1992
+ }
1993
+ async function secretFromDepositEphemeralScalarDecimal(ephemeralKeyScalarDecimal) {
1994
+ const { poseidon } = await primitives();
1995
+ const secret = poseidonHash(poseidon, [bigintFromDecimal(ephemeralKeyScalarDecimal)]);
1996
+ return frDecimalFromBigint(secret);
1997
+ }
1998
+ async function encryptOutputNoteForDeposit(parameters) {
1999
+ const { babyJub, mimc7, poseidon } = await primitives();
2000
+ const secret = await secretFromDepositEphemeralScalarDecimal(parameters.deposit.ephemeralKeyScalar);
2001
+ const plaintext = {
2002
+ value: parameters.deposit.value,
2003
+ assetHi: parameters.deposit.asset[0],
2004
+ assetLo: parameters.deposit.asset[1],
2005
+ nullifier: parameters.deposit.nullifier,
2006
+ secret,
2007
+ applicationId: parameters.deposit.applicationId,
2008
+ };
2009
+ const [sharedX, sharedY] = sharedKeyFromDepositor({
2010
+ babyJub,
2011
+ ecdhShared: parameters.ecdhShared,
2012
+ ephemeralKeyScalarDecimal: parameters.deposit.ephemeralKeyScalar,
2013
+ recipientPublicKey: parameters.deposit.recipientPublicKeys,
2014
+ });
2015
+ const key = deriveEncCipherKey(poseidon, sharedX);
2016
+ const nonce = sharedY;
2017
+ const message = plaintextBigints(plaintext);
2018
+ const ciphertext = encryptFields({ mimc7, poseidon, key, nonce, plaintext: message });
2019
+ const tag = outputNoteTag({ poseidon, key, nonce, plaintext: message });
2020
+ return {
2021
+ ciphertext: ciphertext.map((field) => frDecimalFromBigint(field)),
2022
+ tag: frDecimalFromBigint(tag),
2023
+ };
2024
+ }
2025
+ function ownerModeFromEscrowNonce(escrowNonce) {
2026
+ return bigintFromDecimal(escrowNonce) === ZERO ? ZERO : ONE;
2027
+ }
2028
+ function emptyFlagFromValue(value) {
2029
+ return bigintFromDecimal(value) === ZERO ? ONE : ZERO;
2030
+ }
2031
+ function precommitmentFromOpening(parameters) {
2032
+ const ownerKeyHash = poseidonHash(parameters.poseidon, [
2033
+ parameters.ownerPub[0],
2034
+ parameters.ownerPub[1],
2035
+ ]);
2036
+ return poseidonHash(parameters.poseidon, [
2037
+ parameters.secret,
2038
+ parameters.ownerMode,
2039
+ ownerKeyHash,
2040
+ ]);
2041
+ }
2042
+ async function encryptNoteAuditSlot(parameters) {
2043
+ if (parameters.plaintext.length !== NOTE_AUDIT_LEN) {
2044
+ throw new Error(`encryptNoteAuditSlot: expected ${String(NOTE_AUDIT_LEN)} limbs`);
2045
+ }
2046
+ const { babyJub, mimc7, poseidon } = await primitives();
2047
+ const [sharedX, sharedY] = sharedKeyFromDepositor({
2048
+ babyJub,
2049
+ ecdhShared: parameters.ecdhShared,
2050
+ ephemeralKeyScalarDecimal: parameters.auditEphemeralScalar,
2051
+ recipientPublicKey: parameters.auditPublicKey,
2052
+ });
2053
+ const key = deriveEncCipherKey(poseidon, sharedX);
2054
+ const nonce = sharedY;
2055
+ const message = parameters.plaintext.map((field) => bigintFromDecimal(field));
2056
+ const ciphertext = encryptFields({ mimc7, poseidon, key, nonce, plaintext: message });
2057
+ const tag = poseidonMacTag({
2058
+ poseidon,
2059
+ domain: DOM_AUDIT_TAG,
2060
+ key,
2061
+ nonce,
2062
+ message,
2063
+ });
2064
+ return {
2065
+ ciphertext: ciphertext.map((field) => frDecimalFromBigint(field)),
2066
+ tag: frDecimalFromBigint(tag),
2067
+ };
2068
+ }
2069
+ function auditPlaintextForWithdraw(parameters) {
2070
+ const value = parameters.witness.withdrawnValues[parameters.index] ?? '0';
2071
+ const ownerPub = parameters.witness.ownerPubs[parameters.index] ?? ['0', '0'];
2072
+ const escrowNonce = parameters.witness.withdrawnEscrowNonces[parameters.index] ?? '0';
2073
+ const secret = bigintFromDecimal(parameters.witness.withdrawnSecrets[parameters.index] ?? '0');
2074
+ const ownerMode = ownerModeFromEscrowNonce(escrowNonce);
2075
+ const precommitment = precommitmentFromOpening({
2076
+ poseidon: parameters.poseidon,
2077
+ secret,
2078
+ ownerMode,
2079
+ ownerPub: [bigintFromDecimal(ownerPub[0]), bigintFromDecimal(ownerPub[1])],
2080
+ });
2081
+ const asset = parameters.witness.withdrawnAssets[parameters.index] ?? ['0', '0'];
2082
+ const stellar = parameters.witness.inputRecipientStellar[parameters.index] ?? ['0', '0'];
2083
+ return [
2084
+ frDecimalFromBigint(emptyFlagFromValue(value)),
2085
+ value,
2086
+ asset[0],
2087
+ asset[1],
2088
+ frDecimalFromBigint(precommitment),
2089
+ parameters.witness.withdrawnNullifiers[parameters.index] ?? '0',
2090
+ ownerPub[0],
2091
+ ownerPub[1],
2092
+ parameters.witness.inputApplicationIds[parameters.index] ?? '0',
2093
+ stellar[0],
2094
+ stellar[1],
2095
+ escrowNonce,
2096
+ ];
2097
+ }
2098
+ async function auditPlaintextForDeposit(parameters) {
2099
+ const value = parameters.witness.depositedValues[parameters.index] ?? '0';
2100
+ const ownerPub = parameters.witness.depositedRecipientPublicKeys[parameters.index] ?? ['0', '0'];
2101
+ const escrowNonce = parameters.witness.depositedEscrowNonces[parameters.index] ?? '0';
2102
+ const secret = poseidonHash(parameters.poseidon, [
2103
+ bigintFromDecimal(parameters.witness.depositedEphemeralKeyScalars[parameters.index] ?? '0'),
2104
+ ]);
2105
+ const ownerMode = ownerModeFromEscrowNonce(escrowNonce);
2106
+ const precommitment = precommitmentFromOpening({
2107
+ poseidon: parameters.poseidon,
2108
+ secret,
2109
+ ownerMode,
2110
+ ownerPub: [bigintFromDecimal(ownerPub[0]), bigintFromDecimal(ownerPub[1])],
2111
+ });
2112
+ const asset = parameters.witness.depositedAssets[parameters.index] ?? ['0', '0'];
2113
+ const stellar = parameters.witness.outputRecipientStellar[parameters.index] ?? ['0', '0'];
2114
+ return [
2115
+ frDecimalFromBigint(emptyFlagFromValue(value)),
2116
+ value,
2117
+ asset[0],
2118
+ asset[1],
2119
+ frDecimalFromBigint(precommitment),
2120
+ parameters.witness.depositedNullifiers[parameters.index] ?? '0',
2121
+ ownerPub[0],
2122
+ ownerPub[1],
2123
+ parameters.witness.outputApplicationIds[parameters.index] ?? '0',
2124
+ stellar[0],
2125
+ stellar[1],
2126
+ escrowNonce,
2127
+ ];
2128
+ }
2129
+ async function encryptTransactionCiphertextBlob(parameters) {
2130
+ const { poseidon } = await primitives();
2131
+ const nIns = parameters.witness.withdrawnValues.length;
2132
+ const nOuts = parameters.witness.depositedValues.length;
2133
+ const nAuditSlots = parameters.layout.nAuditSlots;
2134
+ const auditCiphertexts = [];
2135
+ for (let slot = 0; slot < nAuditSlots; slot += 1) {
2136
+ const plaintext = slot < nIns
2137
+ ? auditPlaintextForWithdraw({
2138
+ poseidon,
2139
+ witness: parameters.witness,
2140
+ index: slot,
2141
+ })
2142
+ : await auditPlaintextForDeposit({
2143
+ poseidon,
2144
+ witness: parameters.witness,
2145
+ index: slot - nIns,
2146
+ });
2147
+ const encrypted = await encryptNoteAuditSlot({
2148
+ auditPublicKey: parameters.witness.noteAuditPublicKey,
2149
+ auditEphemeralScalar: parameters.witness.auditEphemeralScalar,
2150
+ plaintext,
2151
+ ecdhShared: parameters.ecdhShared,
2152
+ });
2153
+ auditCiphertexts.push(encrypted.ciphertext);
2154
+ }
2155
+ const outputNoteCiphertexts = [];
2156
+ for (let index = 0; index < nOuts; index += 1) {
2157
+ const deposit = {
2158
+ value: parameters.witness.depositedValues[index] ?? '0',
2159
+ nullifier: parameters.witness.depositedNullifiers[index] ?? '0',
2160
+ ephemeralKeyScalar: parameters.witness.depositedEphemeralKeyScalars[index] ?? '0',
2161
+ asset: parameters.witness.depositedAssets[index] ?? ['0', '0'],
2162
+ applicationId: parameters.witness.outputApplicationIds[index] ?? '0',
2163
+ recipientPublicKeys: parameters.witness.depositedRecipientPublicKeys[index] ?? ['0', '0'],
2164
+ };
2165
+ const encrypted = await encryptOutputNoteForDeposit({
2166
+ deposit,
2167
+ ecdhShared: parameters.ecdhShared,
2168
+ });
2169
+ outputNoteCiphertexts.push([...encrypted.ciphertext]);
2170
+ }
2171
+ const blob = encodeTransactionCiphertextBlob({
2172
+ layout: parameters.layout,
2173
+ auditCiphertexts,
2174
+ outputNoteCiphertexts,
2175
+ });
2176
+ return {
2177
+ ciphertextHex: blob.toString('hex'),
2178
+ auditCiphertexts,
2179
+ outputNoteCiphertexts,
2180
+ };
1912
2181
  }
1913
2182
 
1914
- /** Stroops amount as WASM `u64` (`bigint`). */
1915
- function wasmU64Stroops(amount) {
1916
- const b = typeof amount === 'bigint' ? amount : BigInt(amount);
1917
- if (b < 0n || b > 0xffffffffffffffffn) {
1918
- throw new RangeError('amount must be a non-negative u64 (stroops)');
2183
+ /** Stroops amount as a decimal field string. Reject JS `number` so 18-decimal values cannot silently narrow. */
2184
+ function coinValueDecimal(amount) {
2185
+ if (typeof amount === "number") {
2186
+ throw new TypeError("coin value must be bigint or decimal string, not number");
2187
+ }
2188
+ if (typeof amount === "bigint") {
2189
+ if (amount < 0n) {
2190
+ throw new RangeError("amount must be a non-negative integer");
2191
+ }
2192
+ return amount.toString(10);
2193
+ }
2194
+ if (typeof amount !== "string") {
2195
+ throw new TypeError("coin value must be bigint or decimal string, not number");
1919
2196
  }
1920
- return b;
2197
+ const trimmed = amount.trim();
2198
+ if (!/^[0-9]+$/.test(trimmed)) {
2199
+ throw new TypeError("coin value decimal string must be a non-negative integer");
2200
+ }
2201
+ return trimmed;
2202
+ }
2203
+ function generatedCoinFromWasm(value) {
2204
+ const raw = value;
2205
+ const precommitementHex = raw.precommitementHex ?? raw.precommitement_hex;
2206
+ if (!precommitementHex) {
2207
+ throw new Error("generated coin missing precommitement hex");
2208
+ }
2209
+ return {
2210
+ coin: raw.coin,
2211
+ commitment_hex: raw.commitment_hex,
2212
+ precommitement_hex: precommitementHex,
2213
+ precommitementHex,
2214
+ };
1921
2215
  }
1922
2216
  class PrivacyPoolSDK {
1923
- constructor(wasm, options) {
2217
+ constructor(wasm, options, resolved) {
1924
2218
  this.wasm = wasm;
1925
2219
  this.options = options;
2220
+ this.zkConfigNonce = resolved.nonce;
2221
+ this.circuitConfig = resolved.config;
2222
+ this.layout = layoutFromCircuitConfig(resolved.config);
2223
+ }
2224
+ getZkConfigNonce() {
2225
+ return this.zkConfigNonce;
2226
+ }
2227
+ getLayout() {
2228
+ return this.layout;
1926
2229
  }
1927
2230
  /**
1928
2231
  * Initialize the SDK by loading WASM modules.
1929
2232
  *
1930
- * In Node.js, assets are loaded from the filesystem automatically.
1931
- * In browser, pass pre-loaded buffers via options:
2233
+ * In Node.js, proving artifacts load from `zkArtifactBaseUrl` or
2234
+ * `ZK_ARTIFACT_BASE_URL`, defaulting to this package's GitHub release.
2235
+ * In browser, pass pre-loaded WASM via options:
1932
2236
  * ```ts
1933
2237
  * const sdk = await PrivacyPoolSDK.init({
1934
2238
  * wasmBinary: await fetch('/pkg/client_sdk_wasm_bg.wasm').then(r => r.arrayBuffer()),
1935
- * circuitWasm: await fetch('/assets/main.wasm').then(r => r.arrayBuffer()),
1936
- * zkey: await fetch('/assets/main_final.zkey').then(r => r.arrayBuffer()),
2239
+ * zkArtifactBaseUrl: 'https://github.com/Polynom-Labs/stellar-privacy-layer-contracts/releases/download/v0.8.0',
1937
2240
  * });
1938
2241
  * ```
1939
2242
  */
1940
2243
  static async init(options) {
1941
2244
  const opts = options ?? {};
1942
2245
  const wasm = await loadWasm(opts.wasmBinary);
1943
- return new PrivacyPoolSDK(wasm, opts);
2246
+ const resolved = resolveZkCircuitConfig(opts.zkCircuits, opts.zkConfigNonce);
2247
+ const sdk = new PrivacyPoolSDK(wasm, opts, resolved);
2248
+ sdk.circuitArtifacts = await resolveCircuitArtifacts(resolved.config, opts.zkArtifactBaseUrl);
2249
+ return sdk;
1944
2250
  }
1945
2251
  /**
1946
2252
  * Uniform 32-byte scalar as lowercase hex (Web Crypto). Same in browser and Node 19+.
@@ -1949,39 +2255,40 @@ class PrivacyPoolSDK {
1949
2255
  return generateRandomScalarHex32();
1950
2256
  }
1951
2257
  /**
1952
- * Text to sign with a Stellar wallet for stealth derivation (UTF-8). No WASM required.
2258
+ * Text to sign with a Stellar wallet for spend-key derivation (UTF-8). No WASM required.
2259
+ * The message binds network, pool, registry, and schema version (H6).
1953
2260
  */
1954
- static buildStealthAddressSignMessage(address, nonce = DEFAULT_STEALTH_SIGN_NONCE) {
1955
- return buildStealthAddressSignMessage(address, nonce);
2261
+ static buildStealthAddressSignMessage(address, domain, nonce = DEFAULT_STEALTH_SIGN_NONCE) {
2262
+ return buildStealthAddressSignMessage(address, domain, nonce);
1956
2263
  }
1957
2264
  /**
1958
- * Generate a new coin with random nullifier, secret, and random shared-secret field elements (dev / self-contained tests).
1959
- * @param amount Stroops encoded as `bigint` or integer `number` (WASM `u64`).
2265
+ * Generate a new coin with random nullifier, secret, and random owner-pub field elements (dev / self-contained tests).
2266
+ * @param amount Decimal field element as `bigint` or decimal `string` (never JS `number`).
1960
2267
  * @param assetHiDecimal / assetLoDecimal Decimal Fr strings for Stellar asset contract id (two limbs).
1961
2268
  */
1962
- generateCoin(amount, assetHiDecimal, assetLoDecimal, applicationIdDecimal = '0') {
1963
- return this.wasm.generateCoin(wasmU64Stroops(amount), assetHiDecimal, assetLoDecimal, applicationIdDecimal);
2269
+ generateCoin(amount, assetHiDecimal, assetLoDecimal, applicationIdDecimal = "0") {
2270
+ return generatedCoinFromWasm(this.wasm.generateCoin(coinValueDecimal(amount), assetHiDecimal, assetLoDecimal, applicationIdDecimal));
1964
2271
  }
1965
2272
  /**
1966
- * Generate a coin with the same commitment shape as on-chain deposit: pass `ecdhSharedKey` output (hex x, y).
1967
- * @param amount Stroops (`bigint` | `number`).
2273
+ * Generate a coin with the same commitment shape as on-chain deposit: pass owner public key (hex x, y).
2274
+ * @param amount Decimal field element (`bigint` | `string`).
1968
2275
  */
1969
- generateCoinWithSharedSecret(shared, amount, assetHiDecimal, assetLoDecimal, applicationIdDecimal = '0') {
1970
- return this.wasm.generateCoinWithSharedSecretHex(shared.x, shared.y, wasmU64Stroops(amount), assetHiDecimal, assetLoDecimal, applicationIdDecimal);
2276
+ generateCoinWithOwnerPub(ownerPub, amount, assetHiDecimal, assetLoDecimal, applicationIdDecimal = "0") {
2277
+ return generatedCoinFromWasm(this.wasm.generateCoinWithOwnerPubHex(ownerPub.x, ownerPub.y, coinValueDecimal(amount), assetHiDecimal, assetLoDecimal, applicationIdDecimal));
1971
2278
  }
1972
2279
  /**
1973
2280
  * Coin for a depositor `ephemeralKeyScalar` (32-byte hex): `coin.secret = Poseidon255(1)(scalar)` as in `deposit.circom`.
1974
- * @param amount Stroops (`bigint` | `number`).
2281
+ * @param amount Decimal field element (`bigint` | `string`).
1975
2282
  */
1976
- generateCoinFromDepositEphemeralScalarHex(scalarHex, amount, assetHiDecimal, assetLoDecimal, applicationIdDecimal = '0') {
1977
- return this.wasm.generateCoinFromDepositEphemeralScalarHex(scalarHex, wasmU64Stroops(amount), assetHiDecimal, assetLoDecimal, applicationIdDecimal);
2283
+ generateCoinFromDepositEphemeralScalarHex(scalarHex, amount, assetHiDecimal, assetLoDecimal, applicationIdDecimal = "0") {
2284
+ return generatedCoinFromWasm(this.wasm.generateCoinFromDepositEphemeralScalarHex(scalarHex, coinValueDecimal(amount), assetHiDecimal, assetLoDecimal, applicationIdDecimal));
1978
2285
  }
1979
2286
  /**
1980
- * Aligned deposit coin: `secret = Poseidon₁(scalar)` and ECDH shared key from hex coords (e.g. `ecdhSharedKey(scalar, recipient_x, recipient_y)`).
1981
- * @param amount Stroops (`bigint` | `number`).
2287
+ * Aligned deposit coin: `secret = Poseidon₁(scalar)` and owner pub from recipient hex coords.
2288
+ * @param amount Decimal field element (`bigint` | `string`).
1982
2289
  */
1983
- generateCoinForDepositWithSharedHex(scalarHex, sharedXHex, sharedYHex, amount, assetHiDecimal, assetLoDecimal, applicationIdDecimal = '0') {
1984
- return this.wasm.generateCoinForDepositWithSharedHex(scalarHex, sharedXHex, sharedYHex, wasmU64Stroops(amount), assetHiDecimal, assetLoDecimal, applicationIdDecimal);
2290
+ generateCoinForDepositWithOwnerPubHex(scalarHex, ownerXHex, ownerYHex, amount, assetHiDecimal, assetLoDecimal, applicationIdDecimal = "0") {
2291
+ return generatedCoinFromWasm(this.wasm.generateCoinForDepositWithOwnerPubHex(scalarHex, ownerXHex, ownerYHex, coinValueDecimal(amount), assetHiDecimal, assetLoDecimal, applicationIdDecimal));
1985
2292
  }
1986
2293
  /**
1987
2294
  * Merkle root, path, and coin fields for the first withdraw leg (Rust LeanIMT + Poseidon).
@@ -2000,20 +2307,18 @@ class PrivacyPoolSDK {
2000
2307
  */
2001
2308
  async proveWithdrawal(coin, state, params) {
2002
2309
  const witness = this.buildWithdrawMerkleWitness(coin, state);
2003
- const w0 = withdrawObjectFromMerkleWitness(witness, {
2004
- x: params.ephemeralXHex,
2005
- y: params.ephemeralYHex,
2006
- }, params.applicationId ?? coin.application_id ?? '0');
2310
+ const ownerPubHex = this.ecdhEphemeralPublicKeyFromScalarHex(BigInt(params.privKeyScalar).toString(16).padStart(64, "0"));
2311
+ const w0 = withdrawObjectFromMerkleWitness(witness, ownerPubHex, params.applicationId ?? coin.application_id ?? "0", params.privKeyScalar);
2007
2312
  const fullV = BigInt(coin.value);
2008
2313
  let publicWithdrawals;
2009
2314
  let deposits;
2010
2315
  if (params.publicWithdrawStroops !== undefined) {
2011
2316
  const W = params.publicWithdrawStroops;
2012
2317
  if (params.changeRecipientStealthAddress === undefined) {
2013
- throw new Error('proveWithdrawal: changeRecipientStealthAddress is required when publicWithdrawStroops is set');
2318
+ throw new Error("proveWithdrawal: changeRecipientStealthAddress is required when publicWithdrawStroops is set");
2014
2319
  }
2015
2320
  if (W <= 0n || W >= fullV) {
2016
- throw new Error('proveWithdrawal: publicWithdrawStroops must be strictly between 0 and coin.value');
2321
+ throw new Error("proveWithdrawal: publicWithdrawStroops must be strictly between 0 and coin.value");
2017
2322
  }
2018
2323
  const change = fullV - W;
2019
2324
  publicWithdrawals = [W.toString()];
@@ -2022,29 +2327,29 @@ class PrivacyPoolSDK {
2022
2327
  nullifier: randomFrDecimal(),
2023
2328
  ephemeralKeyScalar: randomFrDecimal253(),
2024
2329
  asset: [coin.asset_hi, coin.asset_lo],
2025
- applicationId: params.applicationId ?? '0',
2330
+ applicationId: params.applicationId ?? "0",
2026
2331
  recipientPublicKeys: recipientPublicKeysDecimalFromStealthAddress(params.changeRecipientStealthAddress),
2027
2332
  };
2028
- deposits = [changeDeposit, 'dummy'];
2333
+ deposits = [changeDeposit];
2029
2334
  }
2030
2335
  else {
2031
2336
  publicWithdrawals = [coin.value];
2032
- deposits = ['dummy', 'dummy'];
2337
+ deposits = [];
2033
2338
  }
2034
2339
  // On-chain `transact` sends `publicWithdrawals` / `publicWithdrawnAssets` to the withdraw account.
2035
2340
  const publicLegs = {
2036
2341
  publicWithdrawnAssets: [[coin.asset_hi, coin.asset_lo]],
2037
- publicDepositedAssets: [['0', '0']],
2038
- publicDeposits: ['0'],
2342
+ publicDepositedAssets: [["0", "0"]],
2343
+ publicDeposits: ["0"],
2039
2344
  publicWithdrawals,
2040
2345
  };
2041
- return this.proveTransaction({
2346
+ return this.proveTransactionSlots({
2042
2347
  stateRoot: witness.stateRoot,
2043
2348
  withdrawAddressHi: params.withdrawAddressHi,
2044
2349
  withdrawAddressLo: params.withdrawAddressLo,
2045
2350
  privKeyScalar: params.privKeyScalar,
2046
- }, publicLegs, [w0, 'dummy'], deposits, params.audit
2047
- ?? resolveTransactionAuditParams(params.applicationId ?? coin.application_id ?? DEFAULT_APPLICATION_ID, DEMO_AUDIT_PUBLIC_KEY));
2351
+ }, publicLegs, [w0], deposits, params.audit ??
2352
+ resolveTransactionAuditParams(params.applicationId ?? coin.application_id ?? DEFAULT_APPLICATION_ID, DEMO_AUDIT_PUBLIC_KEY, this.layout.nAuditSlots));
2048
2353
  }
2049
2354
  /**
2050
2355
  * Convert a snarkjs proof JSON to hex bytes for Soroban.
@@ -2069,39 +2374,50 @@ class PrivacyPoolSDK {
2069
2374
  return this.wasm.ecdhSharedKey(scalarHex, recipientPubXHex, recipientPubYHex);
2070
2375
  }
2071
2376
  /**
2072
- * `Transaction(20,2,2)` proof from high-level legs: maps to witness input (incl. `"dummy"` ECDH via WASM), then Groth16 → Soroban hex.
2073
- *
2074
- * @param publicParams Public inputs: `stateRoot`, `withdrawAddressHi` / `withdrawAddressLo`, `privKeyScalar` (decimal field strings).
2075
- * @param publicLegs Public token legs (`publicWithdrawnAssets`, `publicDepositedAssets`, `publicDeposits`, `publicWithdrawals`).
2076
- * @param withdraws Exactly two withdraw slots (`WithdrawObject` or `"dummy"`).
2077
- * @param deposits Exactly two deposit slots (`DepositObject` or `"dummy"`).
2377
+ * `Transaction(20,nIns,nOuts)` proof from high-level legs: maps to witness input (incl. `"dummy"` ECDH via WASM), then Groth16 → Soroban hex.
2378
+ * Slots are padded to the selected ZK config layout (bundled 2×2 or 6×6).
2078
2379
  */
2079
2380
  async proveTransaction(publicParams, publicLegs, withdraws, deposits, audit) {
2381
+ return this.proveTransactionSlots(publicParams, publicLegs, withdraws, deposits, audit);
2382
+ }
2383
+ async proveTransactionSlots(publicParams, publicLegs, withdraws, deposits, audit) {
2080
2384
  const wasmEcdh = {
2081
2385
  ecdhEphemeralPublicKeyFromScalarHex: (h) => this.wasm.ecdhEphemeralPublicKeyFromScalarHex(h),
2082
2386
  };
2083
- const witnessInput = buildTransactionWitnessInput(publicParams, publicLegs, withdraws, deposits, audit, wasmEcdh);
2084
- const wtns = await generateWitness(witnessInput, this.options.circuitWasm);
2085
- const { proof, publicSignals } = await generateProof(wtns, this.options.zkey);
2387
+ const { nIns, nOuts, publicNInputs, publicNOutputs } = this.layout;
2388
+ const witnessInput = buildTransactionWitnessInput(publicParams, publicLegs, withdraws, deposits, audit, wasmEcdh, { nIns, nOuts, publicNInputs, publicNOutputs });
2389
+ const artifacts = this.circuitArtifacts;
2390
+ if (!artifacts) {
2391
+ throw new Error("PrivacyPoolSDK circuit artifacts are not loaded");
2392
+ }
2393
+ const proved = this.wasm.proveGroth16(artifacts.graph, artifacts.provingKey, artifacts.r1cs, JSON.stringify(witnessInput));
2394
+ const encrypted = await encryptTransactionCiphertextBlob({
2395
+ witness: witnessInput,
2396
+ layout: this.layout,
2397
+ ecdhShared: (priv, pubX, pubY) => this.wasm.ecdhSharedKey(priv, pubX, pubY),
2398
+ });
2086
2399
  return {
2087
- proof_hex: this.proofToHex(proof),
2088
- public_hex: this.publicToHex(publicSignals),
2400
+ proof_hex: proved.proof_hex,
2401
+ public_hex: proved.public_hex,
2402
+ ciphertext_hex: encrypted.ciphertextHex,
2403
+ output_note_ephemeral_scalars: witnessInput.depositedEphemeralKeyScalars,
2089
2404
  };
2090
2405
  }
2091
2406
  /**
2092
- * Calculate nullifier hash: Poseidon(nullifier)
2407
+ * Calculate owner-bound nullifier hash: Poseidon(DOM_NULLIFIER, nullifier, privKeyScalar)
2093
2408
  * @param nullifier Nullifier decimal string from coin data
2409
+ * @param privKeyScalar Spend scalar decimal string
2094
2410
  * @returns Hex string (0x...) of the hash bytes
2095
2411
  */
2096
- calculateNullifierHash(nullifier) {
2097
- return this.wasm.calculateNullifierHash(nullifier);
2412
+ calculateNullifierHash(nullifier, privKeyScalar) {
2413
+ return this.wasm.calculateNullifierHash(nullifier, privKeyScalar);
2098
2414
  }
2099
2415
  /**
2100
2416
  * Ed25519 signature from signing {@link buildStealthAddressSignMessage}: **128 hex chars** (optional `0x`)
2101
- * or **base64** (64 raw bytes after decode). `SHA-256(signature bytes)` → scalar → ECDH → `stpl1` Bech32.
2417
+ * or **base64** (64 raw bytes after decode). Domain-separated SHA-256 → scalar → ECDH → `stpl1` Bech32.
2102
2418
  */
2103
- async generateStealthAddressFromStellarSignature(signature) {
2104
- return stealthAddressFromStellarSignature((h) => this.wasm.ecdhEphemeralPublicKeyFromScalarHex(h), encodeStealthAddress, signature);
2419
+ async generateStealthAddressFromStellarSignature(signature, domain) {
2420
+ return stealthAddressFromStellarSignature((h) => this.wasm.ecdhEphemeralPublicKeyFromScalarHex(h), encodeStealthAddress, signature, domain);
2105
2421
  }
2106
2422
  encodeDecodedEphemeralKey(decoded) {
2107
2423
  return encodeDecodedEphemeralKey(decoded);
@@ -2123,38 +2439,6 @@ class PrivacyPoolSDK {
2123
2439
  }
2124
2440
  }
2125
2441
 
2126
- function onboardingContractValue(onboarding) {
2127
- return {
2128
- owner: onboarding.owner,
2129
- temp_public_key_x: onboarding.temp_public_key_x,
2130
- temp_public_key_y: onboarding.temp_public_key_y,
2131
- encrypted_private_key: onboarding.encrypted_private_key,
2132
- notes: {
2133
- notes: onboarding.notes.map((note) => ({
2134
- value: note.value,
2135
- asset_hi: note.asset_hi,
2136
- asset_lo: note.asset_lo,
2137
- nullifier: note.nullifier,
2138
- secret: note.secret,
2139
- deposited_ephemeral_scalar: note.deposited_ephemeral_scalar,
2140
- })),
2141
- },
2142
- private_address_registration: onboarding.private_address_registration,
2143
- };
2144
- }
2145
- function onboardingToScVal(onboarding) {
2146
- return stellarSdk.nativeToScVal(onboardingContractValue(onboarding));
2147
- }
2148
- function onboardingOptionToScVal(onboarding) {
2149
- if (onboarding === null || onboarding === undefined) {
2150
- return stellarSdk.xdr.ScVal.scvVoid();
2151
- }
2152
- return onboardingToScVal(onboarding);
2153
- }
2154
- function addressToScVal(address) {
2155
- return stellarSdk.nativeToScVal(address, { type: 'address' });
2156
- }
2157
-
2158
2442
  function signatureBase64ToBytes(signature) {
2159
2443
  const value = signature.trim();
2160
2444
  if (/^(0x)?[0-9a-fA-F]{128}$/.test(value)) {
@@ -2163,6 +2447,9 @@ function signatureBase64ToBytes(signature) {
2163
2447
  return Buffer.from(value, 'base64');
2164
2448
  }
2165
2449
 
2450
+ function addressToScVal(address) {
2451
+ return stellarSdk.nativeToScVal(address, { type: 'address' });
2452
+ }
2166
2453
  function passageIdHexToScVal(passageIdHex) {
2167
2454
  const bytes = Buffer.from(passageIdHex.replace(/^0x/, ''), 'hex');
2168
2455
  return stellarSdk.xdr.ScVal.scvBytes(bytes);
@@ -2179,7 +2466,6 @@ function helperSubmitWithPassageOperation(helperContractId, params, passageId, e
2179
2466
  stellarSdk.nativeToScVal(params.nonce, { type: 'u64' }),
2180
2467
  stellarSdk.xdr.ScVal.scvBytes(params.proofBytes),
2181
2468
  stellarSdk.xdr.ScVal.scvBytes(params.publicSignalsBytes),
2182
- onboardingOptionToScVal(params.onboarding),
2183
2469
  passageIdHexToScVal(passageId),
2184
2470
  stellarSdk.xdr.ScVal.scvU32(expiresAtLedger),
2185
2471
  signatureToScVal(signatureBase64ToBytes(signature)),
@@ -2309,6 +2595,20 @@ const COIN_VALUE_STROOPS = 1000000000;
2309
2595
  function resolveApplicationIdFromCli(parsed) {
2310
2596
  return parsed['application-id'] ?? process.env.APPLICATION_ID ?? DEFAULT_APPLICATION_ID;
2311
2597
  }
2598
+ function resolveZkInit(parsed) {
2599
+ const explicit = parsed['profile'] ?? process.env.ZK_PROFILE;
2600
+ if (explicit && (explicit === '6x6' || explicit === 'six' || explicit === '6')) {
2601
+ return { zkConfigNonce: SIX_BY_SIX_BINDING_ZK_NONCE };
2602
+ }
2603
+ const envNonce = (process.env.ZK_CONFIG_NONCE ?? '').trim();
2604
+ if (envNonce === '6' || envNonce === '7') {
2605
+ return { zkConfigNonce: SIX_BY_SIX_BINDING_ZK_NONCE };
2606
+ }
2607
+ if (envNonce === '3' || envNonce === '2' || envNonce === '') {
2608
+ return { zkConfigNonce: BINDING_ZK_NONCE };
2609
+ }
2610
+ return { zkConfigNonce: BINDING_ZK_NONCE };
2611
+ }
2312
2612
  async function main() {
2313
2613
  const args = process.argv.slice(2);
2314
2614
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
@@ -2337,6 +2637,9 @@ async function main() {
2337
2637
  else if (command === 'priv-scalar-from-signature') {
2338
2638
  await handlePrivScalarFromSignature(args.slice(1));
2339
2639
  }
2640
+ else if (command === 'stealth-pubkey-hex') {
2641
+ handleStealthPubkeyHex(args.slice(1));
2642
+ }
2340
2643
  else if (command === 'kyt-submit-approved') {
2341
2644
  await handleKytSubmitApproved(args.slice(1));
2342
2645
  }
@@ -2364,13 +2667,25 @@ Commands:
2364
2667
 
2365
2668
  stealth-sign-message Print message to sign (Stellar wallet / stellar CLI)
2366
2669
  --address <G...> Stellar account address (required)
2670
+ --network-passphrase <string> Network passphrase bound into the spend scalar
2671
+ --pool <C...> Pool contract id bound into the spend scalar
2672
+ --registry <C...> Registry contract id bound into the spend scalar
2673
+ --schema-version <u32> Note schema version (default: 1)
2367
2674
  --nonce <string> Nonce label (default: "main address")
2368
2675
 
2369
2676
  stealth-from-signature Derive stpl1 stealth address from Ed25519 signature
2370
2677
  --signature <value> 128 hex chars (optional 0x) or standard base64 (64 bytes)
2371
2678
  --signature-file <path> Read signature from file (whitespace trimmed)
2679
+ --network-passphrase <string> Spend-scalar domain (same as stealth-sign-message)
2680
+ --pool <C...>
2681
+ --registry <C...>
2682
+ --schema-version <u32>
2372
2683
 
2373
- priv-scalar-from-signature Print privKeyScalar (decimal Fr) from Stellar Ed25519 signature (SHA-256(sig) mod r)
2684
+ priv-scalar-from-signature Print privKeyScalar (decimal Fr) from domain-separated spend digest
2685
+
2686
+ stealth-pubkey-hex Decode a stpl1 stealth address into its BabyJubJub public key
2687
+ --stealth <stpl1...> Stealth address to decode
2688
+ Prints x hex on line 1, y hex on line 2 (registry public_key_x/public_key_y)
2374
2689
 
2375
2690
  kyt-submit-approved Submit helper.register_passage + pool.transact with signed non-root auth
2376
2691
  --source-secret <S...> Secret seed for the depositing/withdrawing Stellar account (or SOURCE_SECRET env)
@@ -2397,10 +2712,12 @@ Commands:
2397
2712
  --public-withdraw-stroops <u64> Optional: amount to withdraw publicly (must be < coin.value). Rest stays in pool as change.
2398
2713
  --change-stealth <stpl1...> Required with --public-withdraw-stroops: stealth for the change note (same as deposit)
2399
2714
  --application-id <dec> Application id (decimal Fr); default: APPLICATION_ID env or 101
2715
+ --profile <2x2|6x6> Circuit artifact profile (default: ZK_PROFILE / ZK_CONFIG_NONCE=7 → 6x6)
2400
2716
  --output-proof <file> Write proof hex to file
2401
2717
  --output-public <file> Write public signals hex to file
2718
+ --output-ciphertext <file> Write userspace MiMC ciphertext blob hex to file
2402
2719
 
2403
- deposit-proof Groth16 proof for deposit-only transact (2 dummy withdraws, 1 deposit + dummy)
2720
+ deposit-proof Groth16 proof for deposit-only transact (dummy withdraws, 1 deposit + dummy outs)
2404
2721
  --state-root <dec> Current Merkle root (decimal Fr), must match pool on submit
2405
2722
  --stealth <stpl1...> Recipient stealth address (decodes to recipient public key)
2406
2723
  --token <C...> Asset contract id (or set TOKEN_ADDRESS)
@@ -2408,8 +2725,10 @@ Commands:
2408
2725
  --ephemeral-scalar-hex <hex> Same 32-byte hex as deposit / generate --scalar
2409
2726
  --value <dec> Deposit amount (stroops as decimal Fr); optional if --coin (must match coin)
2410
2727
  --application-id <dec> Application id (decimal Fr); default: APPLICATION_ID env or 101
2728
+ --profile <2x2|6x6> Circuit artifact profile (default: ZK_PROFILE / ZK_CONFIG_NONCE=7 → 6x6)
2411
2729
  --output-proof <file> Write proof hex to file
2412
2730
  --output-public <file> Write public signals hex to file
2731
+ --output-ciphertext <file> Write userspace MiMC ciphertext blob hex to file
2413
2732
 
2414
2733
  Output (withdraw, deposit-proof):
2415
2734
  Prints proof_hex on first line and public_hex on second line to stdout.
@@ -2478,6 +2797,24 @@ function parseArgs(args) {
2478
2797
  }
2479
2798
  return parsed;
2480
2799
  }
2800
+ function requireSpendScalarDomain(parsed) {
2801
+ const networkPassphrase = parsed['network-passphrase'] ?? networkPassphraseFromArgs(parsed);
2802
+ const poolContract = requireParsedArg(parsed, 'pool');
2803
+ const registryContract = requireParsedArg(parsed, 'registry');
2804
+ const schemaRaw = parsed['schema-version'];
2805
+ const domain = {
2806
+ networkPassphrase,
2807
+ poolContract,
2808
+ registryContract,
2809
+ };
2810
+ if (schemaRaw !== undefined) {
2811
+ domain.schemaVersion = Number(schemaRaw);
2812
+ }
2813
+ else {
2814
+ domain.schemaVersion = OWNER_BOUND_NOTE_SCHEMA_VERSION;
2815
+ }
2816
+ return domain;
2817
+ }
2481
2818
  function handleStealthSignMessage(args) {
2482
2819
  const parsed = parseArgs(args);
2483
2820
  const address = parsed['address'];
@@ -2486,7 +2823,7 @@ function handleStealthSignMessage(args) {
2486
2823
  process.exit(1);
2487
2824
  }
2488
2825
  const nonce = parsed['nonce'] ?? DEFAULT_STEALTH_SIGN_NONCE;
2489
- console.log(buildStealthAddressSignMessage(address, nonce));
2826
+ console.log(buildStealthAddressSignMessage(address, requireSpendScalarDomain(parsed), nonce));
2490
2827
  }
2491
2828
  async function handleStealthFromSignature(args) {
2492
2829
  const parsed = parseArgs(args);
@@ -2499,9 +2836,16 @@ async function handleStealthFromSignature(args) {
2499
2836
  process.exit(1);
2500
2837
  }
2501
2838
  const sdk = await PrivacyPoolSDK.init();
2502
- const stealth = await sdk.generateStealthAddressFromStellarSignature(sig);
2839
+ const stealth = await sdk.generateStealthAddressFromStellarSignature(sig, requireSpendScalarDomain(parsed));
2503
2840
  console.log(stealth);
2504
2841
  }
2842
+ function handleStealthPubkeyHex(args) {
2843
+ const parsed = parseArgs(args);
2844
+ const stealth = requireParsedArg(parsed, 'stealth');
2845
+ const { x, y } = decodeStealthAddress(stealth);
2846
+ console.log(x);
2847
+ console.log(y);
2848
+ }
2505
2849
  async function handlePrivScalarFromSignature(args) {
2506
2850
  const parsed = parseArgs(args);
2507
2851
  let sig = parsed['signature'];
@@ -2512,7 +2856,7 @@ async function handlePrivScalarFromSignature(args) {
2512
2856
  console.error('Error: --signature <hex> or --signature-file <path> is required');
2513
2857
  process.exit(1);
2514
2858
  }
2515
- const dec = await privKeyScalarDecimalFromStellarSignature(sig.trim());
2859
+ const dec = await privKeyScalarDecimalFromStellarSignature(sig.trim(), requireSpendScalarDomain(parsed));
2516
2860
  console.log(dec);
2517
2861
  }
2518
2862
  function handleRandomScalar() {
@@ -2541,33 +2885,29 @@ async function handleKytSubmitApproved(args) {
2541
2885
  });
2542
2886
  console.log(txHash);
2543
2887
  }
2544
- function parseStroopsU64(label, raw, defaultStroops) {
2545
- if (raw === undefined) {
2546
- return defaultStroops;
2547
- }
2888
+ function parseStroopsU64(label, raw, _min) {
2548
2889
  if (!/^\d+$/.test(raw)) {
2549
- console.error(`Error: ${label} must be a non-negative decimal integer (stroops u64)`);
2890
+ console.error(`Error: ${label} must be a non-negative decimal integer`);
2550
2891
  process.exit(1);
2551
2892
  }
2552
- try {
2553
- const b = BigInt(raw);
2554
- if (b < 0n || b > 0xffffffffffffffffn) {
2555
- console.error(`Error: ${label} out of u64 range`);
2556
- process.exit(1);
2557
- }
2558
- return b;
2893
+ return BigInt(raw);
2894
+ }
2895
+ function parseCoinValueDecimal(label, raw, defaultStroops) {
2896
+ if (raw === undefined) {
2897
+ return defaultStroops.toString(10);
2559
2898
  }
2560
- catch {
2561
- console.error(`Error: invalid ${label}`);
2899
+ if (!/^\d+$/.test(raw)) {
2900
+ console.error(`Error: ${label} must be a non-negative decimal integer`);
2562
2901
  process.exit(1);
2563
2902
  }
2903
+ return raw;
2564
2904
  }
2565
2905
  async function handleGenerate(args) {
2566
2906
  const parsed = parseArgs(args);
2567
2907
  const sdk = await PrivacyPoolSDK.init();
2568
2908
  const scalar = parsed['scalar'];
2569
2909
  const stealth = parsed['stealth'];
2570
- const amount = parseStroopsU64('--amount', parsed['amount'], BigInt(COIN_VALUE_STROOPS));
2910
+ const amount = parseCoinValueDecimal('--amount', parsed['amount'], BigInt(COIN_VALUE_STROOPS));
2571
2911
  const applicationId = resolveApplicationIdFromCli(parsed);
2572
2912
  const token = parsed['token'] ?? process.env.TOKEN_ADDRESS;
2573
2913
  if (!token) {
@@ -2582,8 +2922,7 @@ async function handleGenerate(args) {
2582
2922
  let coin;
2583
2923
  if (scalar && stealth) {
2584
2924
  const { x, y } = decodeStealthAddress(stealth);
2585
- const shared = sdk.ecdhSharedKey(scalar, x, y);
2586
- coin = sdk.generateCoinForDepositWithSharedHex(scalar, shared.x, shared.y, amount, assetHi, assetLo, applicationId);
2925
+ coin = sdk.generateCoinForDepositWithOwnerPubHex(scalar, x, y, amount, assetHi, assetLo, applicationId);
2587
2926
  }
2588
2927
  else if (scalar) {
2589
2928
  coin = sdk.generateCoinFromDepositEphemeralScalarHex(scalar, amount, assetHi, assetLo, applicationId);
@@ -2644,9 +2983,10 @@ async function handleWithdraw(args) {
2644
2983
  console.error('Error: --change-stealth is only valid with --public-withdraw-stroops');
2645
2984
  process.exit(1);
2646
2985
  }
2647
- const sdk = await PrivacyPoolSDK.init();
2986
+ const { zkConfigNonce } = resolveZkInit(parsed);
2987
+ const sdk = await PrivacyPoolSDK.init({ zkConfigNonce });
2648
2988
  const applicationId = resolveApplicationIdFromCli(parsed);
2649
- const audit = resolveTransactionAuditParams(applicationId);
2989
+ const audit = resolveTransactionAuditParams(applicationId, undefined, sdk.getLayout().nAuditSlots);
2650
2990
  const result = await sdk.proveWithdrawal(coin, state, {
2651
2991
  withdrawAddressHi: withdrawHi,
2652
2992
  withdrawAddressLo: withdrawLo,
@@ -2657,7 +2997,7 @@ async function handleWithdraw(args) {
2657
2997
  audit,
2658
2998
  ...(pubW !== undefined
2659
2999
  ? {
2660
- publicWithdrawStroops: parseStroopsU64('--public-withdraw-stroops', pubW, 0n),
3000
+ publicWithdrawStroops: parseStroopsU64('--public-withdraw-stroops', pubW),
2661
3001
  changeRecipientStealthAddress: changeStealth,
2662
3002
  }
2663
3003
  : {}),
@@ -2674,6 +3014,10 @@ async function handleWithdraw(args) {
2674
3014
  fs__namespace.writeFileSync(parsed['output-public'], result.public_hex);
2675
3015
  console.error(`Public signals written to: ${parsed['output-public']}`);
2676
3016
  }
3017
+ if (parsed['output-ciphertext'] && result.ciphertext_hex) {
3018
+ fs__namespace.writeFileSync(parsed['output-ciphertext'], result.ciphertext_hex);
3019
+ console.error(`Ciphertext written to: ${parsed['output-ciphertext']}`);
3020
+ }
2677
3021
  }
2678
3022
  async function handleDepositProof(args) {
2679
3023
  const parsed = parseArgs(args);
@@ -2691,9 +3035,10 @@ async function handleDepositProof(args) {
2691
3035
  process.exit(1);
2692
3036
  }
2693
3037
  const [assetHi, assetLo] = stellarContractAddressToAssetFrDecimals(token);
2694
- const sdk = await PrivacyPoolSDK.init();
3038
+ const { zkConfigNonce } = resolveZkInit(parsed);
3039
+ const sdk = await PrivacyPoolSDK.init({ zkConfigNonce });
2695
3040
  const applicationId = resolveApplicationIdFromCli(parsed);
2696
- const audit = resolveTransactionAuditParams(applicationId);
3041
+ const audit = resolveTransactionAuditParams(applicationId, undefined, sdk.getLayout().nAuditSlots);
2697
3042
  const recipientPublicKeys = recipientPublicKeysDecimalFromStealthAddress(parsed['stealth']);
2698
3043
  const coinPath = parsed['coin'];
2699
3044
  const ephemeralScalarHex = parsed['ephemeral-scalar-hex'];
@@ -2757,6 +3102,10 @@ async function handleDepositProof(args) {
2757
3102
  fs__namespace.writeFileSync(parsed['output-public'], result.public_hex);
2758
3103
  console.error(`Public signals written to: ${parsed['output-public']}`);
2759
3104
  }
3105
+ if (parsed['output-ciphertext'] && result.ciphertext_hex) {
3106
+ fs__namespace.writeFileSync(parsed['output-ciphertext'], result.ciphertext_hex);
3107
+ console.error(`Ciphertext written to: ${parsed['output-ciphertext']}`);
3108
+ }
2760
3109
  }
2761
3110
  main().catch((err) => {
2762
3111
  console.error('Error:', err.message || err);