@orbinum/sdk 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -41,6 +41,7 @@ var SubstrateClient = class _SubstrateClient {
41
41
  constructor(_papi) {
42
42
  this._papi = _papi;
43
43
  }
44
+ _papi;
44
45
  _dynamicBuilder = null;
45
46
  _extDecoder = null;
46
47
  /**
@@ -240,6 +241,14 @@ var SubstrateClient = class _SubstrateClient {
240
241
  submitAndWatch(signedHex) {
241
242
  return this._papi.submitAndWatch(signedHex);
242
243
  }
244
+ /**
245
+ * Submits a bare (unsigned) extrinsic hex and waits for finalization.
246
+ * Used for gasless private_transfer and unshield transactions.
247
+ * The bare tx hex is produced by `tx.getBareTx()` from polkadot-api.
248
+ */
249
+ async submitUnsignedAndWatch(bareTxHex) {
250
+ return this._papi.submit(bareTxHex);
251
+ }
243
252
  /**
244
253
  * Convenience: wrap raw call bytes and sign+submit in one step.
245
254
  */
@@ -291,15 +300,14 @@ var SubstrateClient = class _SubstrateClient {
291
300
  }
292
301
  static _buildDataProxy(value) {
293
302
  const formatValue = (v) => {
294
- if (v instanceof Uint8Array) return fromHex(v).toString();
303
+ if (v instanceof Uint8Array) return toHex(v);
295
304
  if (typeof v === "bigint") return v.toString();
296
305
  return String(v);
297
306
  };
298
307
  const jsonifyValue = (v) => {
299
308
  if (v === null || v === void 0) return v;
300
309
  if (typeof v === "bigint") return v.toString();
301
- if (v instanceof Uint8Array)
302
- return `0x${Array.from(v).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
310
+ if (v instanceof Uint8Array) return toHex(v);
303
311
  if (Array.isArray(v)) return v.map(jsonifyValue);
304
312
  if (typeof v === "object") {
305
313
  const obj = v;
@@ -403,11 +411,14 @@ var SubstrateClient = class _SubstrateClient {
403
411
 
404
412
  // src/evm/EvmClient.ts
405
413
  var EvmClient = class {
414
+ /** @param rpcUrl - HTTP URL of the EVM JSON-RPC endpoint (e.g. `"http://localhost:9933"`). */
406
415
  constructor(rpcUrl) {
407
416
  this.rpcUrl = rpcUrl;
408
417
  }
418
+ rpcUrl;
409
419
  /**
410
- * Performs a single JSON-RPC call.
420
+ * Performs a single JSON-RPC call and returns the typed result.
421
+ * Throws on HTTP errors, RPC-level errors, or a `null` result.
411
422
  */
412
423
  async request(method, params = []) {
413
424
  const res = await fetch(this.rpcUrl, {
@@ -427,6 +438,7 @@ var EvmClient = class {
427
438
  }
428
439
  /**
429
440
  * Performs multiple JSON-RPC calls in a single HTTP request (batch).
441
+ * Results are returned in the same order as `calls`, as a typed tuple.
430
442
  */
431
443
  async batchRequest(calls) {
432
444
  const body = calls.map((c, i) => ({
@@ -471,30 +483,22 @@ var EvmClient = class {
471
483
  const hex = await this.request("eth_gasPrice", []);
472
484
  return hexToBigint(hex);
473
485
  }
474
- /**
475
- * Submits a signed raw transaction. Returns the transaction hash.
476
- */
486
+ /** Submits a signed raw transaction. Returns the transaction hash. */
477
487
  async sendRawTransaction(signedHex) {
478
488
  return this.request("eth_sendRawTransaction", [signedHex]);
479
489
  }
480
- /**
481
- * Executes a read-only call without creating a transaction.
482
- */
490
+ /** Executes a read-only call without creating a transaction. Returns the raw ABI-encoded response. */
483
491
  async call(to, data, from) {
484
492
  const txObj = { to, data };
485
493
  if (from) txObj["from"] = from;
486
494
  return this.request("eth_call", [txObj, "latest"]);
487
495
  }
488
- /**
489
- * Estimates the gas for a transaction.
490
- */
496
+ /** Estimates the gas required for a transaction. Returns the estimate in wei as a `bigint`. */
491
497
  async estimateGas(params) {
492
498
  const hex = await this.request("eth_estimateGas", [params]);
493
499
  return hexToBigint(hex);
494
500
  }
495
- /**
496
- * Returns a transaction receipt by hash, or null if not yet mined.
497
- */
501
+ /** Returns a transaction receipt by hash, or `null` if the transaction has not been mined yet. */
498
502
  async getTransactionReceipt(txHash) {
499
503
  const res = await fetch(this.rpcUrl, {
500
504
  method: "POST",
@@ -513,6 +517,56 @@ var EvmClient = class {
513
517
  }
514
518
  return json.result ?? null;
515
519
  }
520
+ /**
521
+ * Polls `eth_getTransactionReceipt` until the transaction is included in a block.
522
+ *
523
+ * @param txHash - The transaction hash to wait for.
524
+ * @param intervalMs - Polling interval in milliseconds (default: 500).
525
+ * @param timeoutMs - Maximum time to wait in milliseconds (default: 60_000).
526
+ * @returns The transaction receipt once mined.
527
+ * @throws If the transaction is not mined within `timeoutMs` or if it reverted (`status == 0x0`).
528
+ */
529
+ async waitForReceipt(txHash, intervalMs = 500, timeoutMs = 6e4) {
530
+ const deadline = Date.now() + timeoutMs;
531
+ while (Date.now() < deadline) {
532
+ const receipt = await this.getTransactionReceipt(txHash);
533
+ if (receipt !== null) {
534
+ if (receipt["status"] === "0x0") {
535
+ let revertDetail = "";
536
+ const nodeReason = receipt["revertReason"];
537
+ if (nodeReason) revertDetail = ` | revertReason: ${nodeReason}`;
538
+ if (!revertDetail) {
539
+ try {
540
+ const blockParam = receipt["blockNumber"] ?? "latest";
541
+ const rawTx = await this.request(
542
+ "eth_getTransactionByHash",
543
+ [txHash]
544
+ ).catch(() => null);
545
+ if (rawTx) {
546
+ const calldata = rawTx["input"] ?? rawTx["data"];
547
+ if (calldata) {
548
+ const revertData = await this.request("eth_call", [
549
+ { from: rawTx["from"], to: rawTx["to"], data: calldata },
550
+ blockParam
551
+ ]).catch(
552
+ (err) => err instanceof Error ? err.message : String(err)
553
+ );
554
+ revertDetail = ` | eth_call: ${revertData}`;
555
+ }
556
+ }
557
+ } catch {
558
+ }
559
+ }
560
+ throw new Error(
561
+ `Transaction reverted on-chain: ${txHash}${revertDetail} | receipt: ${JSON.stringify(receipt)}`
562
+ );
563
+ }
564
+ return receipt;
565
+ }
566
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
567
+ }
568
+ throw new Error(`Transaction not mined within ${timeoutMs}ms: ${txHash}`);
569
+ }
516
570
  };
517
571
 
518
572
  // src/utils/format.ts
@@ -585,10 +639,13 @@ function formatORB(raw, precision = 6) {
585
639
 
586
640
  // src/evm-explorer/EvmExplorer.ts
587
641
  var EvmExplorer = class _EvmExplorer {
642
+ /** @param evm - Underlying `EvmClient` used for all RPC calls. */
588
643
  constructor(evm) {
589
644
  this.evm = evm;
590
645
  }
646
+ evm;
591
647
  // --- Blocks ---
648
+ /** Returns the `count` most recent blocks in descending order (latest first). */
592
649
  async getLatestBlocks(count = 10) {
593
650
  const latest = await this.evm.getBlockNumber();
594
651
  const nums = Array.from({ length: Math.min(count, latest + 1) }, (_, i) => latest - i);
@@ -602,10 +659,12 @@ var EvmExplorer = class _EvmExplorer {
602
659
  );
603
660
  return results.filter((b) => b !== null && !!b.hash).map((b) => this.parseBlock(b));
604
661
  }
662
+ /** Returns a single block by number or hash, or `null` if not found. */
605
663
  async getBlock(hashOrNumber) {
606
664
  const b = await this.fetchBlock(hashOrNumber, false);
607
665
  return b ? this.parseBlock(b) : null;
608
666
  }
667
+ /** Returns all transactions in a block (with receipts), or `[]` if the block is not found. */
609
668
  async getBlockTransactions(hashOrNumber) {
610
669
  try {
611
670
  const b = await this.fetchBlock(hashOrNumber, true);
@@ -618,6 +677,7 @@ var EvmExplorer = class _EvmExplorer {
618
677
  }
619
678
  }
620
679
  // --- Transactions ---
680
+ /** Returns a single transaction with its receipt, or `null` if not found. */
621
681
  async getTransaction(hash) {
622
682
  try {
623
683
  const [tx, receipt] = await Promise.all([
@@ -630,6 +690,10 @@ var EvmExplorer = class _EvmExplorer {
630
690
  return null;
631
691
  }
632
692
  }
693
+ /**
694
+ * Returns lightweight summaries of all transactions sent from or to `address`
695
+ * within the last `maxBlocks` blocks, sorted by block number descending.
696
+ */
633
697
  async getTransactionsByAddress(address, maxBlocks = 300) {
634
698
  const addr = address.toLowerCase();
635
699
  const latest = await this.evm.getBlockNumber();
@@ -681,6 +745,10 @@ var EvmExplorer = class _EvmExplorer {
681
745
  return results;
682
746
  }
683
747
  // --- Address ---
748
+ /**
749
+ * Returns aggregated on-chain data for an EVM address: balance, nonce,
750
+ * bytecode (truncated), and up to 50 recent logs from the last 5 000 blocks.
751
+ */
684
752
  async getAddressInfo(address) {
685
753
  const latest = await this.evm.getBlockNumber().catch(() => 0);
686
754
  const fromBlock = `0x${Math.max(0, latest - 5e3).toString(16)}`;
@@ -711,6 +779,7 @@ var EvmExplorer = class _EvmExplorer {
711
779
  recentLogs
712
780
  };
713
781
  }
782
+ /** Returns the native token balance of `address`, formatted as a decimal string (no symbol). */
714
783
  async getBalance(address) {
715
784
  try {
716
785
  const val = await this.evm.getBalance(address);
@@ -719,6 +788,7 @@ var EvmExplorer = class _EvmExplorer {
719
788
  return "0";
720
789
  }
721
790
  }
791
+ /** Returns the current transaction count (nonce) for `address`, or `0` on error. */
722
792
  async getNonce(address) {
723
793
  try {
724
794
  return await this.evm.getTransactionCount(address);
@@ -726,6 +796,7 @@ var EvmExplorer = class _EvmExplorer {
726
796
  return 0;
727
797
  }
728
798
  }
799
+ /** Returns `true` when `address` has non-empty deployed bytecode. */
729
800
  async getIsContract(address) {
730
801
  try {
731
802
  const code = await this.evm.request("eth_getCode", [address, "latest"]);
@@ -735,6 +806,10 @@ var EvmExplorer = class _EvmExplorer {
735
806
  }
736
807
  }
737
808
  // --- Tokens ---
809
+ /**
810
+ * Fetches ERC-20 metadata for a token contract via ABI calls.
811
+ * Returns `null` when the address does not look like an ERC-20 token.
812
+ */
738
813
  async getTokenInfo(address) {
739
814
  const addr = address.toLowerCase();
740
815
  const [name, symbol, decimals, totalSupply] = await this.evm.batchRequest([
@@ -754,6 +829,10 @@ var EvmExplorer = class _EvmExplorer {
754
829
  isErc20
755
830
  };
756
831
  }
832
+ /**
833
+ * Returns ERC-20 `Transfer` events for `address` from the last 5 000 blocks.
834
+ * When `holderAddress` is provided, restricts results to transfers sent or received by that address.
835
+ */
757
836
  async getTokenTransfers(address, holderAddress) {
758
837
  const TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
759
838
  const latest = await this.evm.getBlockNumber().catch(() => 0);
@@ -781,12 +860,14 @@ var EvmExplorer = class _EvmExplorer {
781
860
  logIndex: hexToNumber(l.logIndex)
782
861
  }));
783
862
  }
863
+ /** Returns the raw ERC-20 balance of `holderAddress` for the token at `tokenAddress` (0x-prefixed hex). */
784
864
  async getTokenBalance(tokenAddress, holderAddress) {
785
865
  const padded = holderAddress.replace(/^0x/, "").toLowerCase().padStart(64, "0");
786
866
  const result = await this.ethCall(tokenAddress, `0x70a08231${padded}`);
787
867
  return result ?? "0x0";
788
868
  }
789
869
  // --- Private: parsers ---
870
+ /** Maps a raw RPC block object to the public `EvmBlock` shape. */
790
871
  parseBlock(b) {
791
872
  return {
792
873
  hash: b.hash,
@@ -799,6 +880,7 @@ var EvmExplorer = class _EvmExplorer {
799
880
  parentHash: b.parentHash
800
881
  };
801
882
  }
883
+ /** Maps a raw RPC transaction + optional receipt to the public `EvmTransaction` shape. */
802
884
  parseTx(tx, receipt) {
803
885
  const parsed = {
804
886
  hash: tx.hash,
@@ -818,6 +900,10 @@ var EvmExplorer = class _EvmExplorer {
818
900
  return parsed;
819
901
  }
820
902
  // --- Private: fetch helpers ---
903
+ /**
904
+ * Fetches a raw block by number or hash. Accepts a plain integer, a decimal string,
905
+ * or a 0x-prefixed hash. Returns `null` on any RPC error.
906
+ */
821
907
  async fetchBlock(hashOrNumber, withTxObjects) {
822
908
  try {
823
909
  if (typeof hashOrNumber === "number" || /^\d+$/.test(String(hashOrNumber))) {
@@ -835,6 +921,7 @@ var EvmExplorer = class _EvmExplorer {
835
921
  return null;
836
922
  }
837
923
  }
924
+ /** Executes a read-only `eth_call` and returns the raw hex result, or `null` on error. */
838
925
  async ethCall(to, data) {
839
926
  try {
840
927
  return await this.evm.call(to, data);
@@ -843,6 +930,7 @@ var EvmExplorer = class _EvmExplorer {
843
930
  }
844
931
  }
845
932
  // --- Private static: ABI decoders ---
933
+ /** Decodes an ABI-encoded `string` return value from a raw 0x-prefixed hex string. */
846
934
  static decodeAbiString(hex) {
847
935
  if (!hex || hex === "0x") return "";
848
936
  const data = hex.startsWith("0x") ? hex.slice(2) : hex;
@@ -857,11 +945,13 @@ var EvmExplorer = class _EvmExplorer {
857
945
  return "";
858
946
  }
859
947
  }
948
+ /** Decodes an ABI-encoded `uint256` return value to a `bigint`. */
860
949
  static decodeAbiUint(hex) {
861
950
  if (!hex || hex === "0x") return 0n;
862
951
  const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
863
952
  return BigInt(`0x${clean || "0"}`);
864
953
  }
954
+ /** Converts a 0x-prefixed hex number to its decimal string representation. Returns `'0'` on parse error. */
865
955
  static hexToDecimalStr(hex) {
866
956
  try {
867
957
  return BigInt(hex).toString();
@@ -896,6 +986,24 @@ var IndexerClient = class {
896
986
  }
897
987
  return res.json();
898
988
  }
989
+ async post(path, body) {
990
+ const controller = new AbortController();
991
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
992
+ try {
993
+ const res = await fetch(`${this.baseUrl}${path}`, {
994
+ method: "POST",
995
+ headers: { "Content-Type": "application/json" },
996
+ body: JSON.stringify(body),
997
+ signal: controller.signal
998
+ });
999
+ if (!res.ok) {
1000
+ throw new Error(`IndexerClient: HTTP ${res.status} for POST ${path}`);
1001
+ }
1002
+ return res.json();
1003
+ } finally {
1004
+ clearTimeout(timer);
1005
+ }
1006
+ }
899
1007
  async getOrNull(path) {
900
1008
  const res = await this._fetchResponse(path);
901
1009
  if (res.status === 404) return null;
@@ -931,6 +1039,21 @@ var IndexerClient = class {
931
1039
  `/shielded/commitments/${encodeURIComponent(hex)}`
932
1040
  );
933
1041
  }
1042
+ /**
1043
+ * Returns a paginated list of stealth scan hints ordered ascending by leafIndex.
1044
+ * Each hint contains only the fields required for ECDH triage and decryption:
1045
+ * leafIndex, commitmentHex, assetId, ephPkHex, encryptedMemo.
1046
+ *
1047
+ * Use `sinceLeafIndex` for incremental scans (cursor = last seen leafIndex + 1).
1048
+ */
1049
+ async getScanHints(params) {
1050
+ const qs = this.buildQuery({
1051
+ page: params?.page,
1052
+ limit: params?.limit,
1053
+ since_leaf_index: params?.sinceLeafIndex
1054
+ });
1055
+ return this.get(`/shielded/scan-hints${qs}`);
1056
+ }
934
1057
  // ─── Nullifiers ────────────────────────────────────────────────────────────
935
1058
  /** Returns a paginated list of spent nullifiers. */
936
1059
  async getNullifiers(params) {
@@ -943,11 +1066,50 @@ var IndexerClient = class {
943
1066
  `/shielded/nullifier/${encodeURIComponent(hex)}/status`
944
1067
  );
945
1068
  }
1069
+ /**
1070
+ * Batch-checks which of the given nullifiers are spent.
1071
+ * Returns only the nullifiers that exist in the spent set.
1072
+ * Accepts up to 100 nullifiers (0x-prefixed hex).
1073
+ */
1074
+ async getNullifiersBatch(nullifiers) {
1075
+ if (nullifiers.length === 0) return [];
1076
+ const res = await this.post("/shielded/nullifiers/batch", {
1077
+ nullifiers: nullifiers.map((n) => n.toLowerCase())
1078
+ });
1079
+ return res.data;
1080
+ }
946
1081
  // ─── Private transfers ─────────────────────────────────────────────────────
947
- /** Returns a paginated list of private transfer events. */
948
- async getTransfers(params) {
949
- const qs = this.buildQuery({ page: params?.page, limit: params?.limit });
950
- return this.get(`/shielded/transfers${qs}`);
1082
+ /**
1083
+ * Returns temporal metadata for private transfers that spent any of the given nullifiers.
1084
+ * Only blockNumber, extrinsicIndex, timestampMs, and hash are returned — no cross-link
1085
+ * between inputs and outputs to prevent graph reconstruction.
1086
+ * Accepts up to 50 nullifiers (0x-prefixed hex).
1087
+ */
1088
+ async getTransfersByNullifiers(nullifiers) {
1089
+ if (nullifiers.length === 0) return [];
1090
+ const qs = this.buildQuery({
1091
+ nullifiers: nullifiers.map((n) => n.toLowerCase()).join(",")
1092
+ });
1093
+ const res = await this.get(
1094
+ `/shielded/transfers/by-nullifiers${qs}`
1095
+ );
1096
+ return res.data;
1097
+ }
1098
+ /**
1099
+ * Returns temporal metadata for private transfers that produced any of the given commitments.
1100
+ * Only blockNumber, extrinsicIndex, timestampMs, and hash are returned — no cross-link
1101
+ * between outputs and inputs to prevent graph reconstruction.
1102
+ * Accepts up to 50 commitments (0x-prefixed hex).
1103
+ */
1104
+ async getTransfersByCommitments(commitments) {
1105
+ if (commitments.length === 0) return [];
1106
+ const qs = this.buildQuery({
1107
+ commitments: commitments.map((c) => c.toLowerCase()).join(",")
1108
+ });
1109
+ const res = await this.get(
1110
+ `/shielded/transfers/by-commitments${qs}`
1111
+ );
1112
+ return res.data;
951
1113
  }
952
1114
  // ─── Unshields ─────────────────────────────────────────────────────────────
953
1115
  /** Returns a paginated list of unshield events. */
@@ -1007,6 +1169,16 @@ var IndexerClient = class {
1007
1169
  `/address/${encodeURIComponent(address.toLowerCase())}/shielded${qs}`
1008
1170
  );
1009
1171
  }
1172
+ /**
1173
+ * Returns a paginated list of unshield events where the given address is the recipient.
1174
+ * Accepts a 0x-prefixed EVM address or an SS58 Substrate address.
1175
+ */
1176
+ async getAddressUnshields(address, params) {
1177
+ const qs = this.buildQuery({ page: params?.page, limit: params?.limit });
1178
+ return this.get(
1179
+ `/address/${encodeURIComponent(address)}/unshields${qs}`
1180
+ );
1181
+ }
1010
1182
  /**
1011
1183
  * Returns a paginated list of all shielded activity (commitments, unshields,
1012
1184
  * private transfers) associated with the given address.
@@ -1042,10 +1214,24 @@ var IndexerClient = class {
1042
1214
  }
1043
1215
  };
1044
1216
 
1045
- // src/shielded-pool/ShieldedPoolModule.ts
1217
+ // src/shielded-pool/pallet/ShieldedPoolModule.ts
1046
1218
  import { Binary as Binary2 } from "polkadot-api";
1047
1219
 
1048
1220
  // src/utils/tx.ts
1221
+ function formatDispatchError(err) {
1222
+ if (err.type === "Module") {
1223
+ const inner = err.value;
1224
+ if (inner?.type) {
1225
+ return `Module(${inner.type})`;
1226
+ }
1227
+ }
1228
+ try {
1229
+ const detail = JSON.stringify(err.value);
1230
+ return detail && detail !== "null" ? `${err.type}(${detail})` : err.type;
1231
+ } catch {
1232
+ return err.type;
1233
+ }
1234
+ }
1049
1235
  function toTxResult(payload) {
1050
1236
  const base = {
1051
1237
  txHash: payload.txHash,
@@ -1054,13 +1240,18 @@ function toTxResult(payload) {
1054
1240
  ok: payload.ok
1055
1241
  };
1056
1242
  if (!payload.ok) {
1057
- return { ...base, error: payload.dispatchError.type };
1243
+ return { ...base, error: formatDispatchError(payload.dispatchError) };
1058
1244
  }
1059
1245
  return base;
1060
1246
  }
1061
1247
  function callUnsafeTx(txEntry, ...args) {
1062
1248
  return txEntry(...args);
1063
1249
  }
1250
+ async function submitBareTx(tx, client) {
1251
+ const bareTxHex = await tx.getBareTx();
1252
+ const payload = await client.submitUnsignedAndWatch(bareTxHex);
1253
+ return toTxResult(payload);
1254
+ }
1064
1255
  function resolveTx(unsafe, pallet, call) {
1065
1256
  const u = unsafe;
1066
1257
  const p = u["tx"]?.[pallet];
@@ -1071,9 +1262,10 @@ function resolveTx(unsafe, pallet, call) {
1071
1262
  return entry;
1072
1263
  }
1073
1264
 
1074
- // src/shielded-pool/EncryptedMemo.ts
1265
+ // src/shielded-pool/protocol/EncryptedMemo.ts
1075
1266
  import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
1076
1267
  import { randomBytes } from "@noble/ciphers/utils.js";
1268
+ import { mulPointEscalar, Base8, packPoint, unpackPoint } from "@zk-kit/baby-jubjub";
1077
1269
 
1078
1270
  // src/utils/bytes.ts
1079
1271
  function bigintTo32Le(n) {
@@ -1120,77 +1312,114 @@ function computePathIndices(leafIndex, depth) {
1120
1312
  return indices;
1121
1313
  }
1122
1314
  function leHexToBigint(hex) {
1123
- const h = hex.startsWith("0x") ? hex.slice(2) : hex;
1124
- const bytes = new Uint8Array(h.length / 2);
1125
- for (let i = 0; i < bytes.length; i++) {
1126
- bytes[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
1127
- }
1128
- return bytesToBigintLE(bytes);
1315
+ return bytesToBigintLE(fromHex(hex));
1129
1316
  }
1130
1317
 
1131
- // src/shielded-pool/helpers.ts
1318
+ // src/utils/crypto-constants.ts
1319
+ var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
1320
+ var BABYJUB_SUBORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
1321
+
1322
+ // src/shielded-pool/protocol/memo.ts
1132
1323
  import { sha256 } from "@noble/hashes/sha2.js";
1133
1324
  var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
1134
- var MEMO_PLAINTEXT_SIZE = 76;
1135
- function serializeMemo(value, ownerPk, blinding, assetId) {
1325
+ var MEMO_PLAINTEXT_SIZE = 116;
1326
+ function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk) {
1136
1327
  const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
1137
1328
  const view = new DataView(buf.buffer);
1138
1329
  view.setBigUint64(0, value & 0xffffffffffffffffn, true);
1139
- buf.set(ownerPk.slice(0, 32), 8);
1140
- buf.set(blinding.slice(0, 32), 40);
1141
- view.setUint32(72, assetId >>> 0, true);
1330
+ view.setBigUint64(8, value >> 64n & 0xffffffffffffffffn, true);
1331
+ buf.set(ownerPk.slice(0, 32), 16);
1332
+ buf.set(blinding.slice(0, 32), 48);
1333
+ view.setUint32(80, assetId >>> 0, true);
1334
+ buf.set(counterpartyPk.slice(0, 32), 84);
1142
1335
  return buf;
1143
1336
  }
1144
- function deriveEncryptionKey(viewingKey, commitment) {
1337
+ function deriveEncryptionKey(sharedSecret, commitment) {
1145
1338
  const h = sha256.create();
1146
- h.update(viewingKey);
1339
+ h.update(sharedSecret);
1147
1340
  h.update(commitment);
1148
1341
  h.update(KEY_DOMAIN);
1149
1342
  return h.digest();
1150
1343
  }
1151
- function toBase64(buf) {
1152
- const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
1153
- let str = "";
1154
- for (const b of bytes) str += String.fromCharCode(b);
1155
- return btoa(str);
1156
- }
1157
- function fromBase64(b64) {
1158
- const bin = atob(b64);
1159
- const out = new Uint8Array(bin.length);
1160
- for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1161
- return out;
1162
- }
1163
1344
 
1164
- // src/shielded-pool/EncryptedMemo.ts
1345
+ // src/shielded-pool/protocol/EncryptedMemo.ts
1165
1346
  var NONCE_SIZE = 12;
1166
- var ENCRYPTED_MEMO_SIZE = 104;
1347
+ var CIPHERTEXT_SIZE = 132;
1348
+ var EPH_PK_SIZE = 32;
1349
+ var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
1350
+ function bytesToBjjScalar(bytes) {
1351
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1352
+ return BigInt("0x" + hex) % BABYJUB_SUBORDER || 1n;
1353
+ }
1354
+ function parsePlaintext(nonce, ciphertextWithMac, encKey) {
1355
+ try {
1356
+ const cipher = chacha20poly1305(encKey, nonce);
1357
+ const plaintext = cipher.decrypt(ciphertextWithMac);
1358
+ const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
1359
+ const valueLo = view.getBigUint64(0, true);
1360
+ const valueHi = view.getBigUint64(8, true);
1361
+ const value = valueLo | valueHi << 64n;
1362
+ const ownerPk = bytesToBigintLE(plaintext.slice(16, 48));
1363
+ const blinding = bytesToBigintLE(plaintext.slice(48, 80));
1364
+ const assetId = BigInt(view.getUint32(80, true));
1365
+ const counterpartyPk = bytesToBigintLE(plaintext.slice(84, 116));
1366
+ return { value, ownerPk, blinding, assetId, counterpartyPk };
1367
+ } catch {
1368
+ return null;
1369
+ }
1370
+ }
1167
1371
  var EncryptedMemo = {
1168
1372
  /**
1169
- * Build and encrypt a memo for a note.
1373
+ * Build and encrypt a memo for a note using ECDH (v2, 168 bytes).
1170
1374
  *
1171
- * @param value Note value in planck.
1172
- * @param ownerPk 32-byte owner public key (little-endian).
1173
- * @param blinding 32-byte blinding scalar (little-endian).
1174
- * @param assetId Asset identifier.
1175
- * @param commitment 32-byte commitment bytes (little-endian).
1176
- * @param recipientVk 32-byte recipient viewing key pass `new Uint8Array(32)`
1177
- * for a publicly-readable (dummy) memo.
1178
- * @returns 104-byte encrypted memo (nonce || ciphertext).
1179
- */
1180
- encrypt(value, ownerPk, blinding, assetId, commitment, recipientVk) {
1375
+ * @param value Note value in planck.
1376
+ * @param ownerPk 32-byte owner public key (LE).
1377
+ * @param blinding 32-byte blinding scalar (LE).
1378
+ * @param assetId Asset identifier.
1379
+ * @param commitment 32-byte commitment bytes (LE).
1380
+ * @param recipientIvkPacked 32-byte LE-encoded packed BJJ viewing public key
1381
+ * (from PrivacyKeyManager.getViewingPublicKeyPacked() or
1382
+ * decoded from a privacy address).
1383
+ * Pass `new Uint8Array(32)` (all zeros) for a publicly-readable memo.
1384
+ * @param counterpartyPk 32-byte counterparty BJJ Ax. Default: all zeros.
1385
+ * @returns 168-byte encrypted memo: nonce(12) || ciphertext+MAC(124) || ephPk(32).
1386
+ */
1387
+ encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, counterpartyPk = new Uint8Array(32), ephSkOverride) {
1181
1388
  const nonce = randomBytes(NONCE_SIZE);
1182
- const key = deriveEncryptionKey(recipientVk, commitment);
1183
- const plaintext = serializeMemo(value, ownerPk, blinding, assetId);
1184
- const cipher = chacha20poly1305(key, nonce);
1389
+ const plaintext = serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk);
1390
+ const isZeroKey = recipientIvkPacked.every((b) => b === 0);
1391
+ let sharedSecret;
1392
+ let ephPkPackedBytes;
1393
+ if (isZeroKey) {
1394
+ sharedSecret = new Uint8Array(32);
1395
+ ephPkPackedBytes = new Uint8Array(EPH_PK_SIZE);
1396
+ } else {
1397
+ const ephSkBytes = ephSkOverride ?? randomBytes(32);
1398
+ if (ephSkBytes.length !== 32)
1399
+ throw new Error("EncryptedMemo.encrypt: ephSkOverride must be 32 bytes");
1400
+ const ephSkScalar = bytesToBjjScalar(ephSkBytes);
1401
+ const ephPkPoint = mulPointEscalar(Base8, ephSkScalar);
1402
+ ephPkPackedBytes = bigintTo32Le(packPoint(ephPkPoint));
1403
+ const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
1404
+ const ivkPoint = unpackPoint(ivkPackedBigint);
1405
+ if (!ivkPoint)
1406
+ throw new Error("EncryptedMemo.encrypt: invalid recipient viewing public key");
1407
+ const sharedPoint = mulPointEscalar(ivkPoint, ephSkScalar);
1408
+ sharedSecret = bigintTo32Le(sharedPoint[0]);
1409
+ }
1410
+ const encKey = deriveEncryptionKey(sharedSecret, commitment);
1411
+ const cipher = chacha20poly1305(encKey, nonce);
1185
1412
  const ciphertext = cipher.encrypt(plaintext);
1186
- const result = new Uint8Array(NONCE_SIZE + ciphertext.length);
1413
+ const result = new Uint8Array(ENCRYPTED_MEMO_SIZE);
1187
1414
  result.set(nonce, 0);
1188
1415
  result.set(ciphertext, NONCE_SIZE);
1416
+ result.set(ephPkPackedBytes, NONCE_SIZE + CIPHERTEXT_SIZE);
1189
1417
  return result;
1190
1418
  },
1191
1419
  /**
1192
- * Returns a 104-byte public memo with a zero recipient viewing key.
1193
- * The memo is still readable by anyone who holds the viewing key (zeros).
1420
+ * Returns a 168-byte public memo encrypted with a zero viewing key.
1421
+ * Decryptable by anyone with `decrypt(memo, commitment, new Uint8Array(32))`.
1422
+ * Convenience alias for `encrypt(..., new Uint8Array(32))`.
1194
1423
  */
1195
1424
  encryptPublic(value, ownerPk, blinding, assetId, commitment) {
1196
1425
  return EncryptedMemo.encrypt(
@@ -1203,226 +1432,89 @@ var EncryptedMemo = {
1203
1432
  );
1204
1433
  },
1205
1434
  /**
1206
- * Returns a 104-byte zeroed dummy memo (no information, always valid on-chain).
1435
+ * Returns a 168-byte zeroed dummy memo (no information, always valid on-chain).
1207
1436
  */
1208
1437
  dummy() {
1209
1438
  return new Uint8Array(ENCRYPTED_MEMO_SIZE);
1210
1439
  },
1211
1440
  /**
1212
- * Decrypt an on-chain EncryptedMemo.
1441
+ * Validates that `bytes` is a properly-sized encrypted memo.
1442
+ * Throws an Error if the length is not ENCRYPTED_MEMO_SIZE (168 bytes).
1213
1443
  *
1214
- * Returns `null` if decryption fails wrong key, bad MAC, or malformed memo.
1215
- * Never throws; safe for scan loops.
1444
+ * Call this at system boundaries (extrinsic builders, precompile encoders)
1445
+ * to catch malformed memos before they reach the chain and fail on-chain.
1216
1446
  *
1217
- * @param memoBytes 104-byte encrypted memo.
1218
- * @param commitment 32-byte note commitment (little-endian).
1219
- * @param recipientVk 32-byte recipient viewing key.
1447
+ * @param bytes The memo bytes to validate.
1448
+ * @param context Optional context string included in the error (e.g. 'shield', 'output[0]').
1220
1449
  */
1221
- decrypt(memoBytes, commitment, recipientVk) {
1222
- if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
1223
- try {
1224
- const nonce = memoBytes.slice(0, NONCE_SIZE);
1225
- const ciphertext = memoBytes.slice(NONCE_SIZE);
1226
- const key = deriveEncryptionKey(recipientVk, commitment);
1227
- const cipher = chacha20poly1305(key, nonce);
1228
- const plaintext = cipher.decrypt(ciphertext);
1229
- const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
1230
- const value = view.getBigUint64(0, true);
1231
- const ownerPk = bytesToBigintLE(plaintext.slice(8, 40));
1232
- const blinding = bytesToBigintLE(plaintext.slice(40, 72));
1233
- const assetId = BigInt(view.getUint32(72, true));
1234
- return { value, ownerPk, blinding, assetId };
1235
- } catch {
1236
- return null;
1450
+ validate(bytes, context) {
1451
+ if (bytes.length !== ENCRYPTED_MEMO_SIZE) {
1452
+ const ctx = context ? ` (${context})` : "";
1453
+ throw new Error(
1454
+ `EncryptedMemo: invalid size${ctx} \u2014 expected ${ENCRYPTED_MEMO_SIZE} bytes, got ${bytes.length}`
1455
+ );
1237
1456
  }
1238
- }
1239
- };
1240
-
1241
- // src/shielded-pool/NoteBuilder.ts
1242
- import { poseidon2, poseidon4 } from "poseidon-lite";
1243
- var NoteBuilder = class {
1457
+ },
1244
1458
  /**
1245
- * Build a ZkNote from the given inputs.
1459
+ * Decrypt an on-chain EncryptedMemo using the recipient's viewing secret key.
1460
+ * Returns null if decryption fails — wrong key, bad MAC, or malformed memo.
1461
+ * Never throws; safe for scan loops.
1246
1462
  *
1247
- * @param input.value Amount in planck (required).
1248
- * @param input.assetId Asset ID default 0n (native ORB-Privacy).
1249
- * @param input.ownerPk BabyJubJub Ax default 0n.
1250
- * @param input.blinding Random scalar — defaults to BigInt(Date.now()).
1251
- * @param input.spendingKey Secret key for nullifier — default 0n.
1463
+ * @param memoBytes 168-byte encrypted memo.
1464
+ * @param commitment 32-byte note commitment (LE).
1465
+ * @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
1252
1466
  */
1253
- static async build(input) {
1254
- const value = input.value;
1255
- const assetId = input.assetId ?? 0n;
1256
- const ownerPk = input.ownerPk ?? 0n;
1257
- const blinding = input.blinding ?? BigInt(Date.now());
1258
- const spendingKey = input.spendingKey ?? 0n;
1259
- const commitment = poseidon4([value, assetId, ownerPk, blinding]);
1260
- const nullifier = poseidon2([commitment, spendingKey]);
1261
- const commitmentBytes = bigintTo32Le(commitment);
1262
- const nullifierBytes = bigintTo32Le(nullifier);
1263
- const memo = input.viewingKey !== void 0 ? Array.from(
1264
- EncryptedMemo.encrypt(
1265
- value,
1266
- bigintTo32Le(ownerPk),
1267
- bigintTo32Le(blinding),
1268
- Number(assetId),
1269
- commitmentBytes,
1270
- input.viewingKey
1271
- )
1272
- ) : Array.from(EncryptedMemo.dummy());
1273
- const note = {
1274
- value,
1275
- assetId,
1276
- ownerPk,
1277
- blinding,
1278
- spendingKey,
1279
- spent: false,
1280
- spentAt: null,
1281
- commitment,
1282
- nullifier,
1283
- commitmentHex: toHex(commitmentBytes),
1284
- nullifierHex: toHex(nullifierBytes),
1285
- memo
1286
- };
1287
- return note;
1288
- }
1467
+ decrypt(memoBytes, commitment, viewingSecretKey) {
1468
+ if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
1469
+ return EncryptedMemo._decrypt(memoBytes, commitment, viewingSecretKey);
1470
+ },
1289
1471
  /**
1290
- * Build the 104-byte encrypted memo for a note.
1472
+ * Extract the ECDH shared secret from an encrypted memo using the recipient's viewing secret key.
1291
1473
  *
1292
- * Pure TypeScript implementation no WASM dependency.
1293
- * Uses ChaCha20-Poly1305 with SHA-256 key derivation.
1474
+ * Used by NoteDecryptor to obtain the shared secret needed for stealth address derivation
1475
+ * without re-running the full decrypt path. Safe to call on any 168-byte memo.
1294
1476
  *
1295
- * @param note The ZkNote whose fields populate the plaintext.
1296
- * @param recipientVk 32-byte recipient viewing key.
1297
- * Pass `new Uint8Array(32)` (default) for a public/dummy memo.
1298
- */
1299
- static buildMemo(note, recipientVk) {
1300
- return EncryptedMemo.encrypt(
1301
- note.value,
1302
- bigintTo32Le(note.ownerPk),
1303
- bigintTo32Le(note.blinding),
1304
- Number(note.assetId),
1305
- bigintTo32Le(note.commitment),
1306
- recipientVk ?? new Uint8Array(32)
1307
- );
1308
- }
1309
- };
1310
-
1311
- // src/shielded-pool/ShieldedPoolModule.ts
1312
- var ShieldedPoolModule = class {
1313
- constructor(substrate) {
1314
- this.substrate = substrate;
1315
- }
1316
- // ─── Extrinsics ────────────────────────────────────────────────────────────
1317
- /**
1318
- * Deposits tokens into the shielded pool.
1319
- * Extrinsic: shieldedPool.shield(assetId, amount, commitment, encryptedMemo)
1320
- */
1321
- async shield(params, signer) {
1322
- const memo = params.encryptedMemo ?? EncryptedMemo.dummy();
1323
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "shield");
1324
- const tx = callUnsafeTx(
1325
- entry,
1326
- params.assetId,
1327
- params.amount.toString(),
1328
- Binary2.fromHex(params.commitment),
1329
- Binary2.fromBytes(memo)
1330
- );
1331
- return toTxResult(await tx.signAndSubmit(signer));
1332
- }
1333
- /**
1334
- * Build a ZkNote locally and submit shieldedPool.shield in one call.
1335
- *
1336
- * Returns both the on-chain result and the note — **save the note locally**,
1337
- * it cannot be recovered after the fact.
1477
+ * Returns `new Uint8Array(32)` (all zeros) for public/dummy memos (zero ephPk).
1478
+ * Returns `null` if the memo is malformed or the ephPk is not a valid BJJ point.
1479
+ * Never throws; safe for scan loops.
1338
1480
  *
1339
- * @param params.value Amount in planck (required).
1340
- * @param params.assetId Asset ID default 0 (native ORB-Privacy).
1341
- * @param params.ownerPk BabyJubJub Ax (default 0n).
1342
- * @param params.blinding Random blinding scalar (default BigInt(Date.now())).
1343
- * @param params.spendingKey Secret spending key (default 0n).
1344
- */
1345
- async buildAndShield(params, signer) {
1346
- const noteInput = {
1347
- value: params.value,
1348
- ...params.assetId !== void 0 && { assetId: BigInt(params.assetId) },
1349
- ...params.ownerPk !== void 0 && { ownerPk: params.ownerPk },
1350
- ...params.blinding !== void 0 && { blinding: params.blinding },
1351
- ...params.spendingKey !== void 0 && { spendingKey: params.spendingKey }
1352
- };
1353
- const note = await NoteBuilder.build(noteInput);
1354
- const memo = NoteBuilder.buildMemo(note);
1355
- const txResult = await this.shield(
1356
- {
1357
- assetId: Number(note.assetId),
1358
- amount: note.value,
1359
- commitment: note.commitmentHex,
1360
- encryptedMemo: memo
1361
- },
1362
- signer
1363
- );
1364
- return { txResult, note };
1365
- }
1366
- /**
1367
- * Withdraws tokens from the shielded pool to a public address.
1368
- * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient)
1369
- */
1370
- async unshield(params, signer) {
1371
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "unshield");
1372
- const tx = callUnsafeTx(
1373
- entry,
1374
- Binary2.fromBytes(params.proof),
1375
- Binary2.fromHex(params.merkleRoot),
1376
- Binary2.fromHex(params.nullifier),
1377
- params.assetId,
1378
- params.amount.toString(),
1379
- Binary2.fromHex(params.recipientAddress)
1380
- );
1381
- return toTxResult(await tx.signAndSubmit(signer));
1382
- }
1383
- /**
1384
- * Performs a private (shielded) transfer between two notes.
1385
- * Extrinsic: shieldedPool.privateTransfer(inputs, outputs, proof, merkleRoot)
1386
- */
1387
- async privateTransfer(params, signer) {
1388
- const inputs = params.inputs.map((inp) => ({
1389
- nullifier: Binary2.fromHex(inp.nullifier),
1390
- commitment: Binary2.fromHex(inp.commitment)
1391
- }));
1392
- const outputs = params.outputs.map((out) => ({
1393
- commitment: Binary2.fromHex(out.commitment),
1394
- memo: Binary2.fromBytes(out.encryptedMemo ?? EncryptedMemo.dummy())
1395
- }));
1396
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "privateTransfer");
1397
- const tx = callUnsafeTx(
1398
- entry,
1399
- inputs,
1400
- outputs,
1401
- Binary2.fromBytes(params.proof),
1402
- Binary2.fromHex(params.merkleRoot)
1403
- );
1404
- return toTxResult(await tx.signAndSubmit(signer));
1405
- }
1406
- /**
1407
- * Deposits multiple notes into the shielded pool in a single extrinsic.
1408
- * Extrinsic: shieldedPool.shieldBatch(operations) — max 20 items.
1481
+ * @param memoBytes 168-byte encrypted memo.
1482
+ * @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
1409
1483
  */
1410
- async shieldBatch(params, signer) {
1411
- const operations = params.items.map((item) => ({
1412
- assetId: item.assetId,
1413
- amount: item.amount.toString(),
1414
- commitment: Binary2.fromHex(item.commitment),
1415
- encryptedMemo: Binary2.fromBytes(item.encryptedMemo ?? EncryptedMemo.dummy())
1416
- }));
1417
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "shieldBatch");
1418
- const tx = callUnsafeTx(entry, operations);
1419
- return toTxResult(await tx.signAndSubmit(signer));
1484
+ extractSharedSecret(memoBytes, viewingSecretKey) {
1485
+ if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
1486
+ const ephPkPackedBytes = memoBytes.slice(NONCE_SIZE + CIPHERTEXT_SIZE);
1487
+ const ephPkPackedBigint = bytesToBigintLE(ephPkPackedBytes);
1488
+ if (ephPkPackedBigint === 0n) {
1489
+ return new Uint8Array(32);
1490
+ }
1491
+ const ephPkPoint = unpackPoint(ephPkPackedBigint);
1492
+ if (!ephPkPoint) return null;
1493
+ const ivskScalar = bytesToBjjScalar(viewingSecretKey);
1494
+ const sharedPoint = mulPointEscalar(ephPkPoint, ivskScalar);
1495
+ return bigintTo32Le(sharedPoint[0]);
1496
+ },
1497
+ /** @internal */
1498
+ _decrypt(memoBytes, commitment, viewingSecretKey) {
1499
+ const nonce = memoBytes.slice(0, NONCE_SIZE);
1500
+ const ciphertextWithMac = memoBytes.slice(NONCE_SIZE, NONCE_SIZE + CIPHERTEXT_SIZE);
1501
+ const ephPkPackedBytes = memoBytes.slice(NONCE_SIZE + CIPHERTEXT_SIZE);
1502
+ const ephPkPackedBigint = bytesToBigintLE(ephPkPackedBytes);
1503
+ let sharedSecret;
1504
+ if (ephPkPackedBigint === 0n) {
1505
+ sharedSecret = new Uint8Array(32);
1506
+ } else {
1507
+ const ephPkPoint = unpackPoint(ephPkPackedBigint);
1508
+ if (!ephPkPoint) return null;
1509
+ const ivskScalar = bytesToBjjScalar(viewingSecretKey);
1510
+ const sharedPoint = mulPointEscalar(ephPkPoint, ivskScalar);
1511
+ sharedSecret = bigintTo32Le(sharedPoint[0]);
1512
+ }
1513
+ const encKey = deriveEncryptionKey(sharedSecret, commitment);
1514
+ return parsePlaintext(nonce, ciphertextWithMac, encKey);
1420
1515
  }
1421
1516
  };
1422
1517
 
1423
- // src/account-mapping/AccountMappingModule.ts
1424
- import { Binary as Binary3 } from "polkadot-api";
1425
-
1426
1518
  // src/utils/address.ts
1427
1519
  import { decodeAddress, encodeAddress } from "@polkadot/util-crypto";
1428
1520
  function normalizeEvmAddress(addr) {
@@ -1559,6 +1651,242 @@ function addressToAccountIdHex(addr) {
1559
1651
  return substrateSs58ToAccountIdHex(addr);
1560
1652
  }
1561
1653
 
1654
+ // src/shielded-pool/pallet/ShieldedPoolModule.ts
1655
+ var ShieldedPoolModule = class {
1656
+ constructor(substrate) {
1657
+ this.substrate = substrate;
1658
+ }
1659
+ substrate;
1660
+ // ─── Extrinsics ────────────────────────────────────────────────────────────
1661
+ /**
1662
+ * Deposits tokens into the shielded pool.
1663
+ * Extrinsic: shieldedPool.shield(assetId, amount, commitment, encryptedMemo)
1664
+ *
1665
+ * Shield is always a signed (public) transaction — the caller's address
1666
+ * appears on-chain as the depositor.
1667
+ */
1668
+ async shield(params, signer, txOptions) {
1669
+ EncryptedMemo.validate(params.encryptedMemo, "shield.encryptedMemo");
1670
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "shield");
1671
+ const tx = callUnsafeTx(entry, {
1672
+ asset_id: params.assetId,
1673
+ amount: params.amount,
1674
+ commitment: Binary2.fromHex(params.commitment),
1675
+ encrypted_memo: Binary2.fromBytes(params.encryptedMemo)
1676
+ });
1677
+ return toTxResult(
1678
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1679
+ );
1680
+ }
1681
+ /**
1682
+ * Withdraws tokens from the shielded pool to a public address.
1683
+ * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
1684
+ * Pass a `signer` to fall back to signed submission (e.g. for testing).
1685
+ * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee)
1686
+ */
1687
+ async unshield(params, signer, txOptions) {
1688
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "unshield");
1689
+ const recipientSs58 = accountIdHexToSs58(params.recipientAddress);
1690
+ if (!recipientSs58) throw new Error(`Invalid recipientAddress: ${params.recipientAddress}`);
1691
+ const changeCommitment = params.changeCommitment ?? "0x" + "00".repeat(32);
1692
+ let changeEncryptedMemo;
1693
+ if (params.changeEncryptedMemo && params.changeEncryptedMemo.length > 0) {
1694
+ EncryptedMemo.validate(params.changeEncryptedMemo, "changeEncryptedMemo");
1695
+ changeEncryptedMemo = Binary2.fromBytes(params.changeEncryptedMemo);
1696
+ } else {
1697
+ changeEncryptedMemo = Binary2.fromBytes(new Uint8Array(0));
1698
+ }
1699
+ const tx = callUnsafeTx(entry, {
1700
+ proof: Binary2.fromBytes(params.proof),
1701
+ merkle_root: Binary2.fromHex(params.merkleRoot),
1702
+ nullifier: Binary2.fromHex(params.nullifier),
1703
+ asset_id: params.assetId,
1704
+ amount: params.amount,
1705
+ recipient: recipientSs58,
1706
+ fee: params.fee ?? 0n,
1707
+ change_commitment: Binary2.fromHex(changeCommitment),
1708
+ change_encrypted_memo: changeEncryptedMemo,
1709
+ relayer: void 0
1710
+ // Option<H160> — None for direct Substrate submissions
1711
+ });
1712
+ if (signer) {
1713
+ return toTxResult(
1714
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1715
+ );
1716
+ }
1717
+ return submitBareTx(tx, this.substrate);
1718
+ }
1719
+ /**
1720
+ * Performs a private (shielded) transfer between two notes.
1721
+ * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
1722
+ * Pass a `signer` to fall back to signed submission (e.g. for testing).
1723
+ * Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee)
1724
+ */
1725
+ async privateTransfer(params, signer, txOptions) {
1726
+ const nullifiers = params.inputs.map((inp) => Binary2.fromHex(inp.nullifier));
1727
+ const commitments = params.outputs.map((out) => Binary2.fromHex(out.commitment));
1728
+ const memos = params.outputs.map((out, i) => {
1729
+ EncryptedMemo.validate(
1730
+ out.encryptedMemo,
1731
+ `privateTransfer.outputs[${i}].encryptedMemo`
1732
+ );
1733
+ return Binary2.fromBytes(out.encryptedMemo);
1734
+ });
1735
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "private_transfer");
1736
+ const tx = callUnsafeTx(entry, {
1737
+ proof: Binary2.fromBytes(params.proof),
1738
+ merkle_root: Binary2.fromHex(params.merkleRoot),
1739
+ nullifiers,
1740
+ commitments,
1741
+ encrypted_memos: memos,
1742
+ asset_id: params.assetId,
1743
+ fee: params.fee ?? 0n,
1744
+ relayer: void 0
1745
+ // Option<H160> — None for direct Substrate submissions
1746
+ });
1747
+ if (signer) {
1748
+ return toTxResult(
1749
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1750
+ );
1751
+ }
1752
+ return submitBareTx(tx, this.substrate);
1753
+ }
1754
+ /**
1755
+ * Deposits multiple notes into the shielded pool in a single extrinsic.
1756
+ * Extrinsic: shieldedPool.shieldBatch(operations) — max 20 items.
1757
+ */
1758
+ async shieldBatch(params, signer, txOptions) {
1759
+ const operations = params.items.map((item, i) => {
1760
+ EncryptedMemo.validate(item.encryptedMemo, `shieldBatch.items[${i}].encryptedMemo`);
1761
+ return {
1762
+ assetId: item.assetId,
1763
+ amount: item.amount.toString(),
1764
+ commitment: Binary2.fromHex(item.commitment),
1765
+ encryptedMemo: Binary2.fromBytes(item.encryptedMemo)
1766
+ };
1767
+ });
1768
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "shield_batch");
1769
+ const tx = callUnsafeTx(entry, operations);
1770
+ return toTxResult(
1771
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1772
+ );
1773
+ }
1774
+ /**
1775
+ * Claims accrued relay fees into the shielded pool.
1776
+ * This is a SIGNED transaction — the relayer must sign it with their wallet.
1777
+ * Before calling this, generate a ZK disclosure proof with generateFeeClaimProof().
1778
+ *
1779
+ * Extrinsic: shieldedPool.claim_shielded_fees(commitment, amount, asset_id, memo, proof, public_signals)
1780
+ */
1781
+ async claimShieldedFees(params, signer, txOptions) {
1782
+ EncryptedMemo.validate(params.encryptedMemo, "claimShieldedFees.encryptedMemo");
1783
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "claim_shielded_fees");
1784
+ const tx = callUnsafeTx(entry, {
1785
+ commitment: Binary2.fromHex(params.commitment),
1786
+ amount: params.amount,
1787
+ asset_id: params.assetId,
1788
+ encrypted_memo: Binary2.fromBytes(params.encryptedMemo),
1789
+ proof: Binary2.fromBytes(params.proof),
1790
+ public_signals: Binary2.fromBytes(params.publicSignals)
1791
+ });
1792
+ return toTxResult(
1793
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1794
+ );
1795
+ }
1796
+ // ─── Selective Disclosure ──────────────────────────────────────────────────
1797
+ /**
1798
+ * Requests a selective disclosure from a target account for a specific commitment.
1799
+ * The auditor's Baby Jubjub public key is included so the note owner knows
1800
+ * which key to encrypt to when generating the proof.
1801
+ * Extrinsic: shieldedPool.request_disclosure(target, commitment, required_fields,
1802
+ * reason, auditor_bjj_pk_x, auditor_bjj_pk_y)
1803
+ */
1804
+ async requestDisclosure(params, signer, txOptions) {
1805
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "request_disclosure");
1806
+ const tx = callUnsafeTx(entry, {
1807
+ target: params.target,
1808
+ commitment: Binary2.fromBytes(new Uint8Array(params.commitment)),
1809
+ required_fields: {
1810
+ value: params.requiredFields.value,
1811
+ asset_id: params.requiredFields.assetId,
1812
+ owner: params.requiredFields.owner
1813
+ },
1814
+ reason: Binary2.fromText(params.reason),
1815
+ auditor_bjj_pk_x: Binary2.fromBytes(new Uint8Array(params.auditorBjjPkX)),
1816
+ auditor_bjj_pk_y: Binary2.fromBytes(new Uint8Array(params.auditorBjjPkY))
1817
+ });
1818
+ return toTxResult(
1819
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1820
+ );
1821
+ }
1822
+ /**
1823
+ * Submits a Groth16 ZK disclosure proof for a note commitment.
1824
+ * The proof reveals the selected fields (value, asset_id, owner_hash) on-chain.
1825
+ * Use generateDisclosureProof() + buildDisclosurePublicSignals() before calling this.
1826
+ * Extrinsic: shieldedPool.disclose(commitment, proof_bytes, public_signals, auditor)
1827
+ */
1828
+ async disclose(params, signer, txOptions) {
1829
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "disclose");
1830
+ const tx = callUnsafeTx(entry, {
1831
+ commitment: Binary2.fromBytes(new Uint8Array(params.commitment)),
1832
+ proof_bytes: Binary2.fromBytes(new Uint8Array(params.proofBytes)),
1833
+ public_signals: Binary2.fromBytes(new Uint8Array(params.publicSignals)),
1834
+ auditor: params.auditor
1835
+ });
1836
+ return toTxResult(
1837
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1838
+ );
1839
+ }
1840
+ /**
1841
+ * Rejects a pending disclosure request from an auditor for a specific commitment.
1842
+ * Extrinsic: shieldedPool.reject_disclosure(auditor, commitment, reason)
1843
+ */
1844
+ async rejectDisclosure(params, signer, txOptions) {
1845
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "reject_disclosure");
1846
+ const tx = callUnsafeTx(entry, {
1847
+ auditor: params.auditor,
1848
+ commitment: Binary2.fromBytes(new Uint8Array(params.commitment)),
1849
+ reason: Binary2.fromText(params.reason)
1850
+ });
1851
+ return toTxResult(
1852
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1853
+ );
1854
+ }
1855
+ /**
1856
+ * Cleans up a disclosure request that has passed its expiration block.
1857
+ * Permissionless — any account can prune expired requests.
1858
+ * Extrinsic: shieldedPool.prune_expired_request(target, auditor, commitment)
1859
+ */
1860
+ async pruneExpiredRequest(params, signer, txOptions) {
1861
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "prune_expired_request");
1862
+ const tx = callUnsafeTx(entry, {
1863
+ target: params.target,
1864
+ auditor: params.auditor,
1865
+ commitment: Binary2.fromBytes(new Uint8Array(params.commitment))
1866
+ });
1867
+ return toTxResult(
1868
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1869
+ );
1870
+ }
1871
+ /**
1872
+ * Revokes a previously submitted voluntary disclosure record.
1873
+ * Only applies to self-disclosures (auditor = None). Auditor-requested records are permanent.
1874
+ * Extrinsic: shieldedPool.revoke_disclosure_record(commitment)
1875
+ */
1876
+ async revokeDisclosureRecord(params, signer, txOptions) {
1877
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "revoke_disclosure_record");
1878
+ const tx = callUnsafeTx(entry, {
1879
+ commitment: Binary2.fromBytes(new Uint8Array(params.commitment))
1880
+ });
1881
+ return toTxResult(
1882
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1883
+ );
1884
+ }
1885
+ };
1886
+
1887
+ // src/account-mapping/AccountMappingModule.ts
1888
+ import { Binary as Binary3 } from "polkadot-api";
1889
+
1562
1890
  // src/account-mapping/helpers.ts
1563
1891
  function mapRawScheme(raw) {
1564
1892
  if (raw === "Eip191" || raw === "eip191") return "Eip191";
@@ -1571,6 +1899,7 @@ var AccountMappingModule = class {
1571
1899
  constructor(substrate) {
1572
1900
  this.substrate = substrate;
1573
1901
  }
1902
+ substrate;
1574
1903
  // ─── Address resolution ─────────────────────────────────────────────────────
1575
1904
  /**
1576
1905
  * Returns the explicitly mapped (or fallback) Substrate AccountId32 hex for
@@ -1956,6 +2285,7 @@ var PrivacyModule = class {
1956
2285
  constructor(substrate) {
1957
2286
  this.substrate = substrate;
1958
2287
  }
2288
+ substrate;
1959
2289
  /** Returns the current Merkle tree root. */
1960
2290
  async getMerkleRoot() {
1961
2291
  return this.substrate.request("privacy_getMerkleRoot", []);
@@ -1973,14 +2303,23 @@ var PrivacyModule = class {
1973
2303
  }
1974
2304
  /**
1975
2305
  * Returns the Merkle inclusion proof for a given commitment hex,
1976
- * bundled with the current Merkle root.
2306
+ * bundled with the Merkle root.
2307
+ *
2308
+ * Uses `privacy_getMerkleProofByCommitment` which resolves root and proof
2309
+ * under the **same block hash**, guaranteeing that the returned path is
2310
+ * consistent with the returned root.
1977
2311
  */
1978
2312
  async getMerkleProofByCommitment(commitmentHex) {
1979
- const [proof, root] = await Promise.all([
1980
- this.getMerkleProof(commitmentHex),
1981
- this.getMerkleRoot()
1982
- ]);
1983
- return { ...proof, root };
2313
+ const raw = await this.substrate.request(
2314
+ "privacy_getMerkleProofByCommitment",
2315
+ [commitmentHex]
2316
+ );
2317
+ return {
2318
+ path: raw.path,
2319
+ leafIndex: raw.leaf_index,
2320
+ treeDepth: raw.tree_depth,
2321
+ root: raw.root
2322
+ };
1984
2323
  }
1985
2324
  /** Returns the spend status of a nullifier. */
1986
2325
  async getNullifierStatus(nullifier) {
@@ -1999,6 +2338,7 @@ var PrivacyModule = class {
1999
2338
  return {
2000
2339
  merkleRoot: raw.merkle_root,
2001
2340
  commitmentCount: raw.commitment_count,
2341
+ nullifierCount: raw.nullifier_count,
2002
2342
  totalBalance: raw.total_balance.toString(),
2003
2343
  assetBalances: raw.asset_balances.map(mapAssetBalance),
2004
2344
  treeDepth: raw.tree_depth
@@ -2024,6 +2364,7 @@ var ZkVerifierModule = class {
2024
2364
  constructor(substrate) {
2025
2365
  this.substrate = substrate;
2026
2366
  }
2367
+ substrate;
2027
2368
  /** Returns basic version info for all registered circuits. */
2028
2369
  async getAllCircuitVersions() {
2029
2370
  const raw = await this.substrate.request(
@@ -2042,6 +2383,48 @@ var ZkVerifierModule = class {
2042
2383
  }
2043
2384
  };
2044
2385
 
2386
+ // src/relayer/RelayerStatusModule.ts
2387
+ var RelayerStatusModule = class {
2388
+ constructor(substrate) {
2389
+ this.substrate = substrate;
2390
+ }
2391
+ substrate;
2392
+ /**
2393
+ * Returns true if the given SS58 address is a registered relayer.
2394
+ */
2395
+ async isRelayer(ss58Address) {
2396
+ return this.substrate.request("relayer_isRelayer", [ss58Address]);
2397
+ }
2398
+ /**
2399
+ * Returns the pending fees (in planck) for the given account and asset.
2400
+ * The node returns the value as a decimal string to avoid u128 overflow.
2401
+ */
2402
+ async pendingFees(ss58Address, assetId) {
2403
+ const raw = await this.substrate.request("relayer_pendingFees", [
2404
+ ss58Address,
2405
+ assetId
2406
+ ]);
2407
+ return BigInt(raw);
2408
+ }
2409
+ /**
2410
+ * Returns the registered EVM address (0x-prefixed) for the given account,
2411
+ * or null if the account is not a registered relayer.
2412
+ */
2413
+ async registeredEvmAddress(ss58Address) {
2414
+ return this.substrate.request("relayer_registeredEvmAddress", [ss58Address]);
2415
+ }
2416
+ /**
2417
+ * Convenience method: returns relayer registry info for an account.
2418
+ */
2419
+ async getRelayerInfo(ss58Address) {
2420
+ const [isRelayer, evmAddress] = await Promise.all([
2421
+ this.isRelayer(ss58Address),
2422
+ this.registeredEvmAddress(ss58Address)
2423
+ ]);
2424
+ return { isRelayer, evmAddress };
2425
+ }
2426
+ };
2427
+
2045
2428
  // src/precompiles/helpers.ts
2046
2429
  var STATIC_TYPES = /* @__PURE__ */ new Set(["uint", "bytes32", "address", "bool"]);
2047
2430
  function concat(arrays) {
@@ -2185,10 +2568,6 @@ function decodeBytes(data, slotOffset = 0) {
2185
2568
  function decodeString(data, slotOffset = 0) {
2186
2569
  return new TextDecoder().decode(decodeBytes(data, slotOffset));
2187
2570
  }
2188
- function hexToBytes(hex) {
2189
- if (hex === "0x" || hex === "") return new Uint8Array(0);
2190
- return fromHex(hex.startsWith("0x") ? hex : "0x" + hex);
2191
- }
2192
2571
 
2193
2572
  // src/precompiles/addresses.ts
2194
2573
  var PRECOMPILE_ADDR = {
@@ -2247,12 +2626,20 @@ var AM_SEL = {
2247
2626
  SET_ACCOUNT_METADATA: new Uint8Array([119, 108, 249, 255])
2248
2627
  };
2249
2628
  var SP_SEL = {
2250
- // shield(uint32,uint256,bytes32,bytes) 0x781442b9
2251
- SHIELD: new Uint8Array([120, 20, 66, 185]),
2252
- // privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[]) 0xdcd5b898
2253
- PRIVATE_TRANSFER: new Uint8Array([220, 213, 184, 152]),
2254
- // unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32) 0xdcf1bff2
2255
- UNSHIELD: new Uint8Array([220, 241, 191, 242])
2629
+ // shield(uint32,bytes32,bytes) 0x9feb22ea (payable, amount = msg.value)
2630
+ SHIELD: new Uint8Array([159, 235, 34, 234]),
2631
+ // privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256) 0x8c0f5d24
2632
+ PRIVATE_TRANSFER: new Uint8Array([140, 15, 93, 36]),
2633
+ // unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32) 0xd21d9a79
2634
+ UNSHIELD: new Uint8Array([210, 29, 154, 121]),
2635
+ // requestDisclosure(bytes32,bytes32,bool,bool,bool,bytes,bytes32,bytes32) → 0xe7022933 (caller = auditor)
2636
+ REQUEST_DISCLOSURE: new Uint8Array([231, 2, 41, 51]),
2637
+ // disclose(bytes32,bytes,bytes,bytes32) → 0xea8a4165 (caller = note owner)
2638
+ DISCLOSE: new Uint8Array([234, 138, 65, 101]),
2639
+ // rejectDisclosure(bytes32,bytes32,bytes) → 0x72b895a9 (caller = target)
2640
+ REJECT_DISCLOSURE: new Uint8Array([114, 184, 149, 169]),
2641
+ // pruneExpiredRequest(bytes32,bytes32,bytes32) → 0x0c338dcf (permissionless)
2642
+ PRUNE_EXPIRED_REQUEST: new Uint8Array([12, 51, 141, 207])
2256
2643
  };
2257
2644
  var KNOWN_PRECOMPILES = {
2258
2645
  // ── Ethereum standard (EIP) ─────────────────────────────────────────────
@@ -2295,9 +2682,13 @@ var KNOWN_PRECOMPILES = {
2295
2682
  "0x0000000000000000000000000000000000000801": {
2296
2683
  name: "ShieldedPool",
2297
2684
  functions: {
2298
- "781442b9": "shield(uint32,uint256,bytes32,bytes)",
2299
- dcd5b898: "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[])",
2300
- dcf1bff2: "unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32)"
2685
+ "9feb22ea": "shield(uint32,bytes32,bytes)",
2686
+ "8c0f5d24": "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256)",
2687
+ d21d9a79: "unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)",
2688
+ e7022933: "requestDisclosure(bytes32,bytes32,bool,bool,bool,bytes,bytes32,bytes32)",
2689
+ ea8a4165: "disclose(bytes32,bytes,bytes,bytes32)",
2690
+ "72b895a9": "rejectDisclosure(bytes32,bytes32,bytes)",
2691
+ "0c338dcf": "pruneExpiredRequest(bytes32,bytes32,bytes32)"
2301
2692
  }
2302
2693
  }
2303
2694
  };
@@ -2311,43 +2702,58 @@ var ShieldedPoolPrecompile = class {
2311
2702
  constructor(evm) {
2312
2703
  this.evm = evm;
2313
2704
  }
2705
+ evm;
2314
2706
  addr = PRECOMPILE_ADDR.SHIELDED_POOL;
2315
2707
  // ─── shield ────────────────────────────────────────────────────────────────
2316
2708
  /**
2317
- * Returns the ABI-encoded calldata for `shield(uint32, uint256, bytes32, bytes)`.
2318
- * Useful when you need to inspect or batch the calldata before sending.
2709
+ * Returns the ABI-encoded calldata for `shield(uint32, bytes32, bytes)`.
2710
+ * The token amount must be sent as `msg.value` (the `value` field of the EVM
2711
+ * transaction) — this is what MetaMask and other wallets display to the user.
2319
2712
  */
2320
2713
  buildShieldCalldata(params) {
2321
- const memo = params.encryptedMemo ?? EncryptedMemo.dummy();
2714
+ EncryptedMemo.validate(params.encryptedMemo, "buildShieldCalldata.encryptedMemo");
2322
2715
  const commitment = fromHex(params.commitment);
2323
2716
  return encodeHex(
2324
2717
  SP_SEL.SHIELD,
2325
2718
  { type: "uint", value: BigInt(params.assetId) },
2326
- { type: "uint", value: params.amount },
2327
2719
  { type: "bytes32", value: commitment },
2328
- { type: "bytes", value: memo }
2720
+ { type: "bytes", value: params.encryptedMemo }
2329
2721
  );
2330
2722
  }
2331
2723
  /**
2332
- * Deposits tokens into the shielded pool from an EVM transaction.
2724
+ * Deposits tokens into the shielded pool from a payable EVM transaction.
2333
2725
  *
2334
- * The EVM caller's address is deterministically mapped to a Substrate
2335
- * AccountId32 (`H160 ++ [0x00; 12]`). The pool deducts from that account.
2726
+ * The token amount is sent as `msg.value` so EVM wallets (MetaMask, etc.) display
2727
+ * the correct amount on the confirmation screen. The precompile dispatches
2728
+ * `shieldedPool.shield` with its own address as origin, so the funds flow:
2729
+ * caller → precompile (via msg.value, handled by EVM)
2730
+ * precompile → pool (via pallet transfer)
2731
+ * This avoids double-deduction while keeping the displayed amount accurate.
2336
2732
  *
2337
2733
  * Extrinsic: `shieldedPool.shield(assetId, amount, commitment, encryptedMemo)`
2338
2734
  */
2339
2735
  async shield(params, signer) {
2340
- return signer({ to: this.addr, data: this.buildShieldCalldata(params) });
2736
+ return signer({
2737
+ to: this.addr,
2738
+ data: this.buildShieldCalldata(params),
2739
+ value: params.amount
2740
+ });
2341
2741
  }
2342
2742
  // ─── privateTransfer ───────────────────────────────────────────────────────
2343
2743
  /**
2344
2744
  * Returns the ABI-encoded calldata for
2345
- * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[])`.
2745
+ * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[], uint32, uint256)`.
2346
2746
  */
2347
2747
  buildPrivateTransferCalldata(params) {
2348
2748
  const nullifiers = params.inputs.map((i) => fromHex(i.nullifier));
2349
2749
  const commitments = params.outputs.map((o) => fromHex(o.commitment));
2350
- const memos = params.outputs.map((o) => o.encryptedMemo ?? EncryptedMemo.dummy());
2750
+ const memos = params.outputs.map((o, i) => {
2751
+ EncryptedMemo.validate(
2752
+ o.encryptedMemo,
2753
+ `buildPrivateTransferCalldata.outputs[${i}].encryptedMemo`
2754
+ );
2755
+ return o.encryptedMemo;
2756
+ });
2351
2757
  const root = fromHex(params.merkleRoot);
2352
2758
  return encodeHex(
2353
2759
  SP_SEL.PRIVATE_TRANSFER,
@@ -2355,7 +2761,9 @@ var ShieldedPoolPrecompile = class {
2355
2761
  { type: "bytes32", value: root },
2356
2762
  { type: "bytes32[]", value: nullifiers },
2357
2763
  { type: "bytes32[]", value: commitments },
2358
- { type: "bytes[]", value: memos }
2764
+ { type: "bytes[]", value: memos },
2765
+ { type: "uint", value: BigInt(params.assetId) },
2766
+ { type: "uint", value: params.fee ?? 0n }
2359
2767
  );
2360
2768
  }
2361
2769
  /**
@@ -2383,6 +2791,9 @@ var ShieldedPoolPrecompile = class {
2383
2791
  const recipientBytes = fromHex(
2384
2792
  "0x" + (recipientRaw.length === 64 ? recipientRaw : recipientRaw.padEnd(64, "0"))
2385
2793
  );
2794
+ const changeCommitmentHex = params.changeCommitment ?? "0x" + "00".repeat(32);
2795
+ const changeCommitment = fromHex(changeCommitmentHex);
2796
+ const changeEncryptedMemo = params.changeEncryptedMemo ?? new Uint8Array();
2386
2797
  return encodeHex(
2387
2798
  SP_SEL.UNSHIELD,
2388
2799
  { type: "bytes", value: proof },
@@ -2390,7 +2801,10 @@ var ShieldedPoolPrecompile = class {
2390
2801
  { type: "bytes32", value: nullifier },
2391
2802
  { type: "uint", value: BigInt(params.assetId) },
2392
2803
  { type: "uint", value: params.amount },
2393
- { type: "bytes32", value: recipientBytes }
2804
+ { type: "bytes32", value: recipientBytes },
2805
+ { type: "uint", value: params.fee ?? 0n },
2806
+ { type: "bytes32", value: changeCommitment },
2807
+ { type: "bytes", value: changeEncryptedMemo }
2394
2808
  );
2395
2809
  }
2396
2810
  /**
@@ -2418,24 +2832,151 @@ var ShieldedPoolPrecompile = class {
2418
2832
  });
2419
2833
  }
2420
2834
  /**
2421
- * Estimates the EVM gas for a `privateTransfer` call.
2835
+ * Estimates the EVM gas for a `privateTransfer` call.
2836
+ */
2837
+ async estimatePrivateTransferGas(params, from) {
2838
+ return this.evm.estimateGas({
2839
+ from,
2840
+ to: this.addr,
2841
+ data: this.buildPrivateTransferCalldata(params)
2842
+ });
2843
+ }
2844
+ /**
2845
+ * Estimates the EVM gas for an `unshield` call.
2846
+ */
2847
+ async estimateUnshieldGas(params, from) {
2848
+ return this.evm.estimateGas({
2849
+ from,
2850
+ to: this.addr,
2851
+ data: this.buildUnshieldCalldata(params)
2852
+ });
2853
+ }
2854
+ // ─── requestDisclosure ─────────────────────────────────────────────────────
2855
+ /**
2856
+ * Returns the ABI-encoded calldata for
2857
+ * `requestDisclosure(bytes32,bytes32,bool,bool,bool,bytes,bytes32,bytes32)`.
2858
+ *
2859
+ * The **EVM caller** of the resulting transaction is treated as the **auditor**
2860
+ * on-chain. No explicit auditor argument is needed.
2861
+ */
2862
+ buildRequestDisclosureCalldata(params) {
2863
+ if (params.auditorBjjPkX.length !== 32)
2864
+ throw new RangeError("requestDisclosure: auditorBjjPkX must be 32 bytes");
2865
+ if (params.auditorBjjPkY.length !== 32)
2866
+ throw new RangeError("requestDisclosure: auditorBjjPkY must be 32 bytes");
2867
+ const target = fromHex(params.target);
2868
+ const commitment = fromHex(params.commitment);
2869
+ const reasonBytes = new TextEncoder().encode(params.reason);
2870
+ return encodeHex(
2871
+ SP_SEL.REQUEST_DISCLOSURE,
2872
+ { type: "bytes32", value: target },
2873
+ { type: "bytes32", value: commitment },
2874
+ { type: "bool", value: params.disclosedValue },
2875
+ { type: "bool", value: params.disclosedAssetId },
2876
+ { type: "bool", value: params.disclosedOwner },
2877
+ { type: "bytes", value: reasonBytes },
2878
+ { type: "bytes32", value: params.auditorBjjPkX },
2879
+ { type: "bytes32", value: params.auditorBjjPkY }
2880
+ );
2881
+ }
2882
+ /**
2883
+ * Requests selective disclosure of a specific commitment.
2884
+ *
2885
+ * The EVM caller is recorded as the auditor on-chain. The note owner can
2886
+ * respond with `disclose()` or reject with `rejectDisclosure()`.
2887
+ *
2888
+ * Extrinsic: `shieldedPool.requestDisclosure(target, commitment, requiredFields, reason, bjjPkX, bjjPkY)`
2889
+ */
2890
+ async requestDisclosure(params, signer) {
2891
+ return signer({ to: this.addr, data: this.buildRequestDisclosureCalldata(params) });
2892
+ }
2893
+ // ─── disclose ──────────────────────────────────────────────────────────────
2894
+ /**
2895
+ * Returns the ABI-encoded calldata for `disclose(bytes32,bytes,bytes,bytes32)`.
2896
+ *
2897
+ * The **EVM caller** is treated as the **note owner** on-chain.
2898
+ * `params.proofBytes` must be exactly 128 bytes; `params.publicSignals` exactly 256 bytes.
2899
+ */
2900
+ buildDiscloseCalldata(params) {
2901
+ if (params.proofBytes.length !== 128)
2902
+ throw new RangeError("disclose: proofBytes must be exactly 128 bytes");
2903
+ if (params.publicSignals.length !== 256)
2904
+ throw new RangeError("disclose: publicSignals must be exactly 256 bytes");
2905
+ const commitment = fromHex(params.commitment);
2906
+ const auditor = fromHex(params.auditor);
2907
+ return encodeHex(
2908
+ SP_SEL.DISCLOSE,
2909
+ { type: "bytes32", value: commitment },
2910
+ { type: "bytes", value: params.proofBytes },
2911
+ { type: "bytes", value: params.publicSignals },
2912
+ { type: "bytes32", value: auditor }
2913
+ );
2914
+ }
2915
+ /**
2916
+ * Submits a selective disclosure proof for a commitment.
2917
+ *
2918
+ * The EVM caller is treated as the note owner on-chain. The Groth16 proof is
2919
+ * verified by the runtime; on success the encrypted signals are stored for
2920
+ * the auditor to decrypt off-chain.
2921
+ *
2922
+ * Extrinsic: `shieldedPool.disclose(commitment, proofBytes, publicSignals, auditor)`
2923
+ */
2924
+ async disclose(params, signer) {
2925
+ return signer({ to: this.addr, data: this.buildDiscloseCalldata(params) });
2926
+ }
2927
+ // ─── rejectDisclosure ──────────────────────────────────────────────────────
2928
+ /**
2929
+ * Returns the ABI-encoded calldata for `rejectDisclosure(bytes32,bytes32,bytes)`.
2930
+ *
2931
+ * The **EVM caller** is treated as the **target** (note owner) on-chain.
2932
+ */
2933
+ buildRejectDisclosureCalldata(params) {
2934
+ const auditor = fromHex(params.auditor);
2935
+ const commitment = fromHex(params.commitment);
2936
+ const reasonBytes = new TextEncoder().encode(params.reason);
2937
+ return encodeHex(
2938
+ SP_SEL.REJECT_DISCLOSURE,
2939
+ { type: "bytes32", value: auditor },
2940
+ { type: "bytes32", value: commitment },
2941
+ { type: "bytes", value: reasonBytes }
2942
+ );
2943
+ }
2944
+ /**
2945
+ * Rejects a pending disclosure request.
2946
+ *
2947
+ * The EVM caller is treated as the note owner (target) on-chain.
2948
+ *
2949
+ * Extrinsic: `shieldedPool.rejectDisclosure(auditor, commitment, reason)`
2950
+ */
2951
+ async rejectDisclosure(params, signer) {
2952
+ return signer({ to: this.addr, data: this.buildRejectDisclosureCalldata(params) });
2953
+ }
2954
+ // ─── pruneExpiredRequest ───────────────────────────────────────────────────
2955
+ /**
2956
+ * Returns the ABI-encoded calldata for `pruneExpiredRequest(bytes32,bytes32,bytes32)`.
2957
+ *
2958
+ * Permissionless: any EVM caller can prune an expired request.
2422
2959
  */
2423
- async estimatePrivateTransferGas(params, from) {
2424
- return this.evm.estimateGas({
2425
- from,
2426
- to: this.addr,
2427
- data: this.buildPrivateTransferCalldata(params)
2428
- });
2960
+ buildPruneExpiredRequestCalldata(params) {
2961
+ const target = fromHex(params.target);
2962
+ const auditor = fromHex(params.auditor);
2963
+ const commitment = fromHex(params.commitment);
2964
+ return encodeHex(
2965
+ SP_SEL.PRUNE_EXPIRED_REQUEST,
2966
+ { type: "bytes32", value: target },
2967
+ { type: "bytes32", value: auditor },
2968
+ { type: "bytes32", value: commitment }
2969
+ );
2429
2970
  }
2430
2971
  /**
2431
- * Estimates the EVM gas for an `unshield` call.
2972
+ * Removes an expired disclosure request from storage.
2973
+ *
2974
+ * Permissionless: any EVM account can call this once `expires_at` has passed.
2975
+ *
2976
+ * Extrinsic: `shieldedPool.pruneExpiredRequest(target, auditor, commitment)`
2432
2977
  */
2433
- async estimateUnshieldGas(params, from) {
2434
- return this.evm.estimateGas({
2435
- from,
2436
- to: this.addr,
2437
- data: this.buildUnshieldCalldata(params)
2438
- });
2978
+ async pruneExpiredRequest(params, signer) {
2979
+ return signer({ to: this.addr, data: this.buildPruneExpiredRequestCalldata(params) });
2439
2980
  }
2440
2981
  };
2441
2982
 
@@ -2444,6 +2985,7 @@ var AccountMappingPrecompile = class {
2444
2985
  constructor(evm) {
2445
2986
  this.evm = evm;
2446
2987
  }
2988
+ evm;
2447
2989
  addr = PRECOMPILE_ADDR.ACCOUNT_MAPPING;
2448
2990
  // ─── Read-only ─────────────────────────────────────────────────────────────
2449
2991
  /**
@@ -2455,7 +2997,7 @@ var AccountMappingPrecompile = class {
2455
2997
  async resolveAlias(alias) {
2456
2998
  try {
2457
2999
  const data = encodeHex(AM_SEL.RESOLVE_ALIAS, { type: "string", value: alias });
2458
- const raw = hexToBytes(await this.evm.call(this.addr, data));
3000
+ const raw = fromHex(await this.evm.call(this.addr, data));
2459
3001
  if (raw.length < 64) return null;
2460
3002
  const owner = decodeAddress2(raw, 0);
2461
3003
  const evm = decodeAddress2(raw, 32);
@@ -2478,7 +3020,7 @@ var AccountMappingPrecompile = class {
2478
3020
  type: "address",
2479
3021
  value: normalizeEvmAddress(evmAddress)
2480
3022
  });
2481
- const raw = hexToBytes(await this.evm.call(this.addr, data));
3023
+ const raw = fromHex(await this.evm.call(this.addr, data));
2482
3024
  if (raw.length === 0) return null;
2483
3025
  const alias = decodeString(raw, 0);
2484
3026
  return alias.length > 0 ? alias : null;
@@ -2498,7 +3040,7 @@ var AccountMappingPrecompile = class {
2498
3040
  { type: "string", value: alias },
2499
3041
  { type: "bytes32", value: commitmentBytes }
2500
3042
  );
2501
- const raw = hexToBytes(await this.evm.call(this.addr, data));
3043
+ const raw = fromHex(await this.evm.call(this.addr, data));
2502
3044
  if (raw.length < 32) return false;
2503
3045
  return decodeBool(raw, 0);
2504
3046
  } catch {
@@ -2703,6 +3245,7 @@ var CryptoPrecompiles = class {
2703
3245
  constructor(evm) {
2704
3246
  this.evm = evm;
2705
3247
  }
3248
+ evm;
2706
3249
  // ─── ECRecover (0x0001) ───────────────────────────────────────────────────
2707
3250
  /**
2708
3251
  * Recovers the Ethereum address from an ECDSA signature.
@@ -2718,7 +3261,7 @@ var CryptoPrecompiles = class {
2718
3261
  input[63] = v;
2719
3262
  input.set(r.slice(0, 32), 64);
2720
3263
  input.set(s.slice(0, 32), 96);
2721
- const raw = hexToBytes(await this.evm.call(PRECOMPILE_ADDR.EC_RECOVER, toHex(input)));
3264
+ const raw = fromHex(await this.evm.call(PRECOMPILE_ADDR.EC_RECOVER, toHex(input)));
2722
3265
  if (raw.length < 32) return "0x" + "00".repeat(20);
2723
3266
  return "0x" + toHex(raw.slice(12, 32)).slice(2);
2724
3267
  }
@@ -2734,7 +3277,7 @@ var CryptoPrecompiles = class {
2734
3277
  input[63] = v;
2735
3278
  input.set(r.slice(0, 32), 64);
2736
3279
  input.set(s.slice(0, 32), 96);
2737
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.EC_RECOVER_PUBKEY, toHex(input)));
3280
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.EC_RECOVER_PUBKEY, toHex(input)));
2738
3281
  }
2739
3282
  // ─── SHA-256 (0x0002) ─────────────────────────────────────────────────────
2740
3283
  /**
@@ -2742,7 +3285,7 @@ var CryptoPrecompiles = class {
2742
3285
  * Returns a 32-byte digest.
2743
3286
  */
2744
3287
  async sha256(data) {
2745
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.SHA256, toHex(data)));
3288
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.SHA256, toHex(data)));
2746
3289
  }
2747
3290
  // ─── RIPEMD-160 (0x0003) ──────────────────────────────────────────────────
2748
3291
  /**
@@ -2750,7 +3293,7 @@ var CryptoPrecompiles = class {
2750
3293
  * Returns the 20-byte digest right-padded to 32 bytes (standard ABI output).
2751
3294
  */
2752
3295
  async ripemd160(data) {
2753
- const raw = hexToBytes(await this.evm.call(PRECOMPILE_ADDR.RIPEMD160, toHex(data)));
3296
+ const raw = fromHex(await this.evm.call(PRECOMPILE_ADDR.RIPEMD160, toHex(data)));
2754
3297
  return raw.length >= 32 ? raw.slice(12, 32) : raw;
2755
3298
  }
2756
3299
  // ─── Identity (0x0004) ────────────────────────────────────────────────────
@@ -2759,7 +3302,7 @@ var CryptoPrecompiles = class {
2759
3302
  * Mainly useful for gas benchmarking.
2760
3303
  */
2761
3304
  async identity(data) {
2762
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.IDENTITY, toHex(data)));
3305
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.IDENTITY, toHex(data)));
2763
3306
  }
2764
3307
  // ─── SHA3-FIPS-256 / Keccak-256 (0x0400) ─────────────────────────────────
2765
3308
  /**
@@ -2767,7 +3310,7 @@ var CryptoPrecompiles = class {
2767
3310
  * Returns a 32-byte digest.
2768
3311
  */
2769
3312
  async keccak256(data) {
2770
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.SHA3_FIPS256, toHex(data)));
3313
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.SHA3_FIPS256, toHex(data)));
2771
3314
  }
2772
3315
  // ─── Curve25519 / Ristretto (0x0402, 0x0403) ─────────────────────────────
2773
3316
  /**
@@ -2790,7 +3333,7 @@ var CryptoPrecompiles = class {
2790
3333
  }
2791
3334
  input.set(pt, i * 32);
2792
3335
  }
2793
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.CURVE25519_ADD, toHex(input)));
3336
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.CURVE25519_ADD, toHex(input)));
2794
3337
  }
2795
3338
  /**
2796
3339
  * Multiplies a Ristretto compressed point by a scalar via EVM precompile.
@@ -2806,39 +3349,44 @@ var CryptoPrecompiles = class {
2806
3349
  const input = new Uint8Array(64);
2807
3350
  input.set(scalar, 0);
2808
3351
  input.set(point, 32);
2809
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.CURVE25519_SCALAR_MUL, toHex(input)));
3352
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.CURVE25519_SCALAR_MUL, toHex(input)));
2810
3353
  }
2811
3354
  };
2812
3355
 
2813
3356
  // src/client/OrbinumClient.ts
2814
3357
  var OrbinumClient = class _OrbinumClient {
2815
- /** Raw access to the Substrate WebSocket connection and RPC. */
3358
+ /** Raw Substrate WebSocket connection use for custom RPC calls or low-level access. */
2816
3359
  substrate;
2817
- /** Raw access to the EVM HTTP JSON-RPC endpoint (if configured). */
3360
+ /** Raw EVM HTTP JSON-RPC client. `null` when `evmRpc` is not configured. */
2818
3361
  evm;
2819
3362
  /**
2820
- * High-level EVM block and transaction explorer (if `evmRpc` is configured).
3363
+ * High-level EVM block and transaction explorer.
2821
3364
  * Provides enriched queries for blocks, transactions, addresses, and token transfers.
3365
+ * `null` when `evmRpc` is not configured.
2822
3366
  */
2823
3367
  evmExplorer;
2824
3368
  /**
2825
- * HTTP client for the Orbinum indexer REST API (if `indexerUrl` is configured).
3369
+ * HTTP client for the Orbinum indexer REST API.
2826
3370
  * Provides paginated access to indexed blocks, extrinsics, shielded events, and nullifiers.
3371
+ * `null` when `indexerUrl` is not configured.
2827
3372
  */
2828
3373
  indexer;
2829
- /** Shielded-pool operations: shield, unshield, privateTransfer, and merkle queries. */
3374
+ /** Shielded-pool extrinsics and Merkle tree queries (`shield`, `unshield`, `privateTransfer`, …). */
2830
3375
  shieldedPool;
2831
- /** Account mapping: aliases, chain links, metadata, marketplace, and identity extrinsics. */
3376
+ /** Account-mapping extrinsics: aliases, chain links, metadata, marketplace, and identity. */
2832
3377
  accountMapping;
2833
- /** Typed access to Orbinum `privacy_*` RPC endpoints. */
3378
+ /** Typed access to `privacy_*` custom RPC endpoints. */
2834
3379
  privacy;
2835
- /** Typed access to zkVerifier_* RPC endpoints. */
3380
+ /** Typed access to `zkVerifier_*` custom RPC endpoints. */
2836
3381
  zkVerifier;
3382
+ /** Typed access to `relayer_*` RPC endpoints (registry lookup and pending fee queries). */
3383
+ relayerStatus;
2837
3384
  /**
2838
- * EVM precompiles: shielded pool + account mapping callable from an EVM wallet.
2839
- * Only available when `evmRpc` is configured. Methods throw if `evm` is null.
3385
+ * Precompile modules for interacting with Orbinum contracts from an EVM wallet.
3386
+ * `null` when `evmRpc` is not configured. Methods on each sub-module throw if `evm` is `null`.
2840
3387
  */
2841
3388
  precompiles;
3389
+ /** @internal Use `OrbinumClient.connect()` to obtain an instance. */
2842
3390
  constructor(substrate, evm, indexer) {
2843
3391
  this.substrate = substrate;
2844
3392
  this.evm = evm;
@@ -2848,6 +3396,7 @@ var OrbinumClient = class _OrbinumClient {
2848
3396
  this.accountMapping = new AccountMappingModule(substrate);
2849
3397
  this.privacy = new PrivacyModule(substrate);
2850
3398
  this.zkVerifier = new ZkVerifierModule(substrate);
3399
+ this.relayerStatus = new RelayerStatusModule(substrate);
2851
3400
  this.precompiles = evm ? {
2852
3401
  shieldedPool: new ShieldedPoolPrecompile(evm),
2853
3402
  accountMapping: new AccountMappingPrecompile(evm),
@@ -2855,8 +3404,10 @@ var OrbinumClient = class _OrbinumClient {
2855
3404
  } : null;
2856
3405
  }
2857
3406
  /**
2858
- * Connects to an Orbinum node and returns a ready-to-use `OrbinumClient`.
2859
- * Throws if the Substrate node is unreachable within `connectTimeoutMs`.
3407
+ * Creates and connects an `OrbinumClient` from the given configuration.
3408
+ *
3409
+ * Establishes the Substrate WebSocket connection and, if configured, instantiates
3410
+ * the EVM and indexer clients. Throws if the node is unreachable within `connectTimeoutMs`.
2860
3411
  */
2861
3412
  static async connect(config) {
2862
3413
  const substrate = await SubstrateClient.connect(
@@ -2867,7 +3418,7 @@ var OrbinumClient = class _OrbinumClient {
2867
3418
  const indexer = config.indexerUrl ? new IndexerClient({ baseUrl: config.indexerUrl }) : null;
2868
3419
  return new _OrbinumClient(substrate, evm, indexer);
2869
3420
  }
2870
- /** Closes the WebSocket connection to the Substrate node. */
3421
+ /** Closes the underlying Substrate WebSocket connection and releases all resources. */
2871
3422
  destroy() {
2872
3423
  this.substrate.destroy();
2873
3424
  }
@@ -2896,6 +3447,7 @@ var OrbinumClientProvider = class {
2896
3447
  _reconnectAttempt = 0;
2897
3448
  // ─── Events ─────────────────────────────────────────────────────────────
2898
3449
  _listeners = /* @__PURE__ */ new Set();
3450
+ /** Creates a new provider with the given configuration. Does not connect automatically — call `connect()` to initiate. */
2899
3451
  constructor(config) {
2900
3452
  this.config = config;
2901
3453
  this.connectTimeoutMs = config.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
@@ -2905,9 +3457,11 @@ var OrbinumClientProvider = class {
2905
3457
  this.reconnectMaxMs = config.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
2906
3458
  }
2907
3459
  // ─── Status ─────────────────────────────────────────────────────────────
3460
+ /** Current connection status. Reflects the last state set by the provider internals. */
2908
3461
  get status() {
2909
3462
  return this._status;
2910
3463
  }
3464
+ /** Updates internal status and notifies all registered listeners. Swallows listener exceptions to avoid cascading failures. */
2911
3465
  setStatus(status, error) {
2912
3466
  this._status = status;
2913
3467
  const event = { status, ...error ? { error } : {} };
@@ -2918,6 +3472,10 @@ var OrbinumClientProvider = class {
2918
3472
  }
2919
3473
  });
2920
3474
  }
3475
+ /**
3476
+ * Registers a listener that is called on every status transition.
3477
+ * Returns an unsubscribe function — call it to stop receiving events.
3478
+ */
2921
3479
  onStatusChange(listener) {
2922
3480
  this._listeners.add(listener);
2923
3481
  return () => {
@@ -2925,10 +3483,18 @@ var OrbinumClientProvider = class {
2925
3483
  };
2926
3484
  }
2927
3485
  // ─── Lifecycle ──────────────────────────────────────────────────────────
3486
+ /**
3487
+ * Initiates the first connection attempt. No-op if the provider is not in `'idle'` state.
3488
+ * Call this once after constructing the provider.
3489
+ */
2928
3490
  connect() {
2929
3491
  if (this._status !== "idle") return;
2930
3492
  this.startConnectAttempt();
2931
3493
  }
3494
+ /**
3495
+ * Tears down the active client and any pending reconnect timers,
3496
+ * then resets the provider back to `'idle'` so `connect()` can be called again.
3497
+ */
2932
3498
  reset() {
2933
3499
  this.cancelReconnect();
2934
3500
  this.teardownClient();
@@ -2936,6 +3502,7 @@ var OrbinumClientProvider = class {
2936
3502
  this.setStatus("idle");
2937
3503
  }
2938
3504
  // ─── Internal connection flow ───────────────────────────────────────────
3505
+ /** Transitions to `'connecting'`, kicks off `attemptConnect`, and schedules a reconnect if it fails. */
2939
3506
  startConnectAttempt() {
2940
3507
  this.setStatus("connecting");
2941
3508
  this._connectingPromise = this.attemptConnect();
@@ -2943,6 +3510,11 @@ var OrbinumClientProvider = class {
2943
3510
  if (this._status !== "idle") this.scheduleReconnect();
2944
3511
  });
2945
3512
  }
3513
+ /**
3514
+ * Performs a single connection attempt race against `connectTimeoutMs`.
3515
+ * On success: stores the client, starts the heartbeat, and returns it.
3516
+ * On failure: destroys any orphaned client and transitions to `'disconnected'`.
3517
+ */
2946
3518
  async attemptConnect() {
2947
3519
  let timeoutId = null;
2948
3520
  let orphanClient = null;
@@ -2990,6 +3562,7 @@ var OrbinumClientProvider = class {
2990
3562
  }
2991
3563
  }
2992
3564
  // ─── Heartbeat ──────────────────────────────────────────────────────────
3565
+ /** Starts the periodic heartbeat loop. Replaces any existing timer. */
2993
3566
  startHeartbeat() {
2994
3567
  this.stopHeartbeat();
2995
3568
  this._heartbeatTimer = setInterval(async () => {
@@ -3002,12 +3575,17 @@ var OrbinumClientProvider = class {
3002
3575
  }
3003
3576
  }, this.heartbeatIntervalMs);
3004
3577
  }
3578
+ /** Clears the heartbeat interval timer if active. */
3005
3579
  stopHeartbeat() {
3006
3580
  if (this._heartbeatTimer) {
3007
3581
  clearInterval(this._heartbeatTimer);
3008
3582
  this._heartbeatTimer = null;
3009
3583
  }
3010
3584
  }
3585
+ /**
3586
+ * Sends a `system_health` RPC ping and waits up to `heartbeatTimeoutMs`.
3587
+ * Returns `true` if the node responds in time, `false` otherwise.
3588
+ */
3011
3589
  async probe() {
3012
3590
  if (!this._orbinumClient) return false;
3013
3591
  try {
@@ -3023,6 +3601,10 @@ var OrbinumClientProvider = class {
3023
3601
  }
3024
3602
  }
3025
3603
  // ─── Reconnection ───────────────────────────────────────────────────────
3604
+ /**
3605
+ * Schedules the next connection attempt using exponential backoff
3606
+ * (capped at `reconnectMaxMs`), then transitions to `'reconnecting'`.
3607
+ */
3026
3608
  scheduleReconnect() {
3027
3609
  if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
3028
3610
  const delay = Math.min(
@@ -3036,6 +3618,7 @@ var OrbinumClientProvider = class {
3036
3618
  if (this._status !== "idle") this.startConnectAttempt();
3037
3619
  }, delay);
3038
3620
  }
3621
+ /** Clears any pending reconnect timer without triggering a new attempt. */
3039
3622
  cancelReconnect() {
3040
3623
  if (this._reconnectTimer) {
3041
3624
  clearTimeout(this._reconnectTimer);
@@ -3043,6 +3626,7 @@ var OrbinumClientProvider = class {
3043
3626
  }
3044
3627
  }
3045
3628
  // ─── Client teardown ────────────────────────────────────────────────────
3629
+ /** Stops the heartbeat, destroys the active client, and clears all in-progress promises. */
3046
3630
  teardownClient() {
3047
3631
  this.stopHeartbeat();
3048
3632
  try {
@@ -3053,11 +3637,19 @@ var OrbinumClientProvider = class {
3053
3637
  this._connectingPromise = null;
3054
3638
  }
3055
3639
  // ─── Client access ──────────────────────────────────────────────────────
3640
+ /**
3641
+ * Returns the active `OrbinumClient`, or awaits the in-progress connection attempt.
3642
+ * Throws if the provider is `'idle'`, `'disconnected'`, or `'reconnecting'`.
3643
+ */
3056
3644
  async getOrbinumClient() {
3057
3645
  if (this._orbinumClient) return this._orbinumClient;
3058
3646
  if (this._connectingPromise) return this._connectingPromise;
3059
3647
  throw new Error(`OrbinumClientProvider: cannot get client in status '${this._status}'`);
3060
3648
  }
3649
+ /**
3650
+ * Same as `getOrbinumClient()` but returns `null` instead of throwing.
3651
+ * Useful in contexts where a missing client is an acceptable no-op.
3652
+ */
3061
3653
  async tryGetOrbinumClient() {
3062
3654
  try {
3063
3655
  return await this.getOrbinumClient();
@@ -3066,15 +3658,28 @@ var OrbinumClientProvider = class {
3066
3658
  }
3067
3659
  }
3068
3660
  // ─── Convenience RPC helpers ────────────────────────────────────────────
3661
+ /**
3662
+ * Sends a single Substrate JSON-RPC request and returns the typed result.
3663
+ * Waits for the client to be ready before dispatching.
3664
+ */
3069
3665
  async rpcSend(method, params = []) {
3070
3666
  const client = await this.getOrbinumClient();
3071
3667
  return client.substrate.request(method, params);
3072
3668
  }
3669
+ /**
3670
+ * Sends a single EVM JSON-RPC request and returns the typed result.
3671
+ * Throws if `evmRpc` was not configured.
3672
+ */
3073
3673
  async evmRpc(method, params = []) {
3074
3674
  const client = await this.getOrbinumClient();
3075
3675
  if (!client.evm) throw new Error("EVM RPC not configured");
3076
3676
  return client.evm.request(method, params);
3077
3677
  }
3678
+ /**
3679
+ * Sends multiple EVM JSON-RPC calls as a single batch request.
3680
+ * Returns a tuple of typed results in the same order as `calls`.
3681
+ * Throws if `evmRpc` was not configured.
3682
+ */
3078
3683
  async evmRpcBatch(calls) {
3079
3684
  const client = await this.getOrbinumClient();
3080
3685
  if (!client.evm) throw new Error("EVM RPC not configured");
@@ -3082,102 +3687,471 @@ var OrbinumClientProvider = class {
3082
3687
  }
3083
3688
  };
3084
3689
 
3085
- // src/shielded-pool/NoteDecryptor.ts
3690
+ // src/utils/stealth.ts
3691
+ import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
3692
+ import { hkdf } from "@noble/hashes/hkdf.js";
3693
+ import { mulPointEscalar as mulPointEscalar2, Base8 as Base82, addPoint } from "@zk-kit/baby-jubjub";
3694
+ var STEALTH_INFO = new TextEncoder().encode("orbinum-stealth-v1");
3695
+ function deriveStealthScalar(sharedSecret, ownerPkBigint) {
3696
+ const salt = bigintTo32Le(ownerPkBigint);
3697
+ const stealthBytes = hkdf(sha2562, sharedSecret, salt, STEALTH_INFO, 32);
3698
+ return bytesToBigintLE(stealthBytes) % BABYJUB_SUBORDER || 1n;
3699
+ }
3700
+ function deriveStealthOwnerPk(sharedSecret, ownerPkBigint, ownerPkPoint) {
3701
+ const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
3702
+ const stealthPt = addPoint(mulPointEscalar2(Base82, stealthScalar), ownerPkPoint);
3703
+ return stealthPt[0];
3704
+ }
3705
+ function deriveStealthSk(sharedSecret, ownerPkBigint, spendingKey) {
3706
+ const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
3707
+ return (stealthScalar + spendingKey) % BABYJUB_SUBORDER || 1n;
3708
+ }
3709
+
3710
+ // src/utils/bjj.ts
3711
+ import { mulPointEscalar as mulPointEscalar3 } from "@zk-kit/baby-jubjub";
3712
+ var BJJ_A = 168700n;
3713
+ var BJJ_D = 168696n;
3714
+ function _modpow(base, exp, mod) {
3715
+ let result = 1n;
3716
+ base = base % mod;
3717
+ while (exp > 0n) {
3718
+ if (exp & 1n) result = result * base % mod;
3719
+ exp >>= 1n;
3720
+ base = base * base % mod;
3721
+ }
3722
+ return result;
3723
+ }
3724
+ function _sqrtModP(y2) {
3725
+ if (y2 === 0n) return 0n;
3726
+ if (_modpow(y2, (BN254_R - 1n) / 2n, BN254_R) !== 1n) return null;
3727
+ let s = 0n;
3728
+ let q = BN254_R - 1n;
3729
+ while ((q & 1n) === 0n) {
3730
+ q >>= 1n;
3731
+ s++;
3732
+ }
3733
+ if (s === 1n) return _modpow(y2, (BN254_R + 1n) / 4n, BN254_R);
3734
+ let z = 2n;
3735
+ while (_modpow(z, (BN254_R - 1n) / 2n, BN254_R) === 1n) z++;
3736
+ let m = s;
3737
+ let c = _modpow(z, q, BN254_R);
3738
+ let t = _modpow(y2, q, BN254_R);
3739
+ let r = _modpow(y2, (q + 1n) / 2n, BN254_R);
3740
+ for (; ; ) {
3741
+ if (t === 1n) return r;
3742
+ let i = 1n;
3743
+ let tmp = t * t % BN254_R;
3744
+ while (tmp !== 1n) {
3745
+ tmp = tmp * tmp % BN254_R;
3746
+ i++;
3747
+ }
3748
+ const b = _modpow(c, 1n << m - i - 1n, BN254_R);
3749
+ m = i;
3750
+ c = b * b % BN254_R;
3751
+ t = t * c % BN254_R;
3752
+ r = r * b % BN254_R;
3753
+ }
3754
+ }
3755
+ function recoverOwnerPkPoint(ax) {
3756
+ const x2 = ax * ax % BN254_R;
3757
+ const num = ((1n - BJJ_A * x2) % BN254_R + BN254_R) % BN254_R;
3758
+ const den = ((1n - BJJ_D * x2) % BN254_R + BN254_R) % BN254_R;
3759
+ if (den === 0n) return null;
3760
+ const denInv = _modpow(den, BN254_R - 2n, BN254_R);
3761
+ const y2 = num * denInv % BN254_R;
3762
+ const y = _sqrtModP(y2);
3763
+ if (y === null) return null;
3764
+ const yAlt = BN254_R - y;
3765
+ try {
3766
+ const check = mulPointEscalar3([ax, y], BABYJUB_SUBORDER);
3767
+ return check[0] === 0n && check[1] === 1n ? [ax, y] : [ax, yAlt];
3768
+ } catch {
3769
+ return [ax, yAlt];
3770
+ }
3771
+ }
3772
+
3773
+ // src/shielded-pool/protocol/NoteBuilder.ts
3774
+ import { mulPointEscalar as mulPointEscalar4, unpackPoint as unpackPoint2 } from "@zk-kit/baby-jubjub";
3775
+ import { randomBytes as randomBytes2 } from "@noble/ciphers/utils.js";
3776
+ import { poseidon2, poseidon4 } from "poseidon-lite";
3777
+ var NoteBuilder = class {
3778
+ /**
3779
+ * Build a ZkNote from the given inputs.
3780
+ *
3781
+ * @param input.value Amount in planck (required).
3782
+ * @param input.assetId Asset ID — default 0n (native ORB-Privacy).
3783
+ * @param input.ownerPk Sender's or recipient's global BabyJubJub Ax — default 0n.
3784
+ * @param input.blinding Random scalar — defaults to BigInt(Date.now()).
3785
+ * @param input.spendingKey Secret key for nullifier — default 0n.
3786
+ * @param input.viewingPublicKey Recipient's 32-byte LE packed BJJ ivk. Triggers memo encryption.
3787
+ * @param input.recipientOwnerPk Recipient's global ownerPk. Required with viewingPublicKey
3788
+ * to enable stealth address derivation. Without it, the
3789
+ * commitment uses ownerPk directly (no stealth).
3790
+ */
3791
+ static async build(input) {
3792
+ const value = input.value;
3793
+ const assetId = input.assetId ?? 0n;
3794
+ const ownerPk = input.ownerPk ?? 0n;
3795
+ const blinding = input.blinding ?? BigInt(Date.now());
3796
+ const spendingKey = input.spendingKey ?? 0n;
3797
+ const counterpartyPk = input.counterpartyPk ?? 0n;
3798
+ const useStealth = input.viewingPublicKey !== void 0 && input.recipientOwnerPk !== void 0;
3799
+ let memo;
3800
+ if (useStealth) {
3801
+ const recipientOwnerPk = input.recipientOwnerPk;
3802
+ const recipientIvkPacked = input.viewingPublicKey;
3803
+ const ephSk = randomBytes2(32);
3804
+ const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
3805
+ const ivkPoint = unpackPoint2(ivkPackedBigint);
3806
+ if (!ivkPoint)
3807
+ throw new Error("NoteBuilder.build: invalid recipient viewing public key");
3808
+ const ephSkScalar = BigInt(toHex(ephSk)) % BABYJUB_SUBORDER || 1n;
3809
+ const sharedPoint = mulPointEscalar4(ivkPoint, ephSkScalar);
3810
+ const sharedSecret = bigintTo32Le(sharedPoint[0]);
3811
+ const recipientPkPoint = recoverOwnerPkPoint(recipientOwnerPk);
3812
+ if (!recipientPkPoint)
3813
+ throw new Error(
3814
+ "NoteBuilder.build: recipientOwnerPk is not a valid BJJ x-coordinate"
3815
+ );
3816
+ const effectiveOwnerPk = deriveStealthOwnerPk(
3817
+ sharedSecret,
3818
+ recipientOwnerPk,
3819
+ recipientPkPoint
3820
+ );
3821
+ const stealthCommitment = poseidon4([value, assetId, effectiveOwnerPk, blinding]);
3822
+ const stealthCommitmentBytes = bigintTo32Le(stealthCommitment);
3823
+ memo = Array.from(
3824
+ EncryptedMemo.encrypt(
3825
+ value,
3826
+ bigintTo32Le(effectiveOwnerPk),
3827
+ bigintTo32Le(blinding),
3828
+ Number(assetId),
3829
+ stealthCommitmentBytes,
3830
+ recipientIvkPacked,
3831
+ bigintTo32Le(counterpartyPk),
3832
+ ephSk
3833
+ )
3834
+ );
3835
+ const commitment2 = stealthCommitment;
3836
+ const nullifier2 = poseidon2([commitment2, spendingKey]);
3837
+ const commitmentBytes2 = stealthCommitmentBytes;
3838
+ const nullifierBytes2 = bigintTo32Le(nullifier2);
3839
+ if (memo.length !== ENCRYPTED_MEMO_SIZE)
3840
+ throw new Error(
3841
+ `NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
3842
+ );
3843
+ return {
3844
+ value,
3845
+ assetId,
3846
+ ownerPk: effectiveOwnerPk,
3847
+ blinding,
3848
+ spendingKey,
3849
+ spent: false,
3850
+ spentAt: null,
3851
+ commitment: commitment2,
3852
+ nullifier: nullifier2,
3853
+ commitmentHex: toHex(commitmentBytes2),
3854
+ nullifierHex: toHex(nullifierBytes2),
3855
+ memo,
3856
+ counterpartyPk
3857
+ };
3858
+ }
3859
+ const commitment = poseidon4([value, assetId, ownerPk, blinding]);
3860
+ const nullifier = poseidon2([commitment, spendingKey]);
3861
+ const commitmentBytes = bigintTo32Le(commitment);
3862
+ const nullifierBytes = bigintTo32Le(nullifier);
3863
+ memo = input.viewingPublicKey !== void 0 ? Array.from(
3864
+ EncryptedMemo.encrypt(
3865
+ value,
3866
+ bigintTo32Le(ownerPk),
3867
+ bigintTo32Le(blinding),
3868
+ Number(assetId),
3869
+ commitmentBytes,
3870
+ input.viewingPublicKey,
3871
+ bigintTo32Le(counterpartyPk)
3872
+ )
3873
+ ) : Array.from(EncryptedMemo.dummy());
3874
+ if (memo.length !== ENCRYPTED_MEMO_SIZE)
3875
+ throw new Error(
3876
+ `NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
3877
+ );
3878
+ return {
3879
+ value,
3880
+ assetId,
3881
+ ownerPk,
3882
+ blinding,
3883
+ spendingKey,
3884
+ spent: false,
3885
+ spentAt: null,
3886
+ commitment,
3887
+ nullifier,
3888
+ commitmentHex: toHex(commitmentBytes),
3889
+ nullifierHex: toHex(nullifierBytes),
3890
+ memo,
3891
+ counterpartyPk
3892
+ };
3893
+ }
3894
+ /**
3895
+ * Build the 168-byte ECDH-encrypted memo for a note.
3896
+ *
3897
+ * Pure TypeScript implementation — no WASM dependency.
3898
+ * Uses ChaCha20-Poly1305 with ECDH key agreement (BabyJubJub ephemeral keypair).
3899
+ *
3900
+ * @param note The ZkNote whose fields populate the plaintext.
3901
+ * @param recipientIvkPacked 32-byte LE packed BJJ viewing public key of the recipient.
3902
+ * Pass `new Uint8Array(32)` (default) for a public/dummy memo.
3903
+ * @param counterpartyPk 32-byte counterparty BabyJubJub Ax.
3904
+ * Pass `new Uint8Array(32)` (default) for no counterparty.
3905
+ */
3906
+ static buildMemo(note, recipientIvkPacked, counterpartyPk) {
3907
+ return EncryptedMemo.encrypt(
3908
+ note.value,
3909
+ bigintTo32Le(note.ownerPk),
3910
+ bigintTo32Le(note.blinding),
3911
+ Number(note.assetId),
3912
+ bigintTo32Le(note.commitment),
3913
+ recipientIvkPacked ?? new Uint8Array(32),
3914
+ counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n)
3915
+ );
3916
+ }
3917
+ };
3918
+
3919
+ // src/shielded-pool/protocol/NoteDecryptor.ts
3086
3920
  import { poseidon2 as poseidon22, poseidon4 as poseidon42 } from "poseidon-lite";
3087
- function tryDecryptNote(commitment, viewingKey, spendingKey) {
3088
- if (!commitment.encryptedMemo) return null;
3921
+ function computeNullifier(commitment, spendingKey) {
3922
+ return poseidon22([commitment, spendingKey]);
3923
+ }
3924
+ function tryDecryptNote(commitment, viewingSecretKey, spendingKey, ownOwnerPk = 0n) {
3925
+ return tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwnerPk).note;
3926
+ }
3927
+ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwnerPk = 0n) {
3928
+ if (!commitment.encryptedMemo) return { note: null, reason: "no_memo" };
3089
3929
  let commitmentBytes;
3090
3930
  let memoBytes;
3091
3931
  try {
3092
3932
  commitmentBytes = fromHex(commitment.commitmentHex);
3093
3933
  memoBytes = fromHex(commitment.encryptedMemo);
3094
3934
  } catch {
3095
- return null;
3935
+ return { note: null, reason: "hex_parse_error" };
3936
+ }
3937
+ if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) {
3938
+ return {
3939
+ note: null,
3940
+ reason: `memo_size_mismatch:got_${memoBytes.length}_expected_${ENCRYPTED_MEMO_SIZE}`
3941
+ };
3942
+ }
3943
+ const plaintext = EncryptedMemo.decrypt(memoBytes, commitmentBytes, viewingSecretKey);
3944
+ if (!plaintext) return { note: null, reason: "decrypt_failed:wrong_key_or_corrupt_mac" };
3945
+ let effectiveOwnerPk = plaintext.ownerPk;
3946
+ let effectiveSpendingKey = spendingKey;
3947
+ if (ownOwnerPk !== 0n && plaintext.ownerPk !== ownOwnerPk) {
3948
+ const sharedSecret = EncryptedMemo.extractSharedSecret(memoBytes, viewingSecretKey);
3949
+ if (!sharedSecret) return { note: null, reason: "stealth_shared_secret_failed" };
3950
+ const ownPkPoint = recoverOwnerPkPoint(ownOwnerPk);
3951
+ if (!ownPkPoint) return { note: null, reason: "stealth_invalid_own_owner_pk" };
3952
+ const stealthOwnerPk = deriveStealthOwnerPk(sharedSecret, ownOwnerPk, ownPkPoint);
3953
+ if (stealthOwnerPk !== plaintext.ownerPk) {
3954
+ return { note: null, reason: "commitment_mismatch" };
3955
+ }
3956
+ effectiveOwnerPk = stealthOwnerPk;
3957
+ effectiveSpendingKey = deriveStealthSk(sharedSecret, ownOwnerPk, spendingKey);
3096
3958
  }
3097
- const plaintext = EncryptedMemo.decrypt(memoBytes, commitmentBytes, viewingKey);
3098
- if (!plaintext) return null;
3099
3959
  const recomputed = poseidon42([
3100
3960
  plaintext.value,
3101
3961
  plaintext.assetId,
3102
- plaintext.ownerPk,
3962
+ effectiveOwnerPk,
3103
3963
  plaintext.blinding
3104
3964
  ]);
3105
- if (recomputed !== bytesToBigintLE(commitmentBytes)) return null;
3106
- const nullifier = poseidon22([recomputed, spendingKey]);
3965
+ if (recomputed !== bytesToBigintLE(commitmentBytes)) {
3966
+ return { note: null, reason: "commitment_mismatch" };
3967
+ }
3968
+ const nullifier = poseidon22([recomputed, effectiveSpendingKey]);
3107
3969
  return {
3108
- value: plaintext.value,
3109
- assetId: plaintext.assetId,
3110
- ownerPk: plaintext.ownerPk,
3111
- blinding: plaintext.blinding,
3112
- spendingKey,
3113
- spent: false,
3114
- spentAt: null,
3115
- commitment: recomputed,
3116
- nullifier,
3117
- commitmentHex: toHex(bigintTo32Le(recomputed)),
3118
- nullifierHex: toHex(bigintTo32Le(nullifier)),
3119
- memo: Array.from(memoBytes)
3970
+ note: {
3971
+ value: plaintext.value,
3972
+ assetId: plaintext.assetId,
3973
+ ownerPk: effectiveOwnerPk,
3974
+ blinding: plaintext.blinding,
3975
+ spendingKey: effectiveSpendingKey,
3976
+ spent: false,
3977
+ spentAt: null,
3978
+ commitment: recomputed,
3979
+ nullifier,
3980
+ commitmentHex: toHex(bigintTo32Le(recomputed)),
3981
+ nullifierHex: toHex(bigintTo32Le(nullifier)),
3982
+ memo: Array.from(memoBytes),
3983
+ counterpartyPk: plaintext.counterpartyPk
3984
+ }
3120
3985
  };
3121
3986
  }
3122
3987
 
3123
- // src/shielded-pool/PrivacyKeys.ts
3124
- import { hkdf } from "@noble/hashes/hkdf.js";
3125
- import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
3126
- import { mulPointEscalar, Base8 } from "@zk-kit/baby-jubjub";
3988
+ // src/shielded-pool/protocol/coinSelection.ts
3989
+ var TRANSFER_TREE_DEPTH = 20;
3990
+ function selectNotes(notes, needed) {
3991
+ const unspent = notes.filter((n) => !n.spent && n.value > 0n);
3992
+ const sorted = [...unspent].sort((a, b) => a.value < b.value ? -1 : 1);
3993
+ const single = sorted.find((n) => n.value >= needed);
3994
+ if (single) return [single, null];
3995
+ for (let i = 0; i < sorted.length; i++) {
3996
+ for (let j = i + 1; j < sorted.length; j++) {
3997
+ const a = sorted[i];
3998
+ const b = sorted[j];
3999
+ if (a !== void 0 && b !== void 0 && a.value + b.value >= needed) {
4000
+ return [a, b];
4001
+ }
4002
+ }
4003
+ }
4004
+ return null;
4005
+ }
4006
+ function buildDummyTransferInput(assetId) {
4007
+ const zeroSibling = "0x" + "00".repeat(32);
4008
+ return {
4009
+ nullifier: 0n,
4010
+ // Constraint 9: nullifier * is_dummy.out === 0 → must be 0
4011
+ value: 0n,
4012
+ // triggers is_dummy[i].out = 1 in the circuit
4013
+ assetId,
4014
+ // must match real note (Constraint 7)
4015
+ ownerPk: 0n,
4016
+ blinding: 0n,
4017
+ spendingKey: 1n,
4018
+ // arbitrary; EdDSA is disabled (enabled = 0) for dummy inputs
4019
+ pathSiblings: Array(TRANSFER_TREE_DEPTH).fill(zeroSibling),
4020
+ leafIndex: 0
4021
+ };
4022
+ }
3127
4023
 
3128
- // src/shielded-pool/constants.ts
3129
- var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
4024
+ // src/shielded-pool/protocol/disclosure.ts
4025
+ import {
4026
+ generateDisclosureProof
4027
+ } from "@orbinum/proof-generator";
4028
+ import { mulPointEscalar as mulPointEscalar5, Base8 as Base83 } from "@zk-kit/baby-jubjub";
4029
+ import { poseidon1, poseidon3 } from "poseidon-lite";
4030
+ function hexFieldToBytes32(hex) {
4031
+ const clean = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
4032
+ return fromHex(clean.padStart(64, "0"));
4033
+ }
4034
+ function deriveBabyJubjubKeypair(substrateSigningKey) {
4035
+ let keyBigInt = 0n;
4036
+ for (let i = 0; i < substrateSigningKey.length; i++) {
4037
+ keyBigInt = keyBigInt << 8n | BigInt(substrateSigningKey[i]);
4038
+ }
4039
+ const sk = poseidon1([keyBigInt]);
4040
+ const pk = mulPointEscalar5(Base83, sk);
4041
+ return { sk, pkX: pk[0], pkY: pk[1] };
4042
+ }
4043
+ function buildDisclosurePublicSignals(commitment, auditorPkX, auditorPkY, proofOutput) {
4044
+ const buf = new Uint8Array(256);
4045
+ buf.set(hexFieldToBytes32(commitment), 0);
4046
+ buf.set(bigintTo32Le(auditorPkX), 32);
4047
+ buf.set(bigintTo32Le(auditorPkY), 64);
4048
+ buf.set(hexFieldToBytes32(proofOutput.encryptedData.epkX), 96);
4049
+ buf.set(hexFieldToBytes32(proofOutput.encryptedData.epkY), 128);
4050
+ buf.set(hexFieldToBytes32(proofOutput.encryptedData.encValue), 160);
4051
+ buf.set(hexFieldToBytes32(proofOutput.encryptedData.encAssetId), 192);
4052
+ buf.set(hexFieldToBytes32(proofOutput.encryptedData.encOwnerHash), 224);
4053
+ return Array.from(buf);
4054
+ }
4055
+ function decryptDisclosureSignals(auditorBjjSk, enc) {
4056
+ const shared = mulPointEscalar5([enc.epkX, enc.epkY], auditorBjjSk);
4057
+ const sharedX = shared[0];
4058
+ const sharedY = shared[1];
4059
+ const k0 = poseidon3([sharedX, sharedY, 0n]);
4060
+ const k1 = poseidon3([sharedX, sharedY, 1n]);
4061
+ const k2 = poseidon3([sharedX, sharedY, 2n]);
4062
+ return {
4063
+ value: (enc.encValue - k0 + BN254_R) % BN254_R,
4064
+ assetId: (enc.encAssetId - k1 + BN254_R) % BN254_R,
4065
+ ownerHash: (enc.encOwnerHash - k2 + BN254_R) % BN254_R
4066
+ };
4067
+ }
3130
4068
 
3131
- // src/shielded-pool/PrivacyKeys.ts
4069
+ // src/utils/blinding.ts
4070
+ function randomBlinding() {
4071
+ const buf = new Uint8Array(32);
4072
+ crypto.getRandomValues(buf);
4073
+ const n = bytesToBigintLE(buf);
4074
+ return n === 0n ? 1n : n % BN254_R;
4075
+ }
4076
+
4077
+ // src/privacy-keys/PrivacyKeys.ts
4078
+ import { hkdf as hkdf2 } from "@noble/hashes/hkdf.js";
4079
+ import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
4080
+ import { mulPointEscalar as mulPointEscalar6, Base8 as Base84, packPoint as packPoint2 } from "@zk-kit/baby-jubjub";
3132
4081
  var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
3133
4082
  function deriveSpendingKeyMessage(chainId, address) {
3134
4083
  return `orbinum-spending-key-v1
3135
4084
  ${chainId}
3136
4085
  ${address.toLowerCase()}`;
3137
4086
  }
3138
- async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
3139
- const hex = signatureHex.startsWith("0x") ? signatureHex.slice(2) : signatureHex;
3140
- const sigBytes = new Uint8Array((hex.match(/.{2}/g) ?? []).map((b) => parseInt(b, 16)));
4087
+ async function deriveMasterKeyBytes(signatureHex, chainId, address) {
4088
+ const sigBytes = fromHex(signatureHex);
3141
4089
  const info = new TextEncoder().encode(`orbinum-sk-v1:${chainId}:${address.toLowerCase()}`);
3142
- const skBytes = hkdf(sha2562, sigBytes, new Uint8Array(0), info, 32);
3143
- const skBigint = BigInt(
3144
- "0x" + Array.from(skBytes).map((b) => b.toString(16).padStart(2, "0")).join("")
3145
- ) % BN254_R;
4090
+ return hkdf2(sha2563, sigBytes, new Uint8Array(0), info, 32);
4091
+ }
4092
+ async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
4093
+ const masterBytes = await deriveMasterKeyBytes(signatureHex, chainId, address);
4094
+ const skBigint = BigInt(toHex(masterBytes)) % BABYJUB_SUBORDER;
3146
4095
  return skBigint === 0n ? 1n : skBigint;
3147
4096
  }
3148
- function deriveViewingKey(spendingKey) {
4097
+ function deriveViewingSecretKey(spendingKey) {
3149
4098
  const ikm = bigintTo32Le(spendingKey);
3150
- return hkdf(sha2562, ikm, void 0, IVK_DOMAIN, 32);
4099
+ return hkdf2(sha2563, ikm, void 0, IVK_DOMAIN, 32);
4100
+ }
4101
+ function deriveViewingPublicKey(ivsk) {
4102
+ const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
4103
+ const ivkPoint = mulPointEscalar6(Base84, ivskScalar);
4104
+ const packed = packPoint2(ivkPoint);
4105
+ return bigintTo32Le(packed);
3151
4106
  }
3152
4107
  function deriveOwnerPk(spendingKey) {
3153
4108
  try {
3154
- const pubPoint = mulPointEscalar(Base8, spendingKey);
4109
+ const pubPoint = mulPointEscalar6(Base84, spendingKey);
3155
4110
  return pubPoint[0];
3156
4111
  } catch {
3157
4112
  return 0n;
3158
4113
  }
3159
4114
  }
3160
4115
 
3161
- // src/shielded-pool/PrivacyKeyManager.ts
4116
+ // src/privacy-keys/PrivacyKeyManager.ts
3162
4117
  var PrivacyKeyManager = class {
3163
4118
  _state = {
3164
4119
  spendingKey: null,
3165
- viewingKey: null,
4120
+ masterBytes: null,
4121
+ viewingSecretKey: null,
4122
+ viewingPublicKeyPacked: null,
3166
4123
  ownerPk: null
3167
4124
  };
3168
4125
  /**
3169
- * Load a spending key into the in-memory session.
3170
- * Derives viewingKey and ownerPk immediately.
4126
+ * Load a spending key and its corresponding master bytes into the in-memory session.
4127
+ * Derives viewingSecretKey, viewingPublicKeyPacked, and ownerPk immediately.
3171
4128
  * Replaces any previously loaded key.
4129
+ *
4130
+ * @param spendingKey Circuit scalar: BigInt(masterBytes) % BABYJUB_SUBORDER, clamped to [1, ∞).
4131
+ * @param masterBytes Raw 32-byte HKDF output before modular reduction. Used to derive
4132
+ * the stable vault key (HKDF(masterBytes, info="orbinum-vault-key-v1")).
3172
4133
  */
3173
- async load(spendingKey) {
3174
- const viewingKey = deriveViewingKey(spendingKey);
4134
+ async load(spendingKey, masterBytes) {
4135
+ const viewingSecretKey = deriveViewingSecretKey(spendingKey);
4136
+ const viewingPublicKeyPacked = deriveViewingPublicKey(viewingSecretKey);
3175
4137
  const ownerPk = deriveOwnerPk(spendingKey);
3176
- this._state = { spendingKey, viewingKey, ownerPk };
4138
+ this._state = {
4139
+ spendingKey,
4140
+ masterBytes,
4141
+ viewingSecretKey,
4142
+ viewingPublicKeyPacked,
4143
+ ownerPk
4144
+ };
3177
4145
  }
3178
4146
  /** Clear all key material from memory. Call on vault lock / sign-out. */
3179
4147
  clear() {
3180
- this._state = { spendingKey: null, viewingKey: null, ownerPk: null };
4148
+ this._state = {
4149
+ spendingKey: null,
4150
+ masterBytes: null,
4151
+ viewingSecretKey: null,
4152
+ viewingPublicKeyPacked: null,
4153
+ ownerPk: null
4154
+ };
3181
4155
  }
3182
4156
  /** Returns true if a spending key has been loaded. */
3183
4157
  isLoaded() {
@@ -3190,12 +4164,29 @@ var PrivacyKeyManager = class {
3190
4164
  }
3191
4165
  return this._state.spendingKey;
3192
4166
  }
3193
- /** Returns the 32-byte viewing key. Throws if not loaded. */
3194
- getViewingKey() {
3195
- if (this._state.viewingKey === null) {
4167
+ /**
4168
+ * Returns the 32-byte viewing secret key (ivsk).
4169
+ * Used internally for decrypting received notes during rescan.
4170
+ * SECURITY: never expose this in addresses or network requests.
4171
+ * Throws if not loaded.
4172
+ */
4173
+ getViewingSecretKey() {
4174
+ if (this._state.viewingSecretKey === null) {
4175
+ throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
4176
+ }
4177
+ return this._state.viewingSecretKey;
4178
+ }
4179
+ /**
4180
+ * Returns the 32-byte LE-encoded packed BJJ viewing public key (ivk).
4181
+ * This is the component embedded in the privacy address and passed to senders
4182
+ * so they can encrypt memos only the recipient can decrypt.
4183
+ * Throws if not loaded.
4184
+ */
4185
+ getViewingPublicKeyPacked() {
4186
+ if (this._state.viewingPublicKeyPacked === null) {
3196
4187
  throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
3197
4188
  }
3198
- return this._state.viewingKey;
4189
+ return this._state.viewingPublicKeyPacked;
3199
4190
  }
3200
4191
  /** Returns the BabyJubJub owner public key (x-coordinate). Throws if not loaded. */
3201
4192
  getOwnerPk() {
@@ -3208,24 +4199,86 @@ var PrivacyKeyManager = class {
3208
4199
  getSpendingKeyBytes() {
3209
4200
  return bigintTo32Le(this.getSpendingKey());
3210
4201
  }
3211
- /** Exports the spending key as a 0x-prefixed 64-char hex string. Throws if not loaded. */
4202
+ /**
4203
+ * Returns the 32-byte master key bytes (pre-modulus HKDF output).
4204
+ * Used to derive the stable vault AES key. Throws if not loaded.
4205
+ */
4206
+ getMasterBytes() {
4207
+ if (this._state.masterBytes === null) {
4208
+ throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
4209
+ }
4210
+ return this._state.masterBytes;
4211
+ }
4212
+ /**
4213
+ * Exports the master key bytes as a "mk:0x{hex}" string.
4214
+ * Storing masterBytes (not the sk scalar) ensures the vault key and
4215
+ * rescan can always be reconstructed regardless of any future modulus change.
4216
+ * Throws if not loaded.
4217
+ */
3212
4218
  exportHex() {
3213
- return "0x" + this.getSpendingKey().toString(16).padStart(64, "0");
4219
+ const mb = this.getMasterBytes();
4220
+ return "mk:0x" + Array.from(mb, (b) => b.toString(16).padStart(2, "0")).join("");
4221
+ }
4222
+ /**
4223
+ * Exports a shareable privacy address encoding the owner public key and
4224
+ * viewing PUBLIC key of the currently loaded identity.
4225
+ *
4226
+ * Format: `orbpriv1:{ownerPk_hex}:{viewingPublicKey_hex}`
4227
+ *
4228
+ * The recipient uses this address so the sender can:
4229
+ * 1. Embed `ownerPk` in the note commitment (Poseidon4 input).
4230
+ * 2. Encrypt the memo via ECDH with the recipient's `viewingPublicKey`.
4231
+ *
4232
+ * SECURITY: Only the viewing PUBLIC key is embedded — the viewing secret key
4233
+ * (used for decryption) is never exported. Holders of this address cannot
4234
+ * decrypt the recipient's notes.
4235
+ *
4236
+ * Throws if no key is loaded.
4237
+ */
4238
+ encodePrivacyAddress() {
4239
+ const ownerPk = this.getOwnerPk();
4240
+ const ivkPacked = this.getViewingPublicKeyPacked();
4241
+ const ownerPkHex = "0x" + ownerPk.toString(16).padStart(64, "0");
4242
+ const ivkHex = "0x" + Array.from(ivkPacked, (b) => b.toString(16).padStart(2, "0")).join("");
4243
+ return `orbpriv1:${ownerPkHex}:${ivkHex}`;
3214
4244
  }
3215
4245
  /**
3216
- * Load a spending key from a 0x-prefixed or bare hex string.
3217
- * Validates the key is in the valid range [1, BN254_R).
4246
+ * Decode a privacy address of the form `orbpriv1:{ownerPk_hex}:{viewingPublicKey_hex}`.
4247
+ * Returns `{ ownerPkHex, viewingPublicKeyHex }` on success, or `null` if the input
4248
+ * does not match the expected format.
4249
+ */
4250
+ static decodePrivacyAddress(address) {
4251
+ if (!address.startsWith("orbpriv1:")) return null;
4252
+ const parts = address.split(":");
4253
+ if (parts.length !== 3) return null;
4254
+ const ownerPkHex = parts[1];
4255
+ const viewingPublicKeyHex = parts[2];
4256
+ if (!ownerPkHex || !viewingPublicKeyHex) return null;
4257
+ return { ownerPkHex, viewingPublicKeyHex };
4258
+ }
4259
+ /**
4260
+ * Load keys from a cached "mk:0x{masterBytes_hex}" string produced by exportHex().
4261
+ * Throws if the format is invalid or masterBytes length is not 32 bytes.
3218
4262
  */
3219
4263
  async importFromHex(hex) {
3220
- const key = BigInt(hex.startsWith("0x") ? hex : "0x" + hex);
3221
- if (key === 0n || key >= BN254_R) {
3222
- throw new Error("PrivacyKeyManager: invalid spending key \u2014 out of BN254 range.");
4264
+ if (!hex.startsWith("mk:")) {
4265
+ throw new Error(
4266
+ 'PrivacyKeyManager: invalid cache format. Expected "mk:0x{masterBytes_hex}".'
4267
+ );
4268
+ }
4269
+ const raw = hex.slice(3);
4270
+ const h = raw.startsWith("0x") ? raw.slice(2) : raw;
4271
+ const masterBytes = new Uint8Array((h.match(/.{2}/g) ?? []).map((b) => parseInt(b, 16)));
4272
+ if (masterBytes.length !== 32) {
4273
+ throw new Error("PrivacyKeyManager: invalid master bytes \u2014 expected 32 bytes.");
3223
4274
  }
3224
- await this.load(key);
4275
+ const masterBigint = BigInt("0x" + h);
4276
+ const sk = masterBigint % BABYJUB_SUBORDER || 1n;
4277
+ await this.load(sk, masterBytes);
3225
4278
  }
3226
4279
  };
3227
4280
 
3228
- // src/shielded-pool/VaultCrypto.ts
4281
+ // src/vault/VaultJson.ts
3229
4282
  function vaultReplacer(_key, value) {
3230
4283
  if (typeof value === "bigint") return { __bigint: value.toString() };
3231
4284
  return value;
@@ -3236,16 +4289,28 @@ function vaultReviver(_key, value) {
3236
4289
  }
3237
4290
  return value;
3238
4291
  }
4292
+
4293
+ // src/utils/encoding.ts
4294
+ function toBase64(buf) {
4295
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
4296
+ let str = "";
4297
+ for (const b of bytes) str += String.fromCharCode(b);
4298
+ return btoa(str);
4299
+ }
4300
+ function fromBase64(b64) {
4301
+ const bin = atob(b64);
4302
+ const out = new Uint8Array(bin.length);
4303
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
4304
+ return out;
4305
+ }
4306
+
4307
+ // src/vault/VaultCrypto.ts
3239
4308
  var VAULT_KEY_INFO = new TextEncoder().encode("orbinum-vault-key-v1");
3240
4309
  var IV_BYTES = 12;
3241
- async function deriveVaultKey(spendingKeyBytes) {
3242
- const keyMaterial = await crypto.subtle.importKey(
3243
- "raw",
3244
- spendingKeyBytes.slice(0),
3245
- "HKDF",
3246
- false,
3247
- ["deriveKey"]
3248
- );
4310
+ async function deriveVaultKey(masterBytes) {
4311
+ const keyMaterial = await crypto.subtle.importKey("raw", masterBytes.slice(0), "HKDF", false, [
4312
+ "deriveKey"
4313
+ ]);
3249
4314
  return crypto.subtle.deriveKey(
3250
4315
  {
3251
4316
  name: "HKDF",
@@ -3274,6 +4339,177 @@ async function decryptJson(key, iv, ciphertext) {
3274
4339
  return JSON.parse(new TextDecoder().decode(plainBuf), vaultReviver);
3275
4340
  }
3276
4341
 
4342
+ // src/vault/errors.ts
4343
+ var VaultLockedError = class extends Error {
4344
+ constructor(message = "Vault is locked. Connect your wallet to unlock it.") {
4345
+ super(message);
4346
+ this.name = "VaultLockedError";
4347
+ }
4348
+ };
4349
+
4350
+ // src/vault/noteOps.ts
4351
+ function applyNoteStatus(note, status) {
4352
+ return {
4353
+ ...note,
4354
+ spent: status?.spent ?? note.spent ?? false,
4355
+ spentAt: status?.spentAt ?? note.spentAt ?? null
4356
+ };
4357
+ }
4358
+ async function encryptNote(key, note) {
4359
+ const { iv, ciphertext } = await encryptJson(key, note);
4360
+ return {
4361
+ commitmentHex: note.commitmentHex,
4362
+ iv,
4363
+ ciphertext,
4364
+ nullifierHex: note.nullifierHex,
4365
+ assetId: note.assetId.toString(),
4366
+ spent: note.spent,
4367
+ spentAt: note.spentAt,
4368
+ updatedAt: Date.now()
4369
+ };
4370
+ }
4371
+ async function decryptNoteRecord(key, rec) {
4372
+ const note = await decryptJson(key, rec.iv, rec.ciphertext);
4373
+ return applyNoteStatus(note, {
4374
+ ...rec.spent !== void 0 && { spent: rec.spent },
4375
+ spentAt: rec.spentAt ?? null
4376
+ });
4377
+ }
4378
+
4379
+ // src/proof-generator/unshield.ts
4380
+ import {
4381
+ CircuitType,
4382
+ generateProof,
4383
+ WebArtifactProvider
4384
+ } from "@orbinum/proof-generator";
4385
+ import { randomBytes as randomBytes3 } from "@noble/ciphers/utils.js";
4386
+ import { mulPointEscalar as mulPointEscalar7, Base8 as Base85 } from "@zk-kit/baby-jubjub";
4387
+ import { poseidon4 as poseidon43 } from "poseidon-lite";
4388
+
4389
+ // src/proof-generator/merkle.ts
4390
+ function merkleProofToCircuit(siblings, leafIndex) {
4391
+ const elements = siblings.map((h) => leHexToBigint(h).toString());
4392
+ const depth = siblings.length;
4393
+ const indices = computePathIndices(leafIndex, depth).map(String);
4394
+ return { elements, indices };
4395
+ }
4396
+
4397
+ // src/proof-generator/unshield.ts
4398
+ async function generateUnshieldProof(inputs, options = {}) {
4399
+ const { elements, indices } = merkleProofToCircuit(inputs.pathSiblings, inputs.leafIndex);
4400
+ const fee = inputs.fee ?? 0n;
4401
+ const changeValue = inputs.changeValue ?? 0n;
4402
+ const noteValue = inputs.amount + fee + changeValue;
4403
+ if (inputs.amount <= 0n) {
4404
+ throw new Error("Unshield amount must be greater than zero.");
4405
+ }
4406
+ if (changeValue < 0n) {
4407
+ throw new Error("changeValue must be >= 0.");
4408
+ }
4409
+ const changeOwnerPubkey = inputs.changeOwnerPubkey ?? mulPointEscalar7(Base85, inputs.spendingKey)[0];
4410
+ const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE(randomBytes3(32)) : 0n);
4411
+ const changeCommitment = changeValue > 0n ? poseidon43([changeValue, inputs.assetId, changeOwnerPubkey, changeBlinding]) : 0n;
4412
+ const circuitInputs = {
4413
+ merkle_root: leHexToBigint(inputs.merkleRoot).toString(),
4414
+ nullifier: inputs.nullifier.toString(),
4415
+ amount: inputs.amount.toString(),
4416
+ recipient: inputs.recipient.toString(),
4417
+ asset_id: inputs.assetId.toString(),
4418
+ fee: fee.toString(),
4419
+ change_commitment: changeCommitment.toString(),
4420
+ note_value: noteValue.toString(),
4421
+ note_asset_id: inputs.assetId.toString(),
4422
+ note_blinding: inputs.blinding.toString(),
4423
+ spending_key: inputs.spendingKey.toString(),
4424
+ path_elements: elements,
4425
+ path_indices: indices,
4426
+ change_value: changeValue.toString(),
4427
+ change_blinding: changeBlinding.toString(),
4428
+ change_owner_pubkey: changeOwnerPubkey.toString()
4429
+ };
4430
+ const provider = options.provider ?? new WebArtifactProvider();
4431
+ const opts = { provider };
4432
+ if (options.verbose !== void 0) opts.verbose = options.verbose;
4433
+ const proofResult = await generateProof(CircuitType.Unshield, circuitInputs, opts);
4434
+ return { ...proofResult, changeCommitment, changeValue, changeBlinding, changeOwnerPubkey };
4435
+ }
4436
+
4437
+ // src/proof-generator/transfer.ts
4438
+ import {
4439
+ CircuitType as CircuitType2,
4440
+ generateProof as generateProof2,
4441
+ WebArtifactProvider as WebArtifactProvider2
4442
+ } from "@orbinum/proof-generator";
4443
+ async function generateTransferProof(params, options = {}) {
4444
+ const root = leHexToBigint(params.merkleRoot).toString();
4445
+ const [i0, i1] = params.inputs;
4446
+ const [o0, o1] = params.outputs;
4447
+ const fee = params.fee ?? 0n;
4448
+ const path0 = merkleProofToCircuit(i0.pathSiblings, i0.leafIndex);
4449
+ const path1 = merkleProofToCircuit(i1.pathSiblings, i1.leafIndex);
4450
+ const circuitInputs = {
4451
+ merkle_root: root,
4452
+ nullifiers: [i0.nullifier.toString(), i1.nullifier.toString()],
4453
+ commitments: [o0.commitment.toString(), o1.commitment.toString()],
4454
+ asset_id: i0.assetId.toString(),
4455
+ fee: fee.toString(),
4456
+ input_values: [i0.value.toString(), i1.value.toString()],
4457
+ input_asset_ids: [i0.assetId.toString(), i1.assetId.toString()],
4458
+ input_blindings: [i0.blinding.toString(), i1.blinding.toString()],
4459
+ spending_keys: [i0.spendingKey.toString(), i1.spendingKey.toString()],
4460
+ input_path_elements: [path0.elements, path1.elements],
4461
+ input_path_indices: [path0.indices, path1.indices],
4462
+ output_values: [o0.value.toString(), o1.value.toString()],
4463
+ output_asset_ids: [o0.assetId.toString(), o1.assetId.toString()],
4464
+ output_owner_pubkeys: [o0.ownerPk.toString(), o1.ownerPk.toString()],
4465
+ output_blindings: [o0.blinding.toString(), o1.blinding.toString()]
4466
+ };
4467
+ const provider = options.provider ?? new WebArtifactProvider2();
4468
+ const opts = { provider };
4469
+ if (options.verbose !== void 0) opts.verbose = options.verbose;
4470
+ return generateProof2(CircuitType2.Transfer, circuitInputs, opts);
4471
+ }
4472
+
4473
+ // src/proof-generator/fee-claim.ts
4474
+ import {
4475
+ generateDisclosureProof as generateDisclosureProof2
4476
+ } from "@orbinum/proof-generator";
4477
+ function hexSignalToBytes(hex) {
4478
+ const clean = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
4479
+ const padded = clean.padStart(64, "0");
4480
+ const bytes = new Uint8Array(32);
4481
+ for (let i = 0; i < 32; i++) {
4482
+ bytes[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
4483
+ }
4484
+ return bytes;
4485
+ }
4486
+ async function generateFeeClaimProof(inputs, options = {}) {
4487
+ const result = await generateDisclosureProof2(
4488
+ inputs.amount,
4489
+ inputs.ownerPubkey,
4490
+ inputs.blinding,
4491
+ inputs.assetId,
4492
+ inputs.commitment,
4493
+ // Baby Jubjub base point G — placeholder, no real auditor for fee claiming
4494
+ 5299619240641551281634865583518297030282874472190772894086521144482721001553n,
4495
+ 16950150798460657717958625567821834550301663161624707787222815936182638968203n,
4496
+ 1n,
4497
+ // r — placeholder ephemeral scalar (NOT cryptographically secure)
4498
+ { discloseValue: true, discloseAssetId: true, discloseOwner: false },
4499
+ options
4500
+ );
4501
+ const [, , sigEncValue, sigEncAssetId, sigEncOwnerHash, sigCommitment] = result.publicSignals.map(hexSignalToBytes);
4502
+ const compact = new Uint8Array(76);
4503
+ compact.set(sigCommitment);
4504
+ compact.set(sigEncValue.subarray(0, 8), 32);
4505
+ compact.set(sigEncAssetId.subarray(0, 4), 40);
4506
+ compact.set(sigEncOwnerHash, 44);
4507
+ return {
4508
+ proof: result.proof,
4509
+ publicSignals: Array.from(compact)
4510
+ };
4511
+ }
4512
+
3277
4513
  // src/account-mapping/types/index.ts
3278
4514
  var SignatureScheme = {
3279
4515
  Eip191: "Eip191",
@@ -3290,7 +4526,7 @@ function decodePrecompileCalldata(address, input) {
3290
4526
  if (!fnSig) return null;
3291
4527
  if (fnSig.startsWith("registerAlias")) {
3292
4528
  try {
3293
- const data = hexToBytes(input.slice(10));
4529
+ const data = fromHex(input.slice(10));
3294
4530
  const alias = decodeString(data, 0);
3295
4531
  return { fnSig, args: { alias } };
3296
4532
  } catch {
@@ -3299,37 +4535,43 @@ function decodePrecompileCalldata(address, input) {
3299
4535
  }
3300
4536
  if (fnSig.startsWith("shield(")) {
3301
4537
  try {
3302
- const data = hexToBytes(input.slice(10));
4538
+ const data = fromHex(input.slice(10));
3303
4539
  const assetId = decodeUint(data, 0);
3304
- const amount = decodeUint(data, 32);
3305
- const commitment = toHex(data.slice(64, 96));
3306
- return { fnSig, args: { assetId, amount, commitment } };
4540
+ const commitment = toHex(data.slice(32, 64));
4541
+ return { fnSig, args: { assetId, commitment } };
3307
4542
  } catch {
3308
4543
  return { fnSig, args: {} };
3309
4544
  }
3310
4545
  }
3311
4546
  if (fnSig.startsWith("unshield(")) {
3312
4547
  try {
3313
- const data = hexToBytes(input.slice(10));
4548
+ const data = fromHex(input.slice(10));
3314
4549
  const root = toHex(data.slice(32, 64));
3315
4550
  const nullifier = toHex(data.slice(64, 96));
3316
4551
  const assetId = decodeUint(data, 96);
3317
4552
  const amount = decodeUint(data, 128);
3318
4553
  const recipient = toHex(data.slice(160, 192));
3319
- return { fnSig, args: { root, nullifier, assetId, amount, recipient } };
4554
+ const fee = decodeUint(data, 192);
4555
+ const changeCommitment = toHex(data.slice(224, 256));
4556
+ return {
4557
+ fnSig,
4558
+ args: { root, nullifier, assetId, amount, recipient, fee, changeCommitment }
4559
+ };
3320
4560
  } catch {
3321
4561
  return { fnSig, args: {} };
3322
4562
  }
3323
4563
  }
3324
4564
  if (fnSig.startsWith("privateTransfer(")) {
3325
4565
  try {
3326
- const data = hexToBytes(input.slice(10));
4566
+ const data = fromHex(input.slice(10));
3327
4567
  const root = toHex(data.slice(32, 64));
3328
4568
  const nullOffset = Number(decodeUint(data, 64));
3329
4569
  const commOffset = Number(decodeUint(data, 96));
3330
4570
  const nullifiers = Number(decodeUint(data, nullOffset));
3331
4571
  const commitments = Number(decodeUint(data, commOffset));
3332
- return { fnSig, args: { root, nullifiers, commitments } };
4572
+ const assetId = decodeUint(data, 160);
4573
+ const fee = decodeUint(data, 192);
4574
+ return { fnSig, args: { root, nullifiers, commitments, assetId, fee } };
3333
4575
  } catch {
3334
4576
  return { fnSig, args: {} };
3335
4577
  }
@@ -3542,7 +4784,11 @@ function mapExtrinsicArgs(section, method, args) {
3542
4784
  if (m_norm === "requestdisclosure") {
3543
4785
  return {
3544
4786
  target: get(0, "target"),
3545
- reason: get(1, "reason")
4787
+ commitment: get(1, "commitment"),
4788
+ required_fields: get(2, "required_fields"),
4789
+ reason: get(3, "reason"),
4790
+ auditor_bjj_pk_x: get(4, "auditor_bjj_pk_x"),
4791
+ auditor_bjj_pk_y: get(5, "auditor_bjj_pk_y")
3546
4792
  };
3547
4793
  }
3548
4794
  if (m_norm === "disclose") {
@@ -3556,7 +4802,8 @@ function mapExtrinsicArgs(section, method, args) {
3556
4802
  if (m_norm === "rejectdisclosure") {
3557
4803
  return {
3558
4804
  auditor: get(0, "auditor"),
3559
- reason: get(1, "reason")
4805
+ commitment: get(1, "commitment"),
4806
+ reason: get(2, "reason")
3560
4807
  };
3561
4808
  }
3562
4809
  if (m_norm === "registerasset") {
@@ -3579,7 +4826,8 @@ function mapExtrinsicArgs(section, method, args) {
3579
4826
  if (m_norm === "pruneexpiredrequest") {
3580
4827
  return {
3581
4828
  target: get(0, "target"),
3582
- auditor: get(1, "auditor")
4829
+ auditor: get(1, "auditor"),
4830
+ commitment: get(2, "commitment")
3583
4831
  };
3584
4832
  }
3585
4833
  if (m_norm === "revokedisclosurerecord") {
@@ -3816,29 +5064,35 @@ function mapZkEventData(method, data) {
3816
5064
  }
3817
5065
  if (m_norm === "disclosed") {
3818
5066
  return {
3819
- who: get(0, "who"),
3820
- commitment: get(1, "commitment"),
3821
- auditor: get(2, "auditor")
5067
+ target: get(0, "target"),
5068
+ auditor: get(1, "auditor"),
5069
+ commitment: get(2, "commitment"),
5070
+ signals: get(3, "signals")
3822
5071
  };
3823
5072
  }
3824
5073
  if (m_norm === "disclosurerequested") {
3825
5074
  return {
3826
5075
  target: get(0, "target"),
3827
5076
  auditor: get(1, "auditor"),
3828
- reason: get(2, "reason")
5077
+ commitment: get(2, "commitment"),
5078
+ required_fields: get(3, "required_fields"),
5079
+ auditor_bjj_pk_x: get(4, "auditor_bjj_pk_x"),
5080
+ auditor_bjj_pk_y: get(5, "auditor_bjj_pk_y")
3829
5081
  };
3830
5082
  }
3831
5083
  if (m_norm === "disclosurerejected") {
3832
5084
  return {
3833
5085
  target: get(0, "target"),
3834
5086
  auditor: get(1, "auditor"),
3835
- reason: get(2, "reason")
5087
+ commitment: get(2, "commitment"),
5088
+ reason: get(3, "reason")
3836
5089
  };
3837
5090
  }
3838
5091
  if (m_norm === "disclosurerequestexpired") {
3839
5092
  return {
3840
5093
  target: get(0, "target"),
3841
- auditor: get(1, "auditor")
5094
+ auditor: get(1, "auditor"),
5095
+ commitment: get(2, "commitment")
3842
5096
  };
3843
5097
  }
3844
5098
  if (m_norm === "disclosurerecordrevoked") {
@@ -4100,9 +5354,13 @@ export {
4100
5354
  AccountId2 as AccountId,
4101
5355
  AccountMappingModule,
4102
5356
  AccountMappingPrecompile,
5357
+ BABYJUB_SUBORDER,
5358
+ BN254_R,
4103
5359
  Blake2256,
4104
5360
  CircuitId,
5361
+ CircuitType,
4105
5362
  CryptoPrecompiles,
5363
+ ENCRYPTED_MEMO_SIZE,
4106
5364
  EncryptedMemo,
4107
5365
  EvmClient,
4108
5366
  EvmExplorer,
@@ -4115,30 +5373,45 @@ export {
4115
5373
  PRECOMPILE_ADDR,
4116
5374
  PrivacyKeyManager,
4117
5375
  PrivacyModule,
5376
+ RelayerStatusModule,
4118
5377
  SLIP0044_NAMESPACE,
4119
5378
  ShieldedPoolModule,
4120
5379
  ShieldedPoolPrecompile,
4121
5380
  SignatureScheme,
4122
5381
  Storage,
4123
5382
  SubstrateClient,
5383
+ VaultLockedError,
5384
+ WebArtifactProvider,
4124
5385
  ZkVerifierModule,
4125
5386
  accountIdHexToSs58,
4126
5387
  addressToAccountIdHex,
5388
+ applyNoteStatus,
4127
5389
  base58,
4128
5390
  bigintTo32Be,
4129
5391
  bigintTo32Le,
4130
5392
  bigintTo32LeArr,
5393
+ buildDisclosurePublicSignals,
5394
+ buildDummyTransferInput,
4131
5395
  bytesToBigintLE,
5396
+ computeNullifier,
4132
5397
  computePathIndices,
4133
5398
  connectInjectedExtension,
4134
5399
  decodePrecompileCalldata,
5400
+ decryptDisclosureSignals,
4135
5401
  decryptJson,
5402
+ decryptNoteRecord,
5403
+ deriveBabyJubjubKeypair,
5404
+ deriveMasterKeyBytes,
4136
5405
  deriveOwnerPk,
4137
5406
  deriveSpendingKeyFromSignature,
4138
5407
  deriveSpendingKeyMessage,
5408
+ deriveStealthOwnerPk,
5409
+ deriveStealthSk,
4139
5410
  deriveVaultKey,
4140
- deriveViewingKey,
5411
+ deriveViewingPublicKey,
5412
+ deriveViewingSecretKey,
4141
5413
  encryptJson,
5414
+ encryptNote,
4142
5415
  ensureHexPrefix,
4143
5416
  evmAddressToAccountId,
4144
5417
  evmToImplicitSubstrate,
@@ -4148,6 +5421,10 @@ export {
4148
5421
  formatORB,
4149
5422
  fromBase64,
4150
5423
  fromHex,
5424
+ generateDisclosureProof,
5425
+ generateFeeClaimProof,
5426
+ generateTransferProof,
5427
+ generateUnshieldProof,
4151
5428
  getInjectedExtensions,
4152
5429
  getPolkadotSigner,
4153
5430
  getPolkadotSignerFromPjs,
@@ -4165,6 +5442,9 @@ export {
4165
5442
  mapExtrinsicArgs,
4166
5443
  mapZkEventData,
4167
5444
  normalizeEvmAddress,
5445
+ randomBlinding,
5446
+ recoverOwnerPkPoint,
5447
+ selectNotes,
4168
5448
  shortHash,
4169
5449
  substrateSs58ToAccountIdHex,
4170
5450
  substrateToEvm,
@@ -4173,6 +5453,7 @@ export {
4173
5453
  toTxResult,
4174
5454
  truncateMiddle,
4175
5455
  tryDecryptNote,
5456
+ tryDecryptNoteVerbose,
4176
5457
  u128,
4177
5458
  u64,
4178
5459
  vaultReplacer,