@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.js CHANGED
@@ -23,9 +23,13 @@ __export(index_exports, {
23
23
  AccountId: () => import_substrate_bindings3.AccountId,
24
24
  AccountMappingModule: () => AccountMappingModule,
25
25
  AccountMappingPrecompile: () => AccountMappingPrecompile,
26
+ BABYJUB_SUBORDER: () => BABYJUB_SUBORDER,
27
+ BN254_R: () => BN254_R,
26
28
  Blake2256: () => import_substrate_bindings3.Blake2256,
27
29
  CircuitId: () => CircuitId,
30
+ CircuitType: () => import_proof_generator2.CircuitType,
28
31
  CryptoPrecompiles: () => CryptoPrecompiles,
32
+ ENCRYPTED_MEMO_SIZE: () => ENCRYPTED_MEMO_SIZE,
29
33
  EncryptedMemo: () => EncryptedMemo,
30
34
  EvmClient: () => EvmClient,
31
35
  EvmExplorer: () => EvmExplorer,
@@ -38,30 +42,45 @@ __export(index_exports, {
38
42
  PRECOMPILE_ADDR: () => PRECOMPILE_ADDR,
39
43
  PrivacyKeyManager: () => PrivacyKeyManager,
40
44
  PrivacyModule: () => PrivacyModule,
45
+ RelayerStatusModule: () => RelayerStatusModule,
41
46
  SLIP0044_NAMESPACE: () => SLIP0044_NAMESPACE,
42
47
  ShieldedPoolModule: () => ShieldedPoolModule,
43
48
  ShieldedPoolPrecompile: () => ShieldedPoolPrecompile,
44
49
  SignatureScheme: () => SignatureScheme,
45
50
  Storage: () => import_substrate_bindings3.Storage,
46
51
  SubstrateClient: () => SubstrateClient,
52
+ VaultLockedError: () => VaultLockedError,
53
+ WebArtifactProvider: () => import_proof_generator2.WebArtifactProvider,
47
54
  ZkVerifierModule: () => ZkVerifierModule,
48
55
  accountIdHexToSs58: () => accountIdHexToSs58,
49
56
  addressToAccountIdHex: () => addressToAccountIdHex,
57
+ applyNoteStatus: () => applyNoteStatus,
50
58
  base58: () => import_base.base58,
51
59
  bigintTo32Be: () => bigintTo32Be,
52
60
  bigintTo32Le: () => bigintTo32Le,
53
61
  bigintTo32LeArr: () => bigintTo32LeArr,
62
+ buildDisclosurePublicSignals: () => buildDisclosurePublicSignals,
63
+ buildDummyTransferInput: () => buildDummyTransferInput,
54
64
  bytesToBigintLE: () => bytesToBigintLE,
65
+ computeNullifier: () => computeNullifier,
55
66
  computePathIndices: () => computePathIndices,
56
67
  connectInjectedExtension: () => import_pjs_signer.connectInjectedExtension,
57
68
  decodePrecompileCalldata: () => decodePrecompileCalldata,
69
+ decryptDisclosureSignals: () => decryptDisclosureSignals,
58
70
  decryptJson: () => decryptJson,
71
+ decryptNoteRecord: () => decryptNoteRecord,
72
+ deriveBabyJubjubKeypair: () => deriveBabyJubjubKeypair,
73
+ deriveMasterKeyBytes: () => deriveMasterKeyBytes,
59
74
  deriveOwnerPk: () => deriveOwnerPk,
60
75
  deriveSpendingKeyFromSignature: () => deriveSpendingKeyFromSignature,
61
76
  deriveSpendingKeyMessage: () => deriveSpendingKeyMessage,
77
+ deriveStealthOwnerPk: () => deriveStealthOwnerPk,
78
+ deriveStealthSk: () => deriveStealthSk,
62
79
  deriveVaultKey: () => deriveVaultKey,
63
- deriveViewingKey: () => deriveViewingKey,
80
+ deriveViewingPublicKey: () => deriveViewingPublicKey,
81
+ deriveViewingSecretKey: () => deriveViewingSecretKey,
64
82
  encryptJson: () => encryptJson,
83
+ encryptNote: () => encryptNote,
65
84
  ensureHexPrefix: () => ensureHexPrefix,
66
85
  evmAddressToAccountId: () => evmAddressToAccountId,
67
86
  evmToImplicitSubstrate: () => evmToImplicitSubstrate,
@@ -71,6 +90,10 @@ __export(index_exports, {
71
90
  formatORB: () => formatORB,
72
91
  fromBase64: () => fromBase64,
73
92
  fromHex: () => fromHex,
93
+ generateDisclosureProof: () => import_proof_generator.generateDisclosureProof,
94
+ generateFeeClaimProof: () => generateFeeClaimProof,
95
+ generateTransferProof: () => generateTransferProof,
96
+ generateUnshieldProof: () => generateUnshieldProof,
74
97
  getInjectedExtensions: () => import_pjs_signer.getInjectedExtensions,
75
98
  getPolkadotSigner: () => import_signer.getPolkadotSigner,
76
99
  getPolkadotSignerFromPjs: () => import_pjs_signer.getPolkadotSignerFromPjs,
@@ -88,6 +111,9 @@ __export(index_exports, {
88
111
  mapExtrinsicArgs: () => mapExtrinsicArgs,
89
112
  mapZkEventData: () => mapZkEventData,
90
113
  normalizeEvmAddress: () => normalizeEvmAddress,
114
+ randomBlinding: () => randomBlinding,
115
+ recoverOwnerPkPoint: () => recoverOwnerPkPoint,
116
+ selectNotes: () => selectNotes,
91
117
  shortHash: () => shortHash,
92
118
  substrateSs58ToAccountIdHex: () => substrateSs58ToAccountIdHex,
93
119
  substrateToEvm: () => substrateToEvm,
@@ -96,6 +122,7 @@ __export(index_exports, {
96
122
  toTxResult: () => toTxResult,
97
123
  truncateMiddle: () => truncateMiddle,
98
124
  tryDecryptNote: () => tryDecryptNote,
125
+ tryDecryptNoteVerbose: () => tryDecryptNoteVerbose,
99
126
  u128: () => import_substrate_bindings3.u128,
100
127
  u64: () => import_substrate_bindings3.u64,
101
128
  vaultReplacer: () => vaultReplacer,
@@ -143,6 +170,7 @@ var SubstrateClient = class _SubstrateClient {
143
170
  constructor(_papi) {
144
171
  this._papi = _papi;
145
172
  }
173
+ _papi;
146
174
  _dynamicBuilder = null;
147
175
  _extDecoder = null;
148
176
  /**
@@ -342,6 +370,14 @@ var SubstrateClient = class _SubstrateClient {
342
370
  submitAndWatch(signedHex) {
343
371
  return this._papi.submitAndWatch(signedHex);
344
372
  }
373
+ /**
374
+ * Submits a bare (unsigned) extrinsic hex and waits for finalization.
375
+ * Used for gasless private_transfer and unshield transactions.
376
+ * The bare tx hex is produced by `tx.getBareTx()` from polkadot-api.
377
+ */
378
+ async submitUnsignedAndWatch(bareTxHex) {
379
+ return this._papi.submit(bareTxHex);
380
+ }
345
381
  /**
346
382
  * Convenience: wrap raw call bytes and sign+submit in one step.
347
383
  */
@@ -393,15 +429,14 @@ var SubstrateClient = class _SubstrateClient {
393
429
  }
394
430
  static _buildDataProxy(value) {
395
431
  const formatValue = (v) => {
396
- if (v instanceof Uint8Array) return fromHex(v).toString();
432
+ if (v instanceof Uint8Array) return toHex(v);
397
433
  if (typeof v === "bigint") return v.toString();
398
434
  return String(v);
399
435
  };
400
436
  const jsonifyValue = (v) => {
401
437
  if (v === null || v === void 0) return v;
402
438
  if (typeof v === "bigint") return v.toString();
403
- if (v instanceof Uint8Array)
404
- return `0x${Array.from(v).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
439
+ if (v instanceof Uint8Array) return toHex(v);
405
440
  if (Array.isArray(v)) return v.map(jsonifyValue);
406
441
  if (typeof v === "object") {
407
442
  const obj = v;
@@ -505,11 +540,14 @@ var SubstrateClient = class _SubstrateClient {
505
540
 
506
541
  // src/evm/EvmClient.ts
507
542
  var EvmClient = class {
543
+ /** @param rpcUrl - HTTP URL of the EVM JSON-RPC endpoint (e.g. `"http://localhost:9933"`). */
508
544
  constructor(rpcUrl) {
509
545
  this.rpcUrl = rpcUrl;
510
546
  }
547
+ rpcUrl;
511
548
  /**
512
- * Performs a single JSON-RPC call.
549
+ * Performs a single JSON-RPC call and returns the typed result.
550
+ * Throws on HTTP errors, RPC-level errors, or a `null` result.
513
551
  */
514
552
  async request(method, params = []) {
515
553
  const res = await fetch(this.rpcUrl, {
@@ -529,6 +567,7 @@ var EvmClient = class {
529
567
  }
530
568
  /**
531
569
  * Performs multiple JSON-RPC calls in a single HTTP request (batch).
570
+ * Results are returned in the same order as `calls`, as a typed tuple.
532
571
  */
533
572
  async batchRequest(calls) {
534
573
  const body = calls.map((c, i) => ({
@@ -573,30 +612,22 @@ var EvmClient = class {
573
612
  const hex = await this.request("eth_gasPrice", []);
574
613
  return hexToBigint(hex);
575
614
  }
576
- /**
577
- * Submits a signed raw transaction. Returns the transaction hash.
578
- */
615
+ /** Submits a signed raw transaction. Returns the transaction hash. */
579
616
  async sendRawTransaction(signedHex) {
580
617
  return this.request("eth_sendRawTransaction", [signedHex]);
581
618
  }
582
- /**
583
- * Executes a read-only call without creating a transaction.
584
- */
619
+ /** Executes a read-only call without creating a transaction. Returns the raw ABI-encoded response. */
585
620
  async call(to, data, from) {
586
621
  const txObj = { to, data };
587
622
  if (from) txObj["from"] = from;
588
623
  return this.request("eth_call", [txObj, "latest"]);
589
624
  }
590
- /**
591
- * Estimates the gas for a transaction.
592
- */
625
+ /** Estimates the gas required for a transaction. Returns the estimate in wei as a `bigint`. */
593
626
  async estimateGas(params) {
594
627
  const hex = await this.request("eth_estimateGas", [params]);
595
628
  return hexToBigint(hex);
596
629
  }
597
- /**
598
- * Returns a transaction receipt by hash, or null if not yet mined.
599
- */
630
+ /** Returns a transaction receipt by hash, or `null` if the transaction has not been mined yet. */
600
631
  async getTransactionReceipt(txHash) {
601
632
  const res = await fetch(this.rpcUrl, {
602
633
  method: "POST",
@@ -615,6 +646,56 @@ var EvmClient = class {
615
646
  }
616
647
  return json.result ?? null;
617
648
  }
649
+ /**
650
+ * Polls `eth_getTransactionReceipt` until the transaction is included in a block.
651
+ *
652
+ * @param txHash - The transaction hash to wait for.
653
+ * @param intervalMs - Polling interval in milliseconds (default: 500).
654
+ * @param timeoutMs - Maximum time to wait in milliseconds (default: 60_000).
655
+ * @returns The transaction receipt once mined.
656
+ * @throws If the transaction is not mined within `timeoutMs` or if it reverted (`status == 0x0`).
657
+ */
658
+ async waitForReceipt(txHash, intervalMs = 500, timeoutMs = 6e4) {
659
+ const deadline = Date.now() + timeoutMs;
660
+ while (Date.now() < deadline) {
661
+ const receipt = await this.getTransactionReceipt(txHash);
662
+ if (receipt !== null) {
663
+ if (receipt["status"] === "0x0") {
664
+ let revertDetail = "";
665
+ const nodeReason = receipt["revertReason"];
666
+ if (nodeReason) revertDetail = ` | revertReason: ${nodeReason}`;
667
+ if (!revertDetail) {
668
+ try {
669
+ const blockParam = receipt["blockNumber"] ?? "latest";
670
+ const rawTx = await this.request(
671
+ "eth_getTransactionByHash",
672
+ [txHash]
673
+ ).catch(() => null);
674
+ if (rawTx) {
675
+ const calldata = rawTx["input"] ?? rawTx["data"];
676
+ if (calldata) {
677
+ const revertData = await this.request("eth_call", [
678
+ { from: rawTx["from"], to: rawTx["to"], data: calldata },
679
+ blockParam
680
+ ]).catch(
681
+ (err) => err instanceof Error ? err.message : String(err)
682
+ );
683
+ revertDetail = ` | eth_call: ${revertData}`;
684
+ }
685
+ }
686
+ } catch {
687
+ }
688
+ }
689
+ throw new Error(
690
+ `Transaction reverted on-chain: ${txHash}${revertDetail} | receipt: ${JSON.stringify(receipt)}`
691
+ );
692
+ }
693
+ return receipt;
694
+ }
695
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
696
+ }
697
+ throw new Error(`Transaction not mined within ${timeoutMs}ms: ${txHash}`);
698
+ }
618
699
  };
619
700
 
620
701
  // src/utils/format.ts
@@ -687,10 +768,13 @@ function formatORB(raw, precision = 6) {
687
768
 
688
769
  // src/evm-explorer/EvmExplorer.ts
689
770
  var EvmExplorer = class _EvmExplorer {
771
+ /** @param evm - Underlying `EvmClient` used for all RPC calls. */
690
772
  constructor(evm) {
691
773
  this.evm = evm;
692
774
  }
775
+ evm;
693
776
  // --- Blocks ---
777
+ /** Returns the `count` most recent blocks in descending order (latest first). */
694
778
  async getLatestBlocks(count = 10) {
695
779
  const latest = await this.evm.getBlockNumber();
696
780
  const nums = Array.from({ length: Math.min(count, latest + 1) }, (_, i) => latest - i);
@@ -704,10 +788,12 @@ var EvmExplorer = class _EvmExplorer {
704
788
  );
705
789
  return results.filter((b) => b !== null && !!b.hash).map((b) => this.parseBlock(b));
706
790
  }
791
+ /** Returns a single block by number or hash, or `null` if not found. */
707
792
  async getBlock(hashOrNumber) {
708
793
  const b = await this.fetchBlock(hashOrNumber, false);
709
794
  return b ? this.parseBlock(b) : null;
710
795
  }
796
+ /** Returns all transactions in a block (with receipts), or `[]` if the block is not found. */
711
797
  async getBlockTransactions(hashOrNumber) {
712
798
  try {
713
799
  const b = await this.fetchBlock(hashOrNumber, true);
@@ -720,6 +806,7 @@ var EvmExplorer = class _EvmExplorer {
720
806
  }
721
807
  }
722
808
  // --- Transactions ---
809
+ /** Returns a single transaction with its receipt, or `null` if not found. */
723
810
  async getTransaction(hash) {
724
811
  try {
725
812
  const [tx, receipt] = await Promise.all([
@@ -732,6 +819,10 @@ var EvmExplorer = class _EvmExplorer {
732
819
  return null;
733
820
  }
734
821
  }
822
+ /**
823
+ * Returns lightweight summaries of all transactions sent from or to `address`
824
+ * within the last `maxBlocks` blocks, sorted by block number descending.
825
+ */
735
826
  async getTransactionsByAddress(address, maxBlocks = 300) {
736
827
  const addr = address.toLowerCase();
737
828
  const latest = await this.evm.getBlockNumber();
@@ -783,6 +874,10 @@ var EvmExplorer = class _EvmExplorer {
783
874
  return results;
784
875
  }
785
876
  // --- Address ---
877
+ /**
878
+ * Returns aggregated on-chain data for an EVM address: balance, nonce,
879
+ * bytecode (truncated), and up to 50 recent logs from the last 5 000 blocks.
880
+ */
786
881
  async getAddressInfo(address) {
787
882
  const latest = await this.evm.getBlockNumber().catch(() => 0);
788
883
  const fromBlock = `0x${Math.max(0, latest - 5e3).toString(16)}`;
@@ -813,6 +908,7 @@ var EvmExplorer = class _EvmExplorer {
813
908
  recentLogs
814
909
  };
815
910
  }
911
+ /** Returns the native token balance of `address`, formatted as a decimal string (no symbol). */
816
912
  async getBalance(address) {
817
913
  try {
818
914
  const val = await this.evm.getBalance(address);
@@ -821,6 +917,7 @@ var EvmExplorer = class _EvmExplorer {
821
917
  return "0";
822
918
  }
823
919
  }
920
+ /** Returns the current transaction count (nonce) for `address`, or `0` on error. */
824
921
  async getNonce(address) {
825
922
  try {
826
923
  return await this.evm.getTransactionCount(address);
@@ -828,6 +925,7 @@ var EvmExplorer = class _EvmExplorer {
828
925
  return 0;
829
926
  }
830
927
  }
928
+ /** Returns `true` when `address` has non-empty deployed bytecode. */
831
929
  async getIsContract(address) {
832
930
  try {
833
931
  const code = await this.evm.request("eth_getCode", [address, "latest"]);
@@ -837,6 +935,10 @@ var EvmExplorer = class _EvmExplorer {
837
935
  }
838
936
  }
839
937
  // --- Tokens ---
938
+ /**
939
+ * Fetches ERC-20 metadata for a token contract via ABI calls.
940
+ * Returns `null` when the address does not look like an ERC-20 token.
941
+ */
840
942
  async getTokenInfo(address) {
841
943
  const addr = address.toLowerCase();
842
944
  const [name, symbol, decimals, totalSupply] = await this.evm.batchRequest([
@@ -856,6 +958,10 @@ var EvmExplorer = class _EvmExplorer {
856
958
  isErc20
857
959
  };
858
960
  }
961
+ /**
962
+ * Returns ERC-20 `Transfer` events for `address` from the last 5 000 blocks.
963
+ * When `holderAddress` is provided, restricts results to transfers sent or received by that address.
964
+ */
859
965
  async getTokenTransfers(address, holderAddress) {
860
966
  const TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
861
967
  const latest = await this.evm.getBlockNumber().catch(() => 0);
@@ -883,12 +989,14 @@ var EvmExplorer = class _EvmExplorer {
883
989
  logIndex: hexToNumber(l.logIndex)
884
990
  }));
885
991
  }
992
+ /** Returns the raw ERC-20 balance of `holderAddress` for the token at `tokenAddress` (0x-prefixed hex). */
886
993
  async getTokenBalance(tokenAddress, holderAddress) {
887
994
  const padded = holderAddress.replace(/^0x/, "").toLowerCase().padStart(64, "0");
888
995
  const result = await this.ethCall(tokenAddress, `0x70a08231${padded}`);
889
996
  return result ?? "0x0";
890
997
  }
891
998
  // --- Private: parsers ---
999
+ /** Maps a raw RPC block object to the public `EvmBlock` shape. */
892
1000
  parseBlock(b) {
893
1001
  return {
894
1002
  hash: b.hash,
@@ -901,6 +1009,7 @@ var EvmExplorer = class _EvmExplorer {
901
1009
  parentHash: b.parentHash
902
1010
  };
903
1011
  }
1012
+ /** Maps a raw RPC transaction + optional receipt to the public `EvmTransaction` shape. */
904
1013
  parseTx(tx, receipt) {
905
1014
  const parsed = {
906
1015
  hash: tx.hash,
@@ -920,6 +1029,10 @@ var EvmExplorer = class _EvmExplorer {
920
1029
  return parsed;
921
1030
  }
922
1031
  // --- Private: fetch helpers ---
1032
+ /**
1033
+ * Fetches a raw block by number or hash. Accepts a plain integer, a decimal string,
1034
+ * or a 0x-prefixed hash. Returns `null` on any RPC error.
1035
+ */
923
1036
  async fetchBlock(hashOrNumber, withTxObjects) {
924
1037
  try {
925
1038
  if (typeof hashOrNumber === "number" || /^\d+$/.test(String(hashOrNumber))) {
@@ -937,6 +1050,7 @@ var EvmExplorer = class _EvmExplorer {
937
1050
  return null;
938
1051
  }
939
1052
  }
1053
+ /** Executes a read-only `eth_call` and returns the raw hex result, or `null` on error. */
940
1054
  async ethCall(to, data) {
941
1055
  try {
942
1056
  return await this.evm.call(to, data);
@@ -945,6 +1059,7 @@ var EvmExplorer = class _EvmExplorer {
945
1059
  }
946
1060
  }
947
1061
  // --- Private static: ABI decoders ---
1062
+ /** Decodes an ABI-encoded `string` return value from a raw 0x-prefixed hex string. */
948
1063
  static decodeAbiString(hex) {
949
1064
  if (!hex || hex === "0x") return "";
950
1065
  const data = hex.startsWith("0x") ? hex.slice(2) : hex;
@@ -959,11 +1074,13 @@ var EvmExplorer = class _EvmExplorer {
959
1074
  return "";
960
1075
  }
961
1076
  }
1077
+ /** Decodes an ABI-encoded `uint256` return value to a `bigint`. */
962
1078
  static decodeAbiUint(hex) {
963
1079
  if (!hex || hex === "0x") return 0n;
964
1080
  const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
965
1081
  return BigInt(`0x${clean || "0"}`);
966
1082
  }
1083
+ /** Converts a 0x-prefixed hex number to its decimal string representation. Returns `'0'` on parse error. */
967
1084
  static hexToDecimalStr(hex) {
968
1085
  try {
969
1086
  return BigInt(hex).toString();
@@ -998,6 +1115,24 @@ var IndexerClient = class {
998
1115
  }
999
1116
  return res.json();
1000
1117
  }
1118
+ async post(path, body) {
1119
+ const controller = new AbortController();
1120
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1121
+ try {
1122
+ const res = await fetch(`${this.baseUrl}${path}`, {
1123
+ method: "POST",
1124
+ headers: { "Content-Type": "application/json" },
1125
+ body: JSON.stringify(body),
1126
+ signal: controller.signal
1127
+ });
1128
+ if (!res.ok) {
1129
+ throw new Error(`IndexerClient: HTTP ${res.status} for POST ${path}`);
1130
+ }
1131
+ return res.json();
1132
+ } finally {
1133
+ clearTimeout(timer);
1134
+ }
1135
+ }
1001
1136
  async getOrNull(path) {
1002
1137
  const res = await this._fetchResponse(path);
1003
1138
  if (res.status === 404) return null;
@@ -1033,6 +1168,21 @@ var IndexerClient = class {
1033
1168
  `/shielded/commitments/${encodeURIComponent(hex)}`
1034
1169
  );
1035
1170
  }
1171
+ /**
1172
+ * Returns a paginated list of stealth scan hints ordered ascending by leafIndex.
1173
+ * Each hint contains only the fields required for ECDH triage and decryption:
1174
+ * leafIndex, commitmentHex, assetId, ephPkHex, encryptedMemo.
1175
+ *
1176
+ * Use `sinceLeafIndex` for incremental scans (cursor = last seen leafIndex + 1).
1177
+ */
1178
+ async getScanHints(params) {
1179
+ const qs = this.buildQuery({
1180
+ page: params?.page,
1181
+ limit: params?.limit,
1182
+ since_leaf_index: params?.sinceLeafIndex
1183
+ });
1184
+ return this.get(`/shielded/scan-hints${qs}`);
1185
+ }
1036
1186
  // ─── Nullifiers ────────────────────────────────────────────────────────────
1037
1187
  /** Returns a paginated list of spent nullifiers. */
1038
1188
  async getNullifiers(params) {
@@ -1045,11 +1195,50 @@ var IndexerClient = class {
1045
1195
  `/shielded/nullifier/${encodeURIComponent(hex)}/status`
1046
1196
  );
1047
1197
  }
1198
+ /**
1199
+ * Batch-checks which of the given nullifiers are spent.
1200
+ * Returns only the nullifiers that exist in the spent set.
1201
+ * Accepts up to 100 nullifiers (0x-prefixed hex).
1202
+ */
1203
+ async getNullifiersBatch(nullifiers) {
1204
+ if (nullifiers.length === 0) return [];
1205
+ const res = await this.post("/shielded/nullifiers/batch", {
1206
+ nullifiers: nullifiers.map((n) => n.toLowerCase())
1207
+ });
1208
+ return res.data;
1209
+ }
1048
1210
  // ─── Private transfers ─────────────────────────────────────────────────────
1049
- /** Returns a paginated list of private transfer events. */
1050
- async getTransfers(params) {
1051
- const qs = this.buildQuery({ page: params?.page, limit: params?.limit });
1052
- return this.get(`/shielded/transfers${qs}`);
1211
+ /**
1212
+ * Returns temporal metadata for private transfers that spent any of the given nullifiers.
1213
+ * Only blockNumber, extrinsicIndex, timestampMs, and hash are returned — no cross-link
1214
+ * between inputs and outputs to prevent graph reconstruction.
1215
+ * Accepts up to 50 nullifiers (0x-prefixed hex).
1216
+ */
1217
+ async getTransfersByNullifiers(nullifiers) {
1218
+ if (nullifiers.length === 0) return [];
1219
+ const qs = this.buildQuery({
1220
+ nullifiers: nullifiers.map((n) => n.toLowerCase()).join(",")
1221
+ });
1222
+ const res = await this.get(
1223
+ `/shielded/transfers/by-nullifiers${qs}`
1224
+ );
1225
+ return res.data;
1226
+ }
1227
+ /**
1228
+ * Returns temporal metadata for private transfers that produced any of the given commitments.
1229
+ * Only blockNumber, extrinsicIndex, timestampMs, and hash are returned — no cross-link
1230
+ * between outputs and inputs to prevent graph reconstruction.
1231
+ * Accepts up to 50 commitments (0x-prefixed hex).
1232
+ */
1233
+ async getTransfersByCommitments(commitments) {
1234
+ if (commitments.length === 0) return [];
1235
+ const qs = this.buildQuery({
1236
+ commitments: commitments.map((c) => c.toLowerCase()).join(",")
1237
+ });
1238
+ const res = await this.get(
1239
+ `/shielded/transfers/by-commitments${qs}`
1240
+ );
1241
+ return res.data;
1053
1242
  }
1054
1243
  // ─── Unshields ─────────────────────────────────────────────────────────────
1055
1244
  /** Returns a paginated list of unshield events. */
@@ -1109,6 +1298,16 @@ var IndexerClient = class {
1109
1298
  `/address/${encodeURIComponent(address.toLowerCase())}/shielded${qs}`
1110
1299
  );
1111
1300
  }
1301
+ /**
1302
+ * Returns a paginated list of unshield events where the given address is the recipient.
1303
+ * Accepts a 0x-prefixed EVM address or an SS58 Substrate address.
1304
+ */
1305
+ async getAddressUnshields(address, params) {
1306
+ const qs = this.buildQuery({ page: params?.page, limit: params?.limit });
1307
+ return this.get(
1308
+ `/address/${encodeURIComponent(address)}/unshields${qs}`
1309
+ );
1310
+ }
1112
1311
  /**
1113
1312
  * Returns a paginated list of all shielded activity (commitments, unshields,
1114
1313
  * private transfers) associated with the given address.
@@ -1144,10 +1343,24 @@ var IndexerClient = class {
1144
1343
  }
1145
1344
  };
1146
1345
 
1147
- // src/shielded-pool/ShieldedPoolModule.ts
1346
+ // src/shielded-pool/pallet/ShieldedPoolModule.ts
1148
1347
  var import_polkadot_api2 = require("polkadot-api");
1149
1348
 
1150
1349
  // src/utils/tx.ts
1350
+ function formatDispatchError(err) {
1351
+ if (err.type === "Module") {
1352
+ const inner = err.value;
1353
+ if (inner?.type) {
1354
+ return `Module(${inner.type})`;
1355
+ }
1356
+ }
1357
+ try {
1358
+ const detail = JSON.stringify(err.value);
1359
+ return detail && detail !== "null" ? `${err.type}(${detail})` : err.type;
1360
+ } catch {
1361
+ return err.type;
1362
+ }
1363
+ }
1151
1364
  function toTxResult(payload) {
1152
1365
  const base = {
1153
1366
  txHash: payload.txHash,
@@ -1156,13 +1369,18 @@ function toTxResult(payload) {
1156
1369
  ok: payload.ok
1157
1370
  };
1158
1371
  if (!payload.ok) {
1159
- return { ...base, error: payload.dispatchError.type };
1372
+ return { ...base, error: formatDispatchError(payload.dispatchError) };
1160
1373
  }
1161
1374
  return base;
1162
1375
  }
1163
1376
  function callUnsafeTx(txEntry, ...args) {
1164
1377
  return txEntry(...args);
1165
1378
  }
1379
+ async function submitBareTx(tx, client) {
1380
+ const bareTxHex = await tx.getBareTx();
1381
+ const payload = await client.submitUnsignedAndWatch(bareTxHex);
1382
+ return toTxResult(payload);
1383
+ }
1166
1384
  function resolveTx(unsafe, pallet, call) {
1167
1385
  const u = unsafe;
1168
1386
  const p = u["tx"]?.[pallet];
@@ -1173,9 +1391,10 @@ function resolveTx(unsafe, pallet, call) {
1173
1391
  return entry;
1174
1392
  }
1175
1393
 
1176
- // src/shielded-pool/EncryptedMemo.ts
1394
+ // src/shielded-pool/protocol/EncryptedMemo.ts
1177
1395
  var import_chacha = require("@noble/ciphers/chacha.js");
1178
1396
  var import_utils = require("@noble/ciphers/utils.js");
1397
+ var import_baby_jubjub = require("@zk-kit/baby-jubjub");
1179
1398
 
1180
1399
  // src/utils/bytes.ts
1181
1400
  function bigintTo32Le(n) {
@@ -1222,77 +1441,114 @@ function computePathIndices(leafIndex, depth) {
1222
1441
  return indices;
1223
1442
  }
1224
1443
  function leHexToBigint(hex) {
1225
- const h = hex.startsWith("0x") ? hex.slice(2) : hex;
1226
- const bytes = new Uint8Array(h.length / 2);
1227
- for (let i = 0; i < bytes.length; i++) {
1228
- bytes[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
1229
- }
1230
- return bytesToBigintLE(bytes);
1444
+ return bytesToBigintLE(fromHex(hex));
1231
1445
  }
1232
1446
 
1233
- // src/shielded-pool/helpers.ts
1447
+ // src/utils/crypto-constants.ts
1448
+ var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
1449
+ var BABYJUB_SUBORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
1450
+
1451
+ // src/shielded-pool/protocol/memo.ts
1234
1452
  var import_sha2 = require("@noble/hashes/sha2.js");
1235
1453
  var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
1236
- var MEMO_PLAINTEXT_SIZE = 76;
1237
- function serializeMemo(value, ownerPk, blinding, assetId) {
1454
+ var MEMO_PLAINTEXT_SIZE = 116;
1455
+ function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk) {
1238
1456
  const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
1239
1457
  const view = new DataView(buf.buffer);
1240
1458
  view.setBigUint64(0, value & 0xffffffffffffffffn, true);
1241
- buf.set(ownerPk.slice(0, 32), 8);
1242
- buf.set(blinding.slice(0, 32), 40);
1243
- view.setUint32(72, assetId >>> 0, true);
1459
+ view.setBigUint64(8, value >> 64n & 0xffffffffffffffffn, true);
1460
+ buf.set(ownerPk.slice(0, 32), 16);
1461
+ buf.set(blinding.slice(0, 32), 48);
1462
+ view.setUint32(80, assetId >>> 0, true);
1463
+ buf.set(counterpartyPk.slice(0, 32), 84);
1244
1464
  return buf;
1245
1465
  }
1246
- function deriveEncryptionKey(viewingKey, commitment) {
1466
+ function deriveEncryptionKey(sharedSecret, commitment) {
1247
1467
  const h = import_sha2.sha256.create();
1248
- h.update(viewingKey);
1468
+ h.update(sharedSecret);
1249
1469
  h.update(commitment);
1250
1470
  h.update(KEY_DOMAIN);
1251
1471
  return h.digest();
1252
1472
  }
1253
- function toBase64(buf) {
1254
- const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
1255
- let str = "";
1256
- for (const b of bytes) str += String.fromCharCode(b);
1257
- return btoa(str);
1258
- }
1259
- function fromBase64(b64) {
1260
- const bin = atob(b64);
1261
- const out = new Uint8Array(bin.length);
1262
- for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1263
- return out;
1264
- }
1265
1473
 
1266
- // src/shielded-pool/EncryptedMemo.ts
1474
+ // src/shielded-pool/protocol/EncryptedMemo.ts
1267
1475
  var NONCE_SIZE = 12;
1268
- var ENCRYPTED_MEMO_SIZE = 104;
1476
+ var CIPHERTEXT_SIZE = 132;
1477
+ var EPH_PK_SIZE = 32;
1478
+ var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
1479
+ function bytesToBjjScalar(bytes) {
1480
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1481
+ return BigInt("0x" + hex) % BABYJUB_SUBORDER || 1n;
1482
+ }
1483
+ function parsePlaintext(nonce, ciphertextWithMac, encKey) {
1484
+ try {
1485
+ const cipher = (0, import_chacha.chacha20poly1305)(encKey, nonce);
1486
+ const plaintext = cipher.decrypt(ciphertextWithMac);
1487
+ const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
1488
+ const valueLo = view.getBigUint64(0, true);
1489
+ const valueHi = view.getBigUint64(8, true);
1490
+ const value = valueLo | valueHi << 64n;
1491
+ const ownerPk = bytesToBigintLE(plaintext.slice(16, 48));
1492
+ const blinding = bytesToBigintLE(plaintext.slice(48, 80));
1493
+ const assetId = BigInt(view.getUint32(80, true));
1494
+ const counterpartyPk = bytesToBigintLE(plaintext.slice(84, 116));
1495
+ return { value, ownerPk, blinding, assetId, counterpartyPk };
1496
+ } catch {
1497
+ return null;
1498
+ }
1499
+ }
1269
1500
  var EncryptedMemo = {
1270
1501
  /**
1271
- * Build and encrypt a memo for a note.
1502
+ * Build and encrypt a memo for a note using ECDH (v2, 168 bytes).
1272
1503
  *
1273
- * @param value Note value in planck.
1274
- * @param ownerPk 32-byte owner public key (little-endian).
1275
- * @param blinding 32-byte blinding scalar (little-endian).
1276
- * @param assetId Asset identifier.
1277
- * @param commitment 32-byte commitment bytes (little-endian).
1278
- * @param recipientVk 32-byte recipient viewing key pass `new Uint8Array(32)`
1279
- * for a publicly-readable (dummy) memo.
1280
- * @returns 104-byte encrypted memo (nonce || ciphertext).
1281
- */
1282
- encrypt(value, ownerPk, blinding, assetId, commitment, recipientVk) {
1504
+ * @param value Note value in planck.
1505
+ * @param ownerPk 32-byte owner public key (LE).
1506
+ * @param blinding 32-byte blinding scalar (LE).
1507
+ * @param assetId Asset identifier.
1508
+ * @param commitment 32-byte commitment bytes (LE).
1509
+ * @param recipientIvkPacked 32-byte LE-encoded packed BJJ viewing public key
1510
+ * (from PrivacyKeyManager.getViewingPublicKeyPacked() or
1511
+ * decoded from a privacy address).
1512
+ * Pass `new Uint8Array(32)` (all zeros) for a publicly-readable memo.
1513
+ * @param counterpartyPk 32-byte counterparty BJJ Ax. Default: all zeros.
1514
+ * @returns 168-byte encrypted memo: nonce(12) || ciphertext+MAC(124) || ephPk(32).
1515
+ */
1516
+ encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, counterpartyPk = new Uint8Array(32), ephSkOverride) {
1283
1517
  const nonce = (0, import_utils.randomBytes)(NONCE_SIZE);
1284
- const key = deriveEncryptionKey(recipientVk, commitment);
1285
- const plaintext = serializeMemo(value, ownerPk, blinding, assetId);
1286
- const cipher = (0, import_chacha.chacha20poly1305)(key, nonce);
1518
+ const plaintext = serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk);
1519
+ const isZeroKey = recipientIvkPacked.every((b) => b === 0);
1520
+ let sharedSecret;
1521
+ let ephPkPackedBytes;
1522
+ if (isZeroKey) {
1523
+ sharedSecret = new Uint8Array(32);
1524
+ ephPkPackedBytes = new Uint8Array(EPH_PK_SIZE);
1525
+ } else {
1526
+ const ephSkBytes = ephSkOverride ?? (0, import_utils.randomBytes)(32);
1527
+ if (ephSkBytes.length !== 32)
1528
+ throw new Error("EncryptedMemo.encrypt: ephSkOverride must be 32 bytes");
1529
+ const ephSkScalar = bytesToBjjScalar(ephSkBytes);
1530
+ const ephPkPoint = (0, import_baby_jubjub.mulPointEscalar)(import_baby_jubjub.Base8, ephSkScalar);
1531
+ ephPkPackedBytes = bigintTo32Le((0, import_baby_jubjub.packPoint)(ephPkPoint));
1532
+ const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
1533
+ const ivkPoint = (0, import_baby_jubjub.unpackPoint)(ivkPackedBigint);
1534
+ if (!ivkPoint)
1535
+ throw new Error("EncryptedMemo.encrypt: invalid recipient viewing public key");
1536
+ const sharedPoint = (0, import_baby_jubjub.mulPointEscalar)(ivkPoint, ephSkScalar);
1537
+ sharedSecret = bigintTo32Le(sharedPoint[0]);
1538
+ }
1539
+ const encKey = deriveEncryptionKey(sharedSecret, commitment);
1540
+ const cipher = (0, import_chacha.chacha20poly1305)(encKey, nonce);
1287
1541
  const ciphertext = cipher.encrypt(plaintext);
1288
- const result = new Uint8Array(NONCE_SIZE + ciphertext.length);
1542
+ const result = new Uint8Array(ENCRYPTED_MEMO_SIZE);
1289
1543
  result.set(nonce, 0);
1290
1544
  result.set(ciphertext, NONCE_SIZE);
1545
+ result.set(ephPkPackedBytes, NONCE_SIZE + CIPHERTEXT_SIZE);
1291
1546
  return result;
1292
1547
  },
1293
1548
  /**
1294
- * Returns a 104-byte public memo with a zero recipient viewing key.
1295
- * The memo is still readable by anyone who holds the viewing key (zeros).
1549
+ * Returns a 168-byte public memo encrypted with a zero viewing key.
1550
+ * Decryptable by anyone with `decrypt(memo, commitment, new Uint8Array(32))`.
1551
+ * Convenience alias for `encrypt(..., new Uint8Array(32))`.
1296
1552
  */
1297
1553
  encryptPublic(value, ownerPk, blinding, assetId, commitment) {
1298
1554
  return EncryptedMemo.encrypt(
@@ -1305,226 +1561,89 @@ var EncryptedMemo = {
1305
1561
  );
1306
1562
  },
1307
1563
  /**
1308
- * Returns a 104-byte zeroed dummy memo (no information, always valid on-chain).
1564
+ * Returns a 168-byte zeroed dummy memo (no information, always valid on-chain).
1309
1565
  */
1310
1566
  dummy() {
1311
1567
  return new Uint8Array(ENCRYPTED_MEMO_SIZE);
1312
1568
  },
1313
1569
  /**
1314
- * Decrypt an on-chain EncryptedMemo.
1570
+ * Validates that `bytes` is a properly-sized encrypted memo.
1571
+ * Throws an Error if the length is not ENCRYPTED_MEMO_SIZE (168 bytes).
1315
1572
  *
1316
- * Returns `null` if decryption fails wrong key, bad MAC, or malformed memo.
1317
- * Never throws; safe for scan loops.
1573
+ * Call this at system boundaries (extrinsic builders, precompile encoders)
1574
+ * to catch malformed memos before they reach the chain and fail on-chain.
1318
1575
  *
1319
- * @param memoBytes 104-byte encrypted memo.
1320
- * @param commitment 32-byte note commitment (little-endian).
1321
- * @param recipientVk 32-byte recipient viewing key.
1576
+ * @param bytes The memo bytes to validate.
1577
+ * @param context Optional context string included in the error (e.g. 'shield', 'output[0]').
1322
1578
  */
1323
- decrypt(memoBytes, commitment, recipientVk) {
1324
- if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
1325
- try {
1326
- const nonce = memoBytes.slice(0, NONCE_SIZE);
1327
- const ciphertext = memoBytes.slice(NONCE_SIZE);
1328
- const key = deriveEncryptionKey(recipientVk, commitment);
1329
- const cipher = (0, import_chacha.chacha20poly1305)(key, nonce);
1330
- const plaintext = cipher.decrypt(ciphertext);
1331
- const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
1332
- const value = view.getBigUint64(0, true);
1333
- const ownerPk = bytesToBigintLE(plaintext.slice(8, 40));
1334
- const blinding = bytesToBigintLE(plaintext.slice(40, 72));
1335
- const assetId = BigInt(view.getUint32(72, true));
1336
- return { value, ownerPk, blinding, assetId };
1337
- } catch {
1338
- return null;
1579
+ validate(bytes, context) {
1580
+ if (bytes.length !== ENCRYPTED_MEMO_SIZE) {
1581
+ const ctx = context ? ` (${context})` : "";
1582
+ throw new Error(
1583
+ `EncryptedMemo: invalid size${ctx} \u2014 expected ${ENCRYPTED_MEMO_SIZE} bytes, got ${bytes.length}`
1584
+ );
1339
1585
  }
1340
- }
1341
- };
1342
-
1343
- // src/shielded-pool/NoteBuilder.ts
1344
- var import_poseidon_lite = require("poseidon-lite");
1345
- var NoteBuilder = class {
1586
+ },
1346
1587
  /**
1347
- * Build a ZkNote from the given inputs.
1588
+ * Decrypt an on-chain EncryptedMemo using the recipient's viewing secret key.
1589
+ * Returns null if decryption fails — wrong key, bad MAC, or malformed memo.
1590
+ * Never throws; safe for scan loops.
1348
1591
  *
1349
- * @param input.value Amount in planck (required).
1350
- * @param input.assetId Asset ID default 0n (native ORB-Privacy).
1351
- * @param input.ownerPk BabyJubJub Ax default 0n.
1352
- * @param input.blinding Random scalar — defaults to BigInt(Date.now()).
1353
- * @param input.spendingKey Secret key for nullifier — default 0n.
1592
+ * @param memoBytes 168-byte encrypted memo.
1593
+ * @param commitment 32-byte note commitment (LE).
1594
+ * @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
1354
1595
  */
1355
- static async build(input) {
1356
- const value = input.value;
1357
- const assetId = input.assetId ?? 0n;
1358
- const ownerPk = input.ownerPk ?? 0n;
1359
- const blinding = input.blinding ?? BigInt(Date.now());
1360
- const spendingKey = input.spendingKey ?? 0n;
1361
- const commitment = (0, import_poseidon_lite.poseidon4)([value, assetId, ownerPk, blinding]);
1362
- const nullifier = (0, import_poseidon_lite.poseidon2)([commitment, spendingKey]);
1363
- const commitmentBytes = bigintTo32Le(commitment);
1364
- const nullifierBytes = bigintTo32Le(nullifier);
1365
- const memo = input.viewingKey !== void 0 ? Array.from(
1366
- EncryptedMemo.encrypt(
1367
- value,
1368
- bigintTo32Le(ownerPk),
1369
- bigintTo32Le(blinding),
1370
- Number(assetId),
1371
- commitmentBytes,
1372
- input.viewingKey
1373
- )
1374
- ) : Array.from(EncryptedMemo.dummy());
1375
- const note = {
1376
- value,
1377
- assetId,
1378
- ownerPk,
1379
- blinding,
1380
- spendingKey,
1381
- spent: false,
1382
- spentAt: null,
1383
- commitment,
1384
- nullifier,
1385
- commitmentHex: toHex(commitmentBytes),
1386
- nullifierHex: toHex(nullifierBytes),
1387
- memo
1388
- };
1389
- return note;
1390
- }
1596
+ decrypt(memoBytes, commitment, viewingSecretKey) {
1597
+ if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
1598
+ return EncryptedMemo._decrypt(memoBytes, commitment, viewingSecretKey);
1599
+ },
1391
1600
  /**
1392
- * Build the 104-byte encrypted memo for a note.
1601
+ * Extract the ECDH shared secret from an encrypted memo using the recipient's viewing secret key.
1393
1602
  *
1394
- * Pure TypeScript implementation no WASM dependency.
1395
- * Uses ChaCha20-Poly1305 with SHA-256 key derivation.
1603
+ * Used by NoteDecryptor to obtain the shared secret needed for stealth address derivation
1604
+ * without re-running the full decrypt path. Safe to call on any 168-byte memo.
1396
1605
  *
1397
- * @param note The ZkNote whose fields populate the plaintext.
1398
- * @param recipientVk 32-byte recipient viewing key.
1399
- * Pass `new Uint8Array(32)` (default) for a public/dummy memo.
1400
- */
1401
- static buildMemo(note, recipientVk) {
1402
- return EncryptedMemo.encrypt(
1403
- note.value,
1404
- bigintTo32Le(note.ownerPk),
1405
- bigintTo32Le(note.blinding),
1406
- Number(note.assetId),
1407
- bigintTo32Le(note.commitment),
1408
- recipientVk ?? new Uint8Array(32)
1409
- );
1410
- }
1411
- };
1412
-
1413
- // src/shielded-pool/ShieldedPoolModule.ts
1414
- var ShieldedPoolModule = class {
1415
- constructor(substrate) {
1416
- this.substrate = substrate;
1417
- }
1418
- // ─── Extrinsics ────────────────────────────────────────────────────────────
1419
- /**
1420
- * Deposits tokens into the shielded pool.
1421
- * Extrinsic: shieldedPool.shield(assetId, amount, commitment, encryptedMemo)
1422
- */
1423
- async shield(params, signer) {
1424
- const memo = params.encryptedMemo ?? EncryptedMemo.dummy();
1425
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "shield");
1426
- const tx = callUnsafeTx(
1427
- entry,
1428
- params.assetId,
1429
- params.amount.toString(),
1430
- import_polkadot_api2.Binary.fromHex(params.commitment),
1431
- import_polkadot_api2.Binary.fromBytes(memo)
1432
- );
1433
- return toTxResult(await tx.signAndSubmit(signer));
1434
- }
1435
- /**
1436
- * Build a ZkNote locally and submit shieldedPool.shield in one call.
1437
- *
1438
- * Returns both the on-chain result and the note — **save the note locally**,
1439
- * it cannot be recovered after the fact.
1606
+ * Returns `new Uint8Array(32)` (all zeros) for public/dummy memos (zero ephPk).
1607
+ * Returns `null` if the memo is malformed or the ephPk is not a valid BJJ point.
1608
+ * Never throws; safe for scan loops.
1440
1609
  *
1441
- * @param params.value Amount in planck (required).
1442
- * @param params.assetId Asset ID default 0 (native ORB-Privacy).
1443
- * @param params.ownerPk BabyJubJub Ax (default 0n).
1444
- * @param params.blinding Random blinding scalar (default BigInt(Date.now())).
1445
- * @param params.spendingKey Secret spending key (default 0n).
1446
- */
1447
- async buildAndShield(params, signer) {
1448
- const noteInput = {
1449
- value: params.value,
1450
- ...params.assetId !== void 0 && { assetId: BigInt(params.assetId) },
1451
- ...params.ownerPk !== void 0 && { ownerPk: params.ownerPk },
1452
- ...params.blinding !== void 0 && { blinding: params.blinding },
1453
- ...params.spendingKey !== void 0 && { spendingKey: params.spendingKey }
1454
- };
1455
- const note = await NoteBuilder.build(noteInput);
1456
- const memo = NoteBuilder.buildMemo(note);
1457
- const txResult = await this.shield(
1458
- {
1459
- assetId: Number(note.assetId),
1460
- amount: note.value,
1461
- commitment: note.commitmentHex,
1462
- encryptedMemo: memo
1463
- },
1464
- signer
1465
- );
1466
- return { txResult, note };
1467
- }
1468
- /**
1469
- * Withdraws tokens from the shielded pool to a public address.
1470
- * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient)
1471
- */
1472
- async unshield(params, signer) {
1473
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "unshield");
1474
- const tx = callUnsafeTx(
1475
- entry,
1476
- import_polkadot_api2.Binary.fromBytes(params.proof),
1477
- import_polkadot_api2.Binary.fromHex(params.merkleRoot),
1478
- import_polkadot_api2.Binary.fromHex(params.nullifier),
1479
- params.assetId,
1480
- params.amount.toString(),
1481
- import_polkadot_api2.Binary.fromHex(params.recipientAddress)
1482
- );
1483
- return toTxResult(await tx.signAndSubmit(signer));
1484
- }
1485
- /**
1486
- * Performs a private (shielded) transfer between two notes.
1487
- * Extrinsic: shieldedPool.privateTransfer(inputs, outputs, proof, merkleRoot)
1610
+ * @param memoBytes 168-byte encrypted memo.
1611
+ * @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
1488
1612
  */
1489
- async privateTransfer(params, signer) {
1490
- const inputs = params.inputs.map((inp) => ({
1491
- nullifier: import_polkadot_api2.Binary.fromHex(inp.nullifier),
1492
- commitment: import_polkadot_api2.Binary.fromHex(inp.commitment)
1493
- }));
1494
- const outputs = params.outputs.map((out) => ({
1495
- commitment: import_polkadot_api2.Binary.fromHex(out.commitment),
1496
- memo: import_polkadot_api2.Binary.fromBytes(out.encryptedMemo ?? EncryptedMemo.dummy())
1497
- }));
1498
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "privateTransfer");
1499
- const tx = callUnsafeTx(
1500
- entry,
1501
- inputs,
1502
- outputs,
1503
- import_polkadot_api2.Binary.fromBytes(params.proof),
1504
- import_polkadot_api2.Binary.fromHex(params.merkleRoot)
1505
- );
1506
- return toTxResult(await tx.signAndSubmit(signer));
1507
- }
1508
- /**
1509
- * Deposits multiple notes into the shielded pool in a single extrinsic.
1510
- * Extrinsic: shieldedPool.shieldBatch(operations) — max 20 items.
1511
- */
1512
- async shieldBatch(params, signer) {
1513
- const operations = params.items.map((item) => ({
1514
- assetId: item.assetId,
1515
- amount: item.amount.toString(),
1516
- commitment: import_polkadot_api2.Binary.fromHex(item.commitment),
1517
- encryptedMemo: import_polkadot_api2.Binary.fromBytes(item.encryptedMemo ?? EncryptedMemo.dummy())
1518
- }));
1519
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "shieldBatch");
1520
- const tx = callUnsafeTx(entry, operations);
1521
- return toTxResult(await tx.signAndSubmit(signer));
1613
+ extractSharedSecret(memoBytes, viewingSecretKey) {
1614
+ if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
1615
+ const ephPkPackedBytes = memoBytes.slice(NONCE_SIZE + CIPHERTEXT_SIZE);
1616
+ const ephPkPackedBigint = bytesToBigintLE(ephPkPackedBytes);
1617
+ if (ephPkPackedBigint === 0n) {
1618
+ return new Uint8Array(32);
1619
+ }
1620
+ const ephPkPoint = (0, import_baby_jubjub.unpackPoint)(ephPkPackedBigint);
1621
+ if (!ephPkPoint) return null;
1622
+ const ivskScalar = bytesToBjjScalar(viewingSecretKey);
1623
+ const sharedPoint = (0, import_baby_jubjub.mulPointEscalar)(ephPkPoint, ivskScalar);
1624
+ return bigintTo32Le(sharedPoint[0]);
1625
+ },
1626
+ /** @internal */
1627
+ _decrypt(memoBytes, commitment, viewingSecretKey) {
1628
+ const nonce = memoBytes.slice(0, NONCE_SIZE);
1629
+ const ciphertextWithMac = memoBytes.slice(NONCE_SIZE, NONCE_SIZE + CIPHERTEXT_SIZE);
1630
+ const ephPkPackedBytes = memoBytes.slice(NONCE_SIZE + CIPHERTEXT_SIZE);
1631
+ const ephPkPackedBigint = bytesToBigintLE(ephPkPackedBytes);
1632
+ let sharedSecret;
1633
+ if (ephPkPackedBigint === 0n) {
1634
+ sharedSecret = new Uint8Array(32);
1635
+ } else {
1636
+ const ephPkPoint = (0, import_baby_jubjub.unpackPoint)(ephPkPackedBigint);
1637
+ if (!ephPkPoint) return null;
1638
+ const ivskScalar = bytesToBjjScalar(viewingSecretKey);
1639
+ const sharedPoint = (0, import_baby_jubjub.mulPointEscalar)(ephPkPoint, ivskScalar);
1640
+ sharedSecret = bigintTo32Le(sharedPoint[0]);
1641
+ }
1642
+ const encKey = deriveEncryptionKey(sharedSecret, commitment);
1643
+ return parsePlaintext(nonce, ciphertextWithMac, encKey);
1522
1644
  }
1523
1645
  };
1524
1646
 
1525
- // src/account-mapping/AccountMappingModule.ts
1526
- var import_polkadot_api3 = require("polkadot-api");
1527
-
1528
1647
  // src/utils/address.ts
1529
1648
  var import_util_crypto = require("@polkadot/util-crypto");
1530
1649
  function normalizeEvmAddress(addr) {
@@ -1661,6 +1780,242 @@ function addressToAccountIdHex(addr) {
1661
1780
  return substrateSs58ToAccountIdHex(addr);
1662
1781
  }
1663
1782
 
1783
+ // src/shielded-pool/pallet/ShieldedPoolModule.ts
1784
+ var ShieldedPoolModule = class {
1785
+ constructor(substrate) {
1786
+ this.substrate = substrate;
1787
+ }
1788
+ substrate;
1789
+ // ─── Extrinsics ────────────────────────────────────────────────────────────
1790
+ /**
1791
+ * Deposits tokens into the shielded pool.
1792
+ * Extrinsic: shieldedPool.shield(assetId, amount, commitment, encryptedMemo)
1793
+ *
1794
+ * Shield is always a signed (public) transaction — the caller's address
1795
+ * appears on-chain as the depositor.
1796
+ */
1797
+ async shield(params, signer, txOptions) {
1798
+ EncryptedMemo.validate(params.encryptedMemo, "shield.encryptedMemo");
1799
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "shield");
1800
+ const tx = callUnsafeTx(entry, {
1801
+ asset_id: params.assetId,
1802
+ amount: params.amount,
1803
+ commitment: import_polkadot_api2.Binary.fromHex(params.commitment),
1804
+ encrypted_memo: import_polkadot_api2.Binary.fromBytes(params.encryptedMemo)
1805
+ });
1806
+ return toTxResult(
1807
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1808
+ );
1809
+ }
1810
+ /**
1811
+ * Withdraws tokens from the shielded pool to a public address.
1812
+ * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
1813
+ * Pass a `signer` to fall back to signed submission (e.g. for testing).
1814
+ * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee)
1815
+ */
1816
+ async unshield(params, signer, txOptions) {
1817
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "unshield");
1818
+ const recipientSs58 = accountIdHexToSs58(params.recipientAddress);
1819
+ if (!recipientSs58) throw new Error(`Invalid recipientAddress: ${params.recipientAddress}`);
1820
+ const changeCommitment = params.changeCommitment ?? "0x" + "00".repeat(32);
1821
+ let changeEncryptedMemo;
1822
+ if (params.changeEncryptedMemo && params.changeEncryptedMemo.length > 0) {
1823
+ EncryptedMemo.validate(params.changeEncryptedMemo, "changeEncryptedMemo");
1824
+ changeEncryptedMemo = import_polkadot_api2.Binary.fromBytes(params.changeEncryptedMemo);
1825
+ } else {
1826
+ changeEncryptedMemo = import_polkadot_api2.Binary.fromBytes(new Uint8Array(0));
1827
+ }
1828
+ const tx = callUnsafeTx(entry, {
1829
+ proof: import_polkadot_api2.Binary.fromBytes(params.proof),
1830
+ merkle_root: import_polkadot_api2.Binary.fromHex(params.merkleRoot),
1831
+ nullifier: import_polkadot_api2.Binary.fromHex(params.nullifier),
1832
+ asset_id: params.assetId,
1833
+ amount: params.amount,
1834
+ recipient: recipientSs58,
1835
+ fee: params.fee ?? 0n,
1836
+ change_commitment: import_polkadot_api2.Binary.fromHex(changeCommitment),
1837
+ change_encrypted_memo: changeEncryptedMemo,
1838
+ relayer: void 0
1839
+ // Option<H160> — None for direct Substrate submissions
1840
+ });
1841
+ if (signer) {
1842
+ return toTxResult(
1843
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1844
+ );
1845
+ }
1846
+ return submitBareTx(tx, this.substrate);
1847
+ }
1848
+ /**
1849
+ * Performs a private (shielded) transfer between two notes.
1850
+ * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
1851
+ * Pass a `signer` to fall back to signed submission (e.g. for testing).
1852
+ * Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee)
1853
+ */
1854
+ async privateTransfer(params, signer, txOptions) {
1855
+ const nullifiers = params.inputs.map((inp) => import_polkadot_api2.Binary.fromHex(inp.nullifier));
1856
+ const commitments = params.outputs.map((out) => import_polkadot_api2.Binary.fromHex(out.commitment));
1857
+ const memos = params.outputs.map((out, i) => {
1858
+ EncryptedMemo.validate(
1859
+ out.encryptedMemo,
1860
+ `privateTransfer.outputs[${i}].encryptedMemo`
1861
+ );
1862
+ return import_polkadot_api2.Binary.fromBytes(out.encryptedMemo);
1863
+ });
1864
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "private_transfer");
1865
+ const tx = callUnsafeTx(entry, {
1866
+ proof: import_polkadot_api2.Binary.fromBytes(params.proof),
1867
+ merkle_root: import_polkadot_api2.Binary.fromHex(params.merkleRoot),
1868
+ nullifiers,
1869
+ commitments,
1870
+ encrypted_memos: memos,
1871
+ asset_id: params.assetId,
1872
+ fee: params.fee ?? 0n,
1873
+ relayer: void 0
1874
+ // Option<H160> — None for direct Substrate submissions
1875
+ });
1876
+ if (signer) {
1877
+ return toTxResult(
1878
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1879
+ );
1880
+ }
1881
+ return submitBareTx(tx, this.substrate);
1882
+ }
1883
+ /**
1884
+ * Deposits multiple notes into the shielded pool in a single extrinsic.
1885
+ * Extrinsic: shieldedPool.shieldBatch(operations) — max 20 items.
1886
+ */
1887
+ async shieldBatch(params, signer, txOptions) {
1888
+ const operations = params.items.map((item, i) => {
1889
+ EncryptedMemo.validate(item.encryptedMemo, `shieldBatch.items[${i}].encryptedMemo`);
1890
+ return {
1891
+ assetId: item.assetId,
1892
+ amount: item.amount.toString(),
1893
+ commitment: import_polkadot_api2.Binary.fromHex(item.commitment),
1894
+ encryptedMemo: import_polkadot_api2.Binary.fromBytes(item.encryptedMemo)
1895
+ };
1896
+ });
1897
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "shield_batch");
1898
+ const tx = callUnsafeTx(entry, operations);
1899
+ return toTxResult(
1900
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1901
+ );
1902
+ }
1903
+ /**
1904
+ * Claims accrued relay fees into the shielded pool.
1905
+ * This is a SIGNED transaction — the relayer must sign it with their wallet.
1906
+ * Before calling this, generate a ZK disclosure proof with generateFeeClaimProof().
1907
+ *
1908
+ * Extrinsic: shieldedPool.claim_shielded_fees(commitment, amount, asset_id, memo, proof, public_signals)
1909
+ */
1910
+ async claimShieldedFees(params, signer, txOptions) {
1911
+ EncryptedMemo.validate(params.encryptedMemo, "claimShieldedFees.encryptedMemo");
1912
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "claim_shielded_fees");
1913
+ const tx = callUnsafeTx(entry, {
1914
+ commitment: import_polkadot_api2.Binary.fromHex(params.commitment),
1915
+ amount: params.amount,
1916
+ asset_id: params.assetId,
1917
+ encrypted_memo: import_polkadot_api2.Binary.fromBytes(params.encryptedMemo),
1918
+ proof: import_polkadot_api2.Binary.fromBytes(params.proof),
1919
+ public_signals: import_polkadot_api2.Binary.fromBytes(params.publicSignals)
1920
+ });
1921
+ return toTxResult(
1922
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1923
+ );
1924
+ }
1925
+ // ─── Selective Disclosure ──────────────────────────────────────────────────
1926
+ /**
1927
+ * Requests a selective disclosure from a target account for a specific commitment.
1928
+ * The auditor's Baby Jubjub public key is included so the note owner knows
1929
+ * which key to encrypt to when generating the proof.
1930
+ * Extrinsic: shieldedPool.request_disclosure(target, commitment, required_fields,
1931
+ * reason, auditor_bjj_pk_x, auditor_bjj_pk_y)
1932
+ */
1933
+ async requestDisclosure(params, signer, txOptions) {
1934
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "request_disclosure");
1935
+ const tx = callUnsafeTx(entry, {
1936
+ target: params.target,
1937
+ commitment: import_polkadot_api2.Binary.fromBytes(new Uint8Array(params.commitment)),
1938
+ required_fields: {
1939
+ value: params.requiredFields.value,
1940
+ asset_id: params.requiredFields.assetId,
1941
+ owner: params.requiredFields.owner
1942
+ },
1943
+ reason: import_polkadot_api2.Binary.fromText(params.reason),
1944
+ auditor_bjj_pk_x: import_polkadot_api2.Binary.fromBytes(new Uint8Array(params.auditorBjjPkX)),
1945
+ auditor_bjj_pk_y: import_polkadot_api2.Binary.fromBytes(new Uint8Array(params.auditorBjjPkY))
1946
+ });
1947
+ return toTxResult(
1948
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1949
+ );
1950
+ }
1951
+ /**
1952
+ * Submits a Groth16 ZK disclosure proof for a note commitment.
1953
+ * The proof reveals the selected fields (value, asset_id, owner_hash) on-chain.
1954
+ * Use generateDisclosureProof() + buildDisclosurePublicSignals() before calling this.
1955
+ * Extrinsic: shieldedPool.disclose(commitment, proof_bytes, public_signals, auditor)
1956
+ */
1957
+ async disclose(params, signer, txOptions) {
1958
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "disclose");
1959
+ const tx = callUnsafeTx(entry, {
1960
+ commitment: import_polkadot_api2.Binary.fromBytes(new Uint8Array(params.commitment)),
1961
+ proof_bytes: import_polkadot_api2.Binary.fromBytes(new Uint8Array(params.proofBytes)),
1962
+ public_signals: import_polkadot_api2.Binary.fromBytes(new Uint8Array(params.publicSignals)),
1963
+ auditor: params.auditor
1964
+ });
1965
+ return toTxResult(
1966
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1967
+ );
1968
+ }
1969
+ /**
1970
+ * Rejects a pending disclosure request from an auditor for a specific commitment.
1971
+ * Extrinsic: shieldedPool.reject_disclosure(auditor, commitment, reason)
1972
+ */
1973
+ async rejectDisclosure(params, signer, txOptions) {
1974
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "reject_disclosure");
1975
+ const tx = callUnsafeTx(entry, {
1976
+ auditor: params.auditor,
1977
+ commitment: import_polkadot_api2.Binary.fromBytes(new Uint8Array(params.commitment)),
1978
+ reason: import_polkadot_api2.Binary.fromText(params.reason)
1979
+ });
1980
+ return toTxResult(
1981
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1982
+ );
1983
+ }
1984
+ /**
1985
+ * Cleans up a disclosure request that has passed its expiration block.
1986
+ * Permissionless — any account can prune expired requests.
1987
+ * Extrinsic: shieldedPool.prune_expired_request(target, auditor, commitment)
1988
+ */
1989
+ async pruneExpiredRequest(params, signer, txOptions) {
1990
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "prune_expired_request");
1991
+ const tx = callUnsafeTx(entry, {
1992
+ target: params.target,
1993
+ auditor: params.auditor,
1994
+ commitment: import_polkadot_api2.Binary.fromBytes(new Uint8Array(params.commitment))
1995
+ });
1996
+ return toTxResult(
1997
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
1998
+ );
1999
+ }
2000
+ /**
2001
+ * Revokes a previously submitted voluntary disclosure record.
2002
+ * Only applies to self-disclosures (auditor = None). Auditor-requested records are permanent.
2003
+ * Extrinsic: shieldedPool.revoke_disclosure_record(commitment)
2004
+ */
2005
+ async revokeDisclosureRecord(params, signer, txOptions) {
2006
+ const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "revoke_disclosure_record");
2007
+ const tx = callUnsafeTx(entry, {
2008
+ commitment: import_polkadot_api2.Binary.fromBytes(new Uint8Array(params.commitment))
2009
+ });
2010
+ return toTxResult(
2011
+ await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
2012
+ );
2013
+ }
2014
+ };
2015
+
2016
+ // src/account-mapping/AccountMappingModule.ts
2017
+ var import_polkadot_api3 = require("polkadot-api");
2018
+
1664
2019
  // src/account-mapping/helpers.ts
1665
2020
  function mapRawScheme(raw) {
1666
2021
  if (raw === "Eip191" || raw === "eip191") return "Eip191";
@@ -1673,6 +2028,7 @@ var AccountMappingModule = class {
1673
2028
  constructor(substrate) {
1674
2029
  this.substrate = substrate;
1675
2030
  }
2031
+ substrate;
1676
2032
  // ─── Address resolution ─────────────────────────────────────────────────────
1677
2033
  /**
1678
2034
  * Returns the explicitly mapped (or fallback) Substrate AccountId32 hex for
@@ -2058,6 +2414,7 @@ var PrivacyModule = class {
2058
2414
  constructor(substrate) {
2059
2415
  this.substrate = substrate;
2060
2416
  }
2417
+ substrate;
2061
2418
  /** Returns the current Merkle tree root. */
2062
2419
  async getMerkleRoot() {
2063
2420
  return this.substrate.request("privacy_getMerkleRoot", []);
@@ -2075,14 +2432,23 @@ var PrivacyModule = class {
2075
2432
  }
2076
2433
  /**
2077
2434
  * Returns the Merkle inclusion proof for a given commitment hex,
2078
- * bundled with the current Merkle root.
2435
+ * bundled with the Merkle root.
2436
+ *
2437
+ * Uses `privacy_getMerkleProofByCommitment` which resolves root and proof
2438
+ * under the **same block hash**, guaranteeing that the returned path is
2439
+ * consistent with the returned root.
2079
2440
  */
2080
2441
  async getMerkleProofByCommitment(commitmentHex) {
2081
- const [proof, root] = await Promise.all([
2082
- this.getMerkleProof(commitmentHex),
2083
- this.getMerkleRoot()
2084
- ]);
2085
- return { ...proof, root };
2442
+ const raw = await this.substrate.request(
2443
+ "privacy_getMerkleProofByCommitment",
2444
+ [commitmentHex]
2445
+ );
2446
+ return {
2447
+ path: raw.path,
2448
+ leafIndex: raw.leaf_index,
2449
+ treeDepth: raw.tree_depth,
2450
+ root: raw.root
2451
+ };
2086
2452
  }
2087
2453
  /** Returns the spend status of a nullifier. */
2088
2454
  async getNullifierStatus(nullifier) {
@@ -2101,6 +2467,7 @@ var PrivacyModule = class {
2101
2467
  return {
2102
2468
  merkleRoot: raw.merkle_root,
2103
2469
  commitmentCount: raw.commitment_count,
2470
+ nullifierCount: raw.nullifier_count,
2104
2471
  totalBalance: raw.total_balance.toString(),
2105
2472
  assetBalances: raw.asset_balances.map(mapAssetBalance),
2106
2473
  treeDepth: raw.tree_depth
@@ -2126,6 +2493,7 @@ var ZkVerifierModule = class {
2126
2493
  constructor(substrate) {
2127
2494
  this.substrate = substrate;
2128
2495
  }
2496
+ substrate;
2129
2497
  /** Returns basic version info for all registered circuits. */
2130
2498
  async getAllCircuitVersions() {
2131
2499
  const raw = await this.substrate.request(
@@ -2144,6 +2512,48 @@ var ZkVerifierModule = class {
2144
2512
  }
2145
2513
  };
2146
2514
 
2515
+ // src/relayer/RelayerStatusModule.ts
2516
+ var RelayerStatusModule = class {
2517
+ constructor(substrate) {
2518
+ this.substrate = substrate;
2519
+ }
2520
+ substrate;
2521
+ /**
2522
+ * Returns true if the given SS58 address is a registered relayer.
2523
+ */
2524
+ async isRelayer(ss58Address) {
2525
+ return this.substrate.request("relayer_isRelayer", [ss58Address]);
2526
+ }
2527
+ /**
2528
+ * Returns the pending fees (in planck) for the given account and asset.
2529
+ * The node returns the value as a decimal string to avoid u128 overflow.
2530
+ */
2531
+ async pendingFees(ss58Address, assetId) {
2532
+ const raw = await this.substrate.request("relayer_pendingFees", [
2533
+ ss58Address,
2534
+ assetId
2535
+ ]);
2536
+ return BigInt(raw);
2537
+ }
2538
+ /**
2539
+ * Returns the registered EVM address (0x-prefixed) for the given account,
2540
+ * or null if the account is not a registered relayer.
2541
+ */
2542
+ async registeredEvmAddress(ss58Address) {
2543
+ return this.substrate.request("relayer_registeredEvmAddress", [ss58Address]);
2544
+ }
2545
+ /**
2546
+ * Convenience method: returns relayer registry info for an account.
2547
+ */
2548
+ async getRelayerInfo(ss58Address) {
2549
+ const [isRelayer, evmAddress] = await Promise.all([
2550
+ this.isRelayer(ss58Address),
2551
+ this.registeredEvmAddress(ss58Address)
2552
+ ]);
2553
+ return { isRelayer, evmAddress };
2554
+ }
2555
+ };
2556
+
2147
2557
  // src/precompiles/helpers.ts
2148
2558
  var STATIC_TYPES = /* @__PURE__ */ new Set(["uint", "bytes32", "address", "bool"]);
2149
2559
  function concat(arrays) {
@@ -2287,10 +2697,6 @@ function decodeBytes(data, slotOffset = 0) {
2287
2697
  function decodeString(data, slotOffset = 0) {
2288
2698
  return new TextDecoder().decode(decodeBytes(data, slotOffset));
2289
2699
  }
2290
- function hexToBytes(hex) {
2291
- if (hex === "0x" || hex === "") return new Uint8Array(0);
2292
- return fromHex(hex.startsWith("0x") ? hex : "0x" + hex);
2293
- }
2294
2700
 
2295
2701
  // src/precompiles/addresses.ts
2296
2702
  var PRECOMPILE_ADDR = {
@@ -2349,12 +2755,20 @@ var AM_SEL = {
2349
2755
  SET_ACCOUNT_METADATA: new Uint8Array([119, 108, 249, 255])
2350
2756
  };
2351
2757
  var SP_SEL = {
2352
- // shield(uint32,uint256,bytes32,bytes) 0x781442b9
2353
- SHIELD: new Uint8Array([120, 20, 66, 185]),
2354
- // privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[]) 0xdcd5b898
2355
- PRIVATE_TRANSFER: new Uint8Array([220, 213, 184, 152]),
2356
- // unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32) 0xdcf1bff2
2357
- UNSHIELD: new Uint8Array([220, 241, 191, 242])
2758
+ // shield(uint32,bytes32,bytes) 0x9feb22ea (payable, amount = msg.value)
2759
+ SHIELD: new Uint8Array([159, 235, 34, 234]),
2760
+ // privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256) 0x8c0f5d24
2761
+ PRIVATE_TRANSFER: new Uint8Array([140, 15, 93, 36]),
2762
+ // unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32) 0xd21d9a79
2763
+ UNSHIELD: new Uint8Array([210, 29, 154, 121]),
2764
+ // requestDisclosure(bytes32,bytes32,bool,bool,bool,bytes,bytes32,bytes32) → 0xe7022933 (caller = auditor)
2765
+ REQUEST_DISCLOSURE: new Uint8Array([231, 2, 41, 51]),
2766
+ // disclose(bytes32,bytes,bytes,bytes32) → 0xea8a4165 (caller = note owner)
2767
+ DISCLOSE: new Uint8Array([234, 138, 65, 101]),
2768
+ // rejectDisclosure(bytes32,bytes32,bytes) → 0x72b895a9 (caller = target)
2769
+ REJECT_DISCLOSURE: new Uint8Array([114, 184, 149, 169]),
2770
+ // pruneExpiredRequest(bytes32,bytes32,bytes32) → 0x0c338dcf (permissionless)
2771
+ PRUNE_EXPIRED_REQUEST: new Uint8Array([12, 51, 141, 207])
2358
2772
  };
2359
2773
  var KNOWN_PRECOMPILES = {
2360
2774
  // ── Ethereum standard (EIP) ─────────────────────────────────────────────
@@ -2397,9 +2811,13 @@ var KNOWN_PRECOMPILES = {
2397
2811
  "0x0000000000000000000000000000000000000801": {
2398
2812
  name: "ShieldedPool",
2399
2813
  functions: {
2400
- "781442b9": "shield(uint32,uint256,bytes32,bytes)",
2401
- dcd5b898: "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[])",
2402
- dcf1bff2: "unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32)"
2814
+ "9feb22ea": "shield(uint32,bytes32,bytes)",
2815
+ "8c0f5d24": "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256)",
2816
+ d21d9a79: "unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)",
2817
+ e7022933: "requestDisclosure(bytes32,bytes32,bool,bool,bool,bytes,bytes32,bytes32)",
2818
+ ea8a4165: "disclose(bytes32,bytes,bytes,bytes32)",
2819
+ "72b895a9": "rejectDisclosure(bytes32,bytes32,bytes)",
2820
+ "0c338dcf": "pruneExpiredRequest(bytes32,bytes32,bytes32)"
2403
2821
  }
2404
2822
  }
2405
2823
  };
@@ -2413,43 +2831,58 @@ var ShieldedPoolPrecompile = class {
2413
2831
  constructor(evm) {
2414
2832
  this.evm = evm;
2415
2833
  }
2834
+ evm;
2416
2835
  addr = PRECOMPILE_ADDR.SHIELDED_POOL;
2417
2836
  // ─── shield ────────────────────────────────────────────────────────────────
2418
2837
  /**
2419
- * Returns the ABI-encoded calldata for `shield(uint32, uint256, bytes32, bytes)`.
2420
- * Useful when you need to inspect or batch the calldata before sending.
2838
+ * Returns the ABI-encoded calldata for `shield(uint32, bytes32, bytes)`.
2839
+ * The token amount must be sent as `msg.value` (the `value` field of the EVM
2840
+ * transaction) — this is what MetaMask and other wallets display to the user.
2421
2841
  */
2422
2842
  buildShieldCalldata(params) {
2423
- const memo = params.encryptedMemo ?? EncryptedMemo.dummy();
2843
+ EncryptedMemo.validate(params.encryptedMemo, "buildShieldCalldata.encryptedMemo");
2424
2844
  const commitment = fromHex(params.commitment);
2425
2845
  return encodeHex(
2426
2846
  SP_SEL.SHIELD,
2427
2847
  { type: "uint", value: BigInt(params.assetId) },
2428
- { type: "uint", value: params.amount },
2429
2848
  { type: "bytes32", value: commitment },
2430
- { type: "bytes", value: memo }
2849
+ { type: "bytes", value: params.encryptedMemo }
2431
2850
  );
2432
2851
  }
2433
2852
  /**
2434
- * Deposits tokens into the shielded pool from an EVM transaction.
2853
+ * Deposits tokens into the shielded pool from a payable EVM transaction.
2435
2854
  *
2436
- * The EVM caller's address is deterministically mapped to a Substrate
2437
- * AccountId32 (`H160 ++ [0x00; 12]`). The pool deducts from that account.
2855
+ * The token amount is sent as `msg.value` so EVM wallets (MetaMask, etc.) display
2856
+ * the correct amount on the confirmation screen. The precompile dispatches
2857
+ * `shieldedPool.shield` with its own address as origin, so the funds flow:
2858
+ * caller → precompile (via msg.value, handled by EVM)
2859
+ * precompile → pool (via pallet transfer)
2860
+ * This avoids double-deduction while keeping the displayed amount accurate.
2438
2861
  *
2439
2862
  * Extrinsic: `shieldedPool.shield(assetId, amount, commitment, encryptedMemo)`
2440
2863
  */
2441
2864
  async shield(params, signer) {
2442
- return signer({ to: this.addr, data: this.buildShieldCalldata(params) });
2865
+ return signer({
2866
+ to: this.addr,
2867
+ data: this.buildShieldCalldata(params),
2868
+ value: params.amount
2869
+ });
2443
2870
  }
2444
2871
  // ─── privateTransfer ───────────────────────────────────────────────────────
2445
2872
  /**
2446
2873
  * Returns the ABI-encoded calldata for
2447
- * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[])`.
2874
+ * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[], uint32, uint256)`.
2448
2875
  */
2449
2876
  buildPrivateTransferCalldata(params) {
2450
2877
  const nullifiers = params.inputs.map((i) => fromHex(i.nullifier));
2451
2878
  const commitments = params.outputs.map((o) => fromHex(o.commitment));
2452
- const memos = params.outputs.map((o) => o.encryptedMemo ?? EncryptedMemo.dummy());
2879
+ const memos = params.outputs.map((o, i) => {
2880
+ EncryptedMemo.validate(
2881
+ o.encryptedMemo,
2882
+ `buildPrivateTransferCalldata.outputs[${i}].encryptedMemo`
2883
+ );
2884
+ return o.encryptedMemo;
2885
+ });
2453
2886
  const root = fromHex(params.merkleRoot);
2454
2887
  return encodeHex(
2455
2888
  SP_SEL.PRIVATE_TRANSFER,
@@ -2457,7 +2890,9 @@ var ShieldedPoolPrecompile = class {
2457
2890
  { type: "bytes32", value: root },
2458
2891
  { type: "bytes32[]", value: nullifiers },
2459
2892
  { type: "bytes32[]", value: commitments },
2460
- { type: "bytes[]", value: memos }
2893
+ { type: "bytes[]", value: memos },
2894
+ { type: "uint", value: BigInt(params.assetId) },
2895
+ { type: "uint", value: params.fee ?? 0n }
2461
2896
  );
2462
2897
  }
2463
2898
  /**
@@ -2485,6 +2920,9 @@ var ShieldedPoolPrecompile = class {
2485
2920
  const recipientBytes = fromHex(
2486
2921
  "0x" + (recipientRaw.length === 64 ? recipientRaw : recipientRaw.padEnd(64, "0"))
2487
2922
  );
2923
+ const changeCommitmentHex = params.changeCommitment ?? "0x" + "00".repeat(32);
2924
+ const changeCommitment = fromHex(changeCommitmentHex);
2925
+ const changeEncryptedMemo = params.changeEncryptedMemo ?? new Uint8Array();
2488
2926
  return encodeHex(
2489
2927
  SP_SEL.UNSHIELD,
2490
2928
  { type: "bytes", value: proof },
@@ -2492,7 +2930,10 @@ var ShieldedPoolPrecompile = class {
2492
2930
  { type: "bytes32", value: nullifier },
2493
2931
  { type: "uint", value: BigInt(params.assetId) },
2494
2932
  { type: "uint", value: params.amount },
2495
- { type: "bytes32", value: recipientBytes }
2933
+ { type: "bytes32", value: recipientBytes },
2934
+ { type: "uint", value: params.fee ?? 0n },
2935
+ { type: "bytes32", value: changeCommitment },
2936
+ { type: "bytes", value: changeEncryptedMemo }
2496
2937
  );
2497
2938
  }
2498
2939
  /**
@@ -2520,24 +2961,151 @@ var ShieldedPoolPrecompile = class {
2520
2961
  });
2521
2962
  }
2522
2963
  /**
2523
- * Estimates the EVM gas for a `privateTransfer` call.
2964
+ * Estimates the EVM gas for a `privateTransfer` call.
2965
+ */
2966
+ async estimatePrivateTransferGas(params, from) {
2967
+ return this.evm.estimateGas({
2968
+ from,
2969
+ to: this.addr,
2970
+ data: this.buildPrivateTransferCalldata(params)
2971
+ });
2972
+ }
2973
+ /**
2974
+ * Estimates the EVM gas for an `unshield` call.
2975
+ */
2976
+ async estimateUnshieldGas(params, from) {
2977
+ return this.evm.estimateGas({
2978
+ from,
2979
+ to: this.addr,
2980
+ data: this.buildUnshieldCalldata(params)
2981
+ });
2982
+ }
2983
+ // ─── requestDisclosure ─────────────────────────────────────────────────────
2984
+ /**
2985
+ * Returns the ABI-encoded calldata for
2986
+ * `requestDisclosure(bytes32,bytes32,bool,bool,bool,bytes,bytes32,bytes32)`.
2987
+ *
2988
+ * The **EVM caller** of the resulting transaction is treated as the **auditor**
2989
+ * on-chain. No explicit auditor argument is needed.
2990
+ */
2991
+ buildRequestDisclosureCalldata(params) {
2992
+ if (params.auditorBjjPkX.length !== 32)
2993
+ throw new RangeError("requestDisclosure: auditorBjjPkX must be 32 bytes");
2994
+ if (params.auditorBjjPkY.length !== 32)
2995
+ throw new RangeError("requestDisclosure: auditorBjjPkY must be 32 bytes");
2996
+ const target = fromHex(params.target);
2997
+ const commitment = fromHex(params.commitment);
2998
+ const reasonBytes = new TextEncoder().encode(params.reason);
2999
+ return encodeHex(
3000
+ SP_SEL.REQUEST_DISCLOSURE,
3001
+ { type: "bytes32", value: target },
3002
+ { type: "bytes32", value: commitment },
3003
+ { type: "bool", value: params.disclosedValue },
3004
+ { type: "bool", value: params.disclosedAssetId },
3005
+ { type: "bool", value: params.disclosedOwner },
3006
+ { type: "bytes", value: reasonBytes },
3007
+ { type: "bytes32", value: params.auditorBjjPkX },
3008
+ { type: "bytes32", value: params.auditorBjjPkY }
3009
+ );
3010
+ }
3011
+ /**
3012
+ * Requests selective disclosure of a specific commitment.
3013
+ *
3014
+ * The EVM caller is recorded as the auditor on-chain. The note owner can
3015
+ * respond with `disclose()` or reject with `rejectDisclosure()`.
3016
+ *
3017
+ * Extrinsic: `shieldedPool.requestDisclosure(target, commitment, requiredFields, reason, bjjPkX, bjjPkY)`
3018
+ */
3019
+ async requestDisclosure(params, signer) {
3020
+ return signer({ to: this.addr, data: this.buildRequestDisclosureCalldata(params) });
3021
+ }
3022
+ // ─── disclose ──────────────────────────────────────────────────────────────
3023
+ /**
3024
+ * Returns the ABI-encoded calldata for `disclose(bytes32,bytes,bytes,bytes32)`.
3025
+ *
3026
+ * The **EVM caller** is treated as the **note owner** on-chain.
3027
+ * `params.proofBytes` must be exactly 128 bytes; `params.publicSignals` exactly 256 bytes.
3028
+ */
3029
+ buildDiscloseCalldata(params) {
3030
+ if (params.proofBytes.length !== 128)
3031
+ throw new RangeError("disclose: proofBytes must be exactly 128 bytes");
3032
+ if (params.publicSignals.length !== 256)
3033
+ throw new RangeError("disclose: publicSignals must be exactly 256 bytes");
3034
+ const commitment = fromHex(params.commitment);
3035
+ const auditor = fromHex(params.auditor);
3036
+ return encodeHex(
3037
+ SP_SEL.DISCLOSE,
3038
+ { type: "bytes32", value: commitment },
3039
+ { type: "bytes", value: params.proofBytes },
3040
+ { type: "bytes", value: params.publicSignals },
3041
+ { type: "bytes32", value: auditor }
3042
+ );
3043
+ }
3044
+ /**
3045
+ * Submits a selective disclosure proof for a commitment.
3046
+ *
3047
+ * The EVM caller is treated as the note owner on-chain. The Groth16 proof is
3048
+ * verified by the runtime; on success the encrypted signals are stored for
3049
+ * the auditor to decrypt off-chain.
3050
+ *
3051
+ * Extrinsic: `shieldedPool.disclose(commitment, proofBytes, publicSignals, auditor)`
3052
+ */
3053
+ async disclose(params, signer) {
3054
+ return signer({ to: this.addr, data: this.buildDiscloseCalldata(params) });
3055
+ }
3056
+ // ─── rejectDisclosure ──────────────────────────────────────────────────────
3057
+ /**
3058
+ * Returns the ABI-encoded calldata for `rejectDisclosure(bytes32,bytes32,bytes)`.
3059
+ *
3060
+ * The **EVM caller** is treated as the **target** (note owner) on-chain.
3061
+ */
3062
+ buildRejectDisclosureCalldata(params) {
3063
+ const auditor = fromHex(params.auditor);
3064
+ const commitment = fromHex(params.commitment);
3065
+ const reasonBytes = new TextEncoder().encode(params.reason);
3066
+ return encodeHex(
3067
+ SP_SEL.REJECT_DISCLOSURE,
3068
+ { type: "bytes32", value: auditor },
3069
+ { type: "bytes32", value: commitment },
3070
+ { type: "bytes", value: reasonBytes }
3071
+ );
3072
+ }
3073
+ /**
3074
+ * Rejects a pending disclosure request.
3075
+ *
3076
+ * The EVM caller is treated as the note owner (target) on-chain.
3077
+ *
3078
+ * Extrinsic: `shieldedPool.rejectDisclosure(auditor, commitment, reason)`
3079
+ */
3080
+ async rejectDisclosure(params, signer) {
3081
+ return signer({ to: this.addr, data: this.buildRejectDisclosureCalldata(params) });
3082
+ }
3083
+ // ─── pruneExpiredRequest ───────────────────────────────────────────────────
3084
+ /**
3085
+ * Returns the ABI-encoded calldata for `pruneExpiredRequest(bytes32,bytes32,bytes32)`.
3086
+ *
3087
+ * Permissionless: any EVM caller can prune an expired request.
2524
3088
  */
2525
- async estimatePrivateTransferGas(params, from) {
2526
- return this.evm.estimateGas({
2527
- from,
2528
- to: this.addr,
2529
- data: this.buildPrivateTransferCalldata(params)
2530
- });
3089
+ buildPruneExpiredRequestCalldata(params) {
3090
+ const target = fromHex(params.target);
3091
+ const auditor = fromHex(params.auditor);
3092
+ const commitment = fromHex(params.commitment);
3093
+ return encodeHex(
3094
+ SP_SEL.PRUNE_EXPIRED_REQUEST,
3095
+ { type: "bytes32", value: target },
3096
+ { type: "bytes32", value: auditor },
3097
+ { type: "bytes32", value: commitment }
3098
+ );
2531
3099
  }
2532
3100
  /**
2533
- * Estimates the EVM gas for an `unshield` call.
3101
+ * Removes an expired disclosure request from storage.
3102
+ *
3103
+ * Permissionless: any EVM account can call this once `expires_at` has passed.
3104
+ *
3105
+ * Extrinsic: `shieldedPool.pruneExpiredRequest(target, auditor, commitment)`
2534
3106
  */
2535
- async estimateUnshieldGas(params, from) {
2536
- return this.evm.estimateGas({
2537
- from,
2538
- to: this.addr,
2539
- data: this.buildUnshieldCalldata(params)
2540
- });
3107
+ async pruneExpiredRequest(params, signer) {
3108
+ return signer({ to: this.addr, data: this.buildPruneExpiredRequestCalldata(params) });
2541
3109
  }
2542
3110
  };
2543
3111
 
@@ -2546,6 +3114,7 @@ var AccountMappingPrecompile = class {
2546
3114
  constructor(evm) {
2547
3115
  this.evm = evm;
2548
3116
  }
3117
+ evm;
2549
3118
  addr = PRECOMPILE_ADDR.ACCOUNT_MAPPING;
2550
3119
  // ─── Read-only ─────────────────────────────────────────────────────────────
2551
3120
  /**
@@ -2557,7 +3126,7 @@ var AccountMappingPrecompile = class {
2557
3126
  async resolveAlias(alias) {
2558
3127
  try {
2559
3128
  const data = encodeHex(AM_SEL.RESOLVE_ALIAS, { type: "string", value: alias });
2560
- const raw = hexToBytes(await this.evm.call(this.addr, data));
3129
+ const raw = fromHex(await this.evm.call(this.addr, data));
2561
3130
  if (raw.length < 64) return null;
2562
3131
  const owner = decodeAddress2(raw, 0);
2563
3132
  const evm = decodeAddress2(raw, 32);
@@ -2580,7 +3149,7 @@ var AccountMappingPrecompile = class {
2580
3149
  type: "address",
2581
3150
  value: normalizeEvmAddress(evmAddress)
2582
3151
  });
2583
- const raw = hexToBytes(await this.evm.call(this.addr, data));
3152
+ const raw = fromHex(await this.evm.call(this.addr, data));
2584
3153
  if (raw.length === 0) return null;
2585
3154
  const alias = decodeString(raw, 0);
2586
3155
  return alias.length > 0 ? alias : null;
@@ -2600,7 +3169,7 @@ var AccountMappingPrecompile = class {
2600
3169
  { type: "string", value: alias },
2601
3170
  { type: "bytes32", value: commitmentBytes }
2602
3171
  );
2603
- const raw = hexToBytes(await this.evm.call(this.addr, data));
3172
+ const raw = fromHex(await this.evm.call(this.addr, data));
2604
3173
  if (raw.length < 32) return false;
2605
3174
  return decodeBool(raw, 0);
2606
3175
  } catch {
@@ -2805,6 +3374,7 @@ var CryptoPrecompiles = class {
2805
3374
  constructor(evm) {
2806
3375
  this.evm = evm;
2807
3376
  }
3377
+ evm;
2808
3378
  // ─── ECRecover (0x0001) ───────────────────────────────────────────────────
2809
3379
  /**
2810
3380
  * Recovers the Ethereum address from an ECDSA signature.
@@ -2820,7 +3390,7 @@ var CryptoPrecompiles = class {
2820
3390
  input[63] = v;
2821
3391
  input.set(r.slice(0, 32), 64);
2822
3392
  input.set(s.slice(0, 32), 96);
2823
- const raw = hexToBytes(await this.evm.call(PRECOMPILE_ADDR.EC_RECOVER, toHex(input)));
3393
+ const raw = fromHex(await this.evm.call(PRECOMPILE_ADDR.EC_RECOVER, toHex(input)));
2824
3394
  if (raw.length < 32) return "0x" + "00".repeat(20);
2825
3395
  return "0x" + toHex(raw.slice(12, 32)).slice(2);
2826
3396
  }
@@ -2836,7 +3406,7 @@ var CryptoPrecompiles = class {
2836
3406
  input[63] = v;
2837
3407
  input.set(r.slice(0, 32), 64);
2838
3408
  input.set(s.slice(0, 32), 96);
2839
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.EC_RECOVER_PUBKEY, toHex(input)));
3409
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.EC_RECOVER_PUBKEY, toHex(input)));
2840
3410
  }
2841
3411
  // ─── SHA-256 (0x0002) ─────────────────────────────────────────────────────
2842
3412
  /**
@@ -2844,7 +3414,7 @@ var CryptoPrecompiles = class {
2844
3414
  * Returns a 32-byte digest.
2845
3415
  */
2846
3416
  async sha256(data) {
2847
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.SHA256, toHex(data)));
3417
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.SHA256, toHex(data)));
2848
3418
  }
2849
3419
  // ─── RIPEMD-160 (0x0003) ──────────────────────────────────────────────────
2850
3420
  /**
@@ -2852,7 +3422,7 @@ var CryptoPrecompiles = class {
2852
3422
  * Returns the 20-byte digest right-padded to 32 bytes (standard ABI output).
2853
3423
  */
2854
3424
  async ripemd160(data) {
2855
- const raw = hexToBytes(await this.evm.call(PRECOMPILE_ADDR.RIPEMD160, toHex(data)));
3425
+ const raw = fromHex(await this.evm.call(PRECOMPILE_ADDR.RIPEMD160, toHex(data)));
2856
3426
  return raw.length >= 32 ? raw.slice(12, 32) : raw;
2857
3427
  }
2858
3428
  // ─── Identity (0x0004) ────────────────────────────────────────────────────
@@ -2861,7 +3431,7 @@ var CryptoPrecompiles = class {
2861
3431
  * Mainly useful for gas benchmarking.
2862
3432
  */
2863
3433
  async identity(data) {
2864
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.IDENTITY, toHex(data)));
3434
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.IDENTITY, toHex(data)));
2865
3435
  }
2866
3436
  // ─── SHA3-FIPS-256 / Keccak-256 (0x0400) ─────────────────────────────────
2867
3437
  /**
@@ -2869,7 +3439,7 @@ var CryptoPrecompiles = class {
2869
3439
  * Returns a 32-byte digest.
2870
3440
  */
2871
3441
  async keccak256(data) {
2872
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.SHA3_FIPS256, toHex(data)));
3442
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.SHA3_FIPS256, toHex(data)));
2873
3443
  }
2874
3444
  // ─── Curve25519 / Ristretto (0x0402, 0x0403) ─────────────────────────────
2875
3445
  /**
@@ -2892,7 +3462,7 @@ var CryptoPrecompiles = class {
2892
3462
  }
2893
3463
  input.set(pt, i * 32);
2894
3464
  }
2895
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.CURVE25519_ADD, toHex(input)));
3465
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.CURVE25519_ADD, toHex(input)));
2896
3466
  }
2897
3467
  /**
2898
3468
  * Multiplies a Ristretto compressed point by a scalar via EVM precompile.
@@ -2908,39 +3478,44 @@ var CryptoPrecompiles = class {
2908
3478
  const input = new Uint8Array(64);
2909
3479
  input.set(scalar, 0);
2910
3480
  input.set(point, 32);
2911
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.CURVE25519_SCALAR_MUL, toHex(input)));
3481
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.CURVE25519_SCALAR_MUL, toHex(input)));
2912
3482
  }
2913
3483
  };
2914
3484
 
2915
3485
  // src/client/OrbinumClient.ts
2916
3486
  var OrbinumClient = class _OrbinumClient {
2917
- /** Raw access to the Substrate WebSocket connection and RPC. */
3487
+ /** Raw Substrate WebSocket connection use for custom RPC calls or low-level access. */
2918
3488
  substrate;
2919
- /** Raw access to the EVM HTTP JSON-RPC endpoint (if configured). */
3489
+ /** Raw EVM HTTP JSON-RPC client. `null` when `evmRpc` is not configured. */
2920
3490
  evm;
2921
3491
  /**
2922
- * High-level EVM block and transaction explorer (if `evmRpc` is configured).
3492
+ * High-level EVM block and transaction explorer.
2923
3493
  * Provides enriched queries for blocks, transactions, addresses, and token transfers.
3494
+ * `null` when `evmRpc` is not configured.
2924
3495
  */
2925
3496
  evmExplorer;
2926
3497
  /**
2927
- * HTTP client for the Orbinum indexer REST API (if `indexerUrl` is configured).
3498
+ * HTTP client for the Orbinum indexer REST API.
2928
3499
  * Provides paginated access to indexed blocks, extrinsics, shielded events, and nullifiers.
3500
+ * `null` when `indexerUrl` is not configured.
2929
3501
  */
2930
3502
  indexer;
2931
- /** Shielded-pool operations: shield, unshield, privateTransfer, and merkle queries. */
3503
+ /** Shielded-pool extrinsics and Merkle tree queries (`shield`, `unshield`, `privateTransfer`, …). */
2932
3504
  shieldedPool;
2933
- /** Account mapping: aliases, chain links, metadata, marketplace, and identity extrinsics. */
3505
+ /** Account-mapping extrinsics: aliases, chain links, metadata, marketplace, and identity. */
2934
3506
  accountMapping;
2935
- /** Typed access to Orbinum `privacy_*` RPC endpoints. */
3507
+ /** Typed access to `privacy_*` custom RPC endpoints. */
2936
3508
  privacy;
2937
- /** Typed access to zkVerifier_* RPC endpoints. */
3509
+ /** Typed access to `zkVerifier_*` custom RPC endpoints. */
2938
3510
  zkVerifier;
3511
+ /** Typed access to `relayer_*` RPC endpoints (registry lookup and pending fee queries). */
3512
+ relayerStatus;
2939
3513
  /**
2940
- * EVM precompiles: shielded pool + account mapping callable from an EVM wallet.
2941
- * Only available when `evmRpc` is configured. Methods throw if `evm` is null.
3514
+ * Precompile modules for interacting with Orbinum contracts from an EVM wallet.
3515
+ * `null` when `evmRpc` is not configured. Methods on each sub-module throw if `evm` is `null`.
2942
3516
  */
2943
3517
  precompiles;
3518
+ /** @internal Use `OrbinumClient.connect()` to obtain an instance. */
2944
3519
  constructor(substrate, evm, indexer) {
2945
3520
  this.substrate = substrate;
2946
3521
  this.evm = evm;
@@ -2950,6 +3525,7 @@ var OrbinumClient = class _OrbinumClient {
2950
3525
  this.accountMapping = new AccountMappingModule(substrate);
2951
3526
  this.privacy = new PrivacyModule(substrate);
2952
3527
  this.zkVerifier = new ZkVerifierModule(substrate);
3528
+ this.relayerStatus = new RelayerStatusModule(substrate);
2953
3529
  this.precompiles = evm ? {
2954
3530
  shieldedPool: new ShieldedPoolPrecompile(evm),
2955
3531
  accountMapping: new AccountMappingPrecompile(evm),
@@ -2957,8 +3533,10 @@ var OrbinumClient = class _OrbinumClient {
2957
3533
  } : null;
2958
3534
  }
2959
3535
  /**
2960
- * Connects to an Orbinum node and returns a ready-to-use `OrbinumClient`.
2961
- * Throws if the Substrate node is unreachable within `connectTimeoutMs`.
3536
+ * Creates and connects an `OrbinumClient` from the given configuration.
3537
+ *
3538
+ * Establishes the Substrate WebSocket connection and, if configured, instantiates
3539
+ * the EVM and indexer clients. Throws if the node is unreachable within `connectTimeoutMs`.
2962
3540
  */
2963
3541
  static async connect(config) {
2964
3542
  const substrate = await SubstrateClient.connect(
@@ -2969,7 +3547,7 @@ var OrbinumClient = class _OrbinumClient {
2969
3547
  const indexer = config.indexerUrl ? new IndexerClient({ baseUrl: config.indexerUrl }) : null;
2970
3548
  return new _OrbinumClient(substrate, evm, indexer);
2971
3549
  }
2972
- /** Closes the WebSocket connection to the Substrate node. */
3550
+ /** Closes the underlying Substrate WebSocket connection and releases all resources. */
2973
3551
  destroy() {
2974
3552
  this.substrate.destroy();
2975
3553
  }
@@ -2998,6 +3576,7 @@ var OrbinumClientProvider = class {
2998
3576
  _reconnectAttempt = 0;
2999
3577
  // ─── Events ─────────────────────────────────────────────────────────────
3000
3578
  _listeners = /* @__PURE__ */ new Set();
3579
+ /** Creates a new provider with the given configuration. Does not connect automatically — call `connect()` to initiate. */
3001
3580
  constructor(config) {
3002
3581
  this.config = config;
3003
3582
  this.connectTimeoutMs = config.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
@@ -3007,9 +3586,11 @@ var OrbinumClientProvider = class {
3007
3586
  this.reconnectMaxMs = config.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
3008
3587
  }
3009
3588
  // ─── Status ─────────────────────────────────────────────────────────────
3589
+ /** Current connection status. Reflects the last state set by the provider internals. */
3010
3590
  get status() {
3011
3591
  return this._status;
3012
3592
  }
3593
+ /** Updates internal status and notifies all registered listeners. Swallows listener exceptions to avoid cascading failures. */
3013
3594
  setStatus(status, error) {
3014
3595
  this._status = status;
3015
3596
  const event = { status, ...error ? { error } : {} };
@@ -3020,6 +3601,10 @@ var OrbinumClientProvider = class {
3020
3601
  }
3021
3602
  });
3022
3603
  }
3604
+ /**
3605
+ * Registers a listener that is called on every status transition.
3606
+ * Returns an unsubscribe function — call it to stop receiving events.
3607
+ */
3023
3608
  onStatusChange(listener) {
3024
3609
  this._listeners.add(listener);
3025
3610
  return () => {
@@ -3027,10 +3612,18 @@ var OrbinumClientProvider = class {
3027
3612
  };
3028
3613
  }
3029
3614
  // ─── Lifecycle ──────────────────────────────────────────────────────────
3615
+ /**
3616
+ * Initiates the first connection attempt. No-op if the provider is not in `'idle'` state.
3617
+ * Call this once after constructing the provider.
3618
+ */
3030
3619
  connect() {
3031
3620
  if (this._status !== "idle") return;
3032
3621
  this.startConnectAttempt();
3033
3622
  }
3623
+ /**
3624
+ * Tears down the active client and any pending reconnect timers,
3625
+ * then resets the provider back to `'idle'` so `connect()` can be called again.
3626
+ */
3034
3627
  reset() {
3035
3628
  this.cancelReconnect();
3036
3629
  this.teardownClient();
@@ -3038,6 +3631,7 @@ var OrbinumClientProvider = class {
3038
3631
  this.setStatus("idle");
3039
3632
  }
3040
3633
  // ─── Internal connection flow ───────────────────────────────────────────
3634
+ /** Transitions to `'connecting'`, kicks off `attemptConnect`, and schedules a reconnect if it fails. */
3041
3635
  startConnectAttempt() {
3042
3636
  this.setStatus("connecting");
3043
3637
  this._connectingPromise = this.attemptConnect();
@@ -3045,6 +3639,11 @@ var OrbinumClientProvider = class {
3045
3639
  if (this._status !== "idle") this.scheduleReconnect();
3046
3640
  });
3047
3641
  }
3642
+ /**
3643
+ * Performs a single connection attempt race against `connectTimeoutMs`.
3644
+ * On success: stores the client, starts the heartbeat, and returns it.
3645
+ * On failure: destroys any orphaned client and transitions to `'disconnected'`.
3646
+ */
3048
3647
  async attemptConnect() {
3049
3648
  let timeoutId = null;
3050
3649
  let orphanClient = null;
@@ -3092,6 +3691,7 @@ var OrbinumClientProvider = class {
3092
3691
  }
3093
3692
  }
3094
3693
  // ─── Heartbeat ──────────────────────────────────────────────────────────
3694
+ /** Starts the periodic heartbeat loop. Replaces any existing timer. */
3095
3695
  startHeartbeat() {
3096
3696
  this.stopHeartbeat();
3097
3697
  this._heartbeatTimer = setInterval(async () => {
@@ -3104,12 +3704,17 @@ var OrbinumClientProvider = class {
3104
3704
  }
3105
3705
  }, this.heartbeatIntervalMs);
3106
3706
  }
3707
+ /** Clears the heartbeat interval timer if active. */
3107
3708
  stopHeartbeat() {
3108
3709
  if (this._heartbeatTimer) {
3109
3710
  clearInterval(this._heartbeatTimer);
3110
3711
  this._heartbeatTimer = null;
3111
3712
  }
3112
3713
  }
3714
+ /**
3715
+ * Sends a `system_health` RPC ping and waits up to `heartbeatTimeoutMs`.
3716
+ * Returns `true` if the node responds in time, `false` otherwise.
3717
+ */
3113
3718
  async probe() {
3114
3719
  if (!this._orbinumClient) return false;
3115
3720
  try {
@@ -3125,6 +3730,10 @@ var OrbinumClientProvider = class {
3125
3730
  }
3126
3731
  }
3127
3732
  // ─── Reconnection ───────────────────────────────────────────────────────
3733
+ /**
3734
+ * Schedules the next connection attempt using exponential backoff
3735
+ * (capped at `reconnectMaxMs`), then transitions to `'reconnecting'`.
3736
+ */
3128
3737
  scheduleReconnect() {
3129
3738
  if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
3130
3739
  const delay = Math.min(
@@ -3138,6 +3747,7 @@ var OrbinumClientProvider = class {
3138
3747
  if (this._status !== "idle") this.startConnectAttempt();
3139
3748
  }, delay);
3140
3749
  }
3750
+ /** Clears any pending reconnect timer without triggering a new attempt. */
3141
3751
  cancelReconnect() {
3142
3752
  if (this._reconnectTimer) {
3143
3753
  clearTimeout(this._reconnectTimer);
@@ -3145,6 +3755,7 @@ var OrbinumClientProvider = class {
3145
3755
  }
3146
3756
  }
3147
3757
  // ─── Client teardown ────────────────────────────────────────────────────
3758
+ /** Stops the heartbeat, destroys the active client, and clears all in-progress promises. */
3148
3759
  teardownClient() {
3149
3760
  this.stopHeartbeat();
3150
3761
  try {
@@ -3155,11 +3766,19 @@ var OrbinumClientProvider = class {
3155
3766
  this._connectingPromise = null;
3156
3767
  }
3157
3768
  // ─── Client access ──────────────────────────────────────────────────────
3769
+ /**
3770
+ * Returns the active `OrbinumClient`, or awaits the in-progress connection attempt.
3771
+ * Throws if the provider is `'idle'`, `'disconnected'`, or `'reconnecting'`.
3772
+ */
3158
3773
  async getOrbinumClient() {
3159
3774
  if (this._orbinumClient) return this._orbinumClient;
3160
3775
  if (this._connectingPromise) return this._connectingPromise;
3161
3776
  throw new Error(`OrbinumClientProvider: cannot get client in status '${this._status}'`);
3162
3777
  }
3778
+ /**
3779
+ * Same as `getOrbinumClient()` but returns `null` instead of throwing.
3780
+ * Useful in contexts where a missing client is an acceptable no-op.
3781
+ */
3163
3782
  async tryGetOrbinumClient() {
3164
3783
  try {
3165
3784
  return await this.getOrbinumClient();
@@ -3168,15 +3787,28 @@ var OrbinumClientProvider = class {
3168
3787
  }
3169
3788
  }
3170
3789
  // ─── Convenience RPC helpers ────────────────────────────────────────────
3790
+ /**
3791
+ * Sends a single Substrate JSON-RPC request and returns the typed result.
3792
+ * Waits for the client to be ready before dispatching.
3793
+ */
3171
3794
  async rpcSend(method, params = []) {
3172
3795
  const client = await this.getOrbinumClient();
3173
3796
  return client.substrate.request(method, params);
3174
3797
  }
3798
+ /**
3799
+ * Sends a single EVM JSON-RPC request and returns the typed result.
3800
+ * Throws if `evmRpc` was not configured.
3801
+ */
3175
3802
  async evmRpc(method, params = []) {
3176
3803
  const client = await this.getOrbinumClient();
3177
3804
  if (!client.evm) throw new Error("EVM RPC not configured");
3178
3805
  return client.evm.request(method, params);
3179
3806
  }
3807
+ /**
3808
+ * Sends multiple EVM JSON-RPC calls as a single batch request.
3809
+ * Returns a tuple of typed results in the same order as `calls`.
3810
+ * Throws if `evmRpc` was not configured.
3811
+ */
3180
3812
  async evmRpcBatch(calls) {
3181
3813
  const client = await this.getOrbinumClient();
3182
3814
  if (!client.evm) throw new Error("EVM RPC not configured");
@@ -3184,102 +3816,469 @@ var OrbinumClientProvider = class {
3184
3816
  }
3185
3817
  };
3186
3818
 
3187
- // src/shielded-pool/NoteDecryptor.ts
3819
+ // src/utils/stealth.ts
3820
+ var import_sha22 = require("@noble/hashes/sha2.js");
3821
+ var import_hkdf = require("@noble/hashes/hkdf.js");
3822
+ var import_baby_jubjub2 = require("@zk-kit/baby-jubjub");
3823
+ var STEALTH_INFO = new TextEncoder().encode("orbinum-stealth-v1");
3824
+ function deriveStealthScalar(sharedSecret, ownerPkBigint) {
3825
+ const salt = bigintTo32Le(ownerPkBigint);
3826
+ const stealthBytes = (0, import_hkdf.hkdf)(import_sha22.sha256, sharedSecret, salt, STEALTH_INFO, 32);
3827
+ return bytesToBigintLE(stealthBytes) % BABYJUB_SUBORDER || 1n;
3828
+ }
3829
+ function deriveStealthOwnerPk(sharedSecret, ownerPkBigint, ownerPkPoint) {
3830
+ const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
3831
+ const stealthPt = (0, import_baby_jubjub2.addPoint)((0, import_baby_jubjub2.mulPointEscalar)(import_baby_jubjub2.Base8, stealthScalar), ownerPkPoint);
3832
+ return stealthPt[0];
3833
+ }
3834
+ function deriveStealthSk(sharedSecret, ownerPkBigint, spendingKey) {
3835
+ const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
3836
+ return (stealthScalar + spendingKey) % BABYJUB_SUBORDER || 1n;
3837
+ }
3838
+
3839
+ // src/utils/bjj.ts
3840
+ var import_baby_jubjub3 = require("@zk-kit/baby-jubjub");
3841
+ var BJJ_A = 168700n;
3842
+ var BJJ_D = 168696n;
3843
+ function _modpow(base, exp, mod) {
3844
+ let result = 1n;
3845
+ base = base % mod;
3846
+ while (exp > 0n) {
3847
+ if (exp & 1n) result = result * base % mod;
3848
+ exp >>= 1n;
3849
+ base = base * base % mod;
3850
+ }
3851
+ return result;
3852
+ }
3853
+ function _sqrtModP(y2) {
3854
+ if (y2 === 0n) return 0n;
3855
+ if (_modpow(y2, (BN254_R - 1n) / 2n, BN254_R) !== 1n) return null;
3856
+ let s = 0n;
3857
+ let q = BN254_R - 1n;
3858
+ while ((q & 1n) === 0n) {
3859
+ q >>= 1n;
3860
+ s++;
3861
+ }
3862
+ if (s === 1n) return _modpow(y2, (BN254_R + 1n) / 4n, BN254_R);
3863
+ let z = 2n;
3864
+ while (_modpow(z, (BN254_R - 1n) / 2n, BN254_R) === 1n) z++;
3865
+ let m = s;
3866
+ let c = _modpow(z, q, BN254_R);
3867
+ let t = _modpow(y2, q, BN254_R);
3868
+ let r = _modpow(y2, (q + 1n) / 2n, BN254_R);
3869
+ for (; ; ) {
3870
+ if (t === 1n) return r;
3871
+ let i = 1n;
3872
+ let tmp = t * t % BN254_R;
3873
+ while (tmp !== 1n) {
3874
+ tmp = tmp * tmp % BN254_R;
3875
+ i++;
3876
+ }
3877
+ const b = _modpow(c, 1n << m - i - 1n, BN254_R);
3878
+ m = i;
3879
+ c = b * b % BN254_R;
3880
+ t = t * c % BN254_R;
3881
+ r = r * b % BN254_R;
3882
+ }
3883
+ }
3884
+ function recoverOwnerPkPoint(ax) {
3885
+ const x2 = ax * ax % BN254_R;
3886
+ const num = ((1n - BJJ_A * x2) % BN254_R + BN254_R) % BN254_R;
3887
+ const den = ((1n - BJJ_D * x2) % BN254_R + BN254_R) % BN254_R;
3888
+ if (den === 0n) return null;
3889
+ const denInv = _modpow(den, BN254_R - 2n, BN254_R);
3890
+ const y2 = num * denInv % BN254_R;
3891
+ const y = _sqrtModP(y2);
3892
+ if (y === null) return null;
3893
+ const yAlt = BN254_R - y;
3894
+ try {
3895
+ const check = (0, import_baby_jubjub3.mulPointEscalar)([ax, y], BABYJUB_SUBORDER);
3896
+ return check[0] === 0n && check[1] === 1n ? [ax, y] : [ax, yAlt];
3897
+ } catch {
3898
+ return [ax, yAlt];
3899
+ }
3900
+ }
3901
+
3902
+ // src/shielded-pool/protocol/NoteBuilder.ts
3903
+ var import_baby_jubjub4 = require("@zk-kit/baby-jubjub");
3904
+ var import_utils2 = require("@noble/ciphers/utils.js");
3905
+ var import_poseidon_lite = require("poseidon-lite");
3906
+ var NoteBuilder = class {
3907
+ /**
3908
+ * Build a ZkNote from the given inputs.
3909
+ *
3910
+ * @param input.value Amount in planck (required).
3911
+ * @param input.assetId Asset ID — default 0n (native ORB-Privacy).
3912
+ * @param input.ownerPk Sender's or recipient's global BabyJubJub Ax — default 0n.
3913
+ * @param input.blinding Random scalar — defaults to BigInt(Date.now()).
3914
+ * @param input.spendingKey Secret key for nullifier — default 0n.
3915
+ * @param input.viewingPublicKey Recipient's 32-byte LE packed BJJ ivk. Triggers memo encryption.
3916
+ * @param input.recipientOwnerPk Recipient's global ownerPk. Required with viewingPublicKey
3917
+ * to enable stealth address derivation. Without it, the
3918
+ * commitment uses ownerPk directly (no stealth).
3919
+ */
3920
+ static async build(input) {
3921
+ const value = input.value;
3922
+ const assetId = input.assetId ?? 0n;
3923
+ const ownerPk = input.ownerPk ?? 0n;
3924
+ const blinding = input.blinding ?? BigInt(Date.now());
3925
+ const spendingKey = input.spendingKey ?? 0n;
3926
+ const counterpartyPk = input.counterpartyPk ?? 0n;
3927
+ const useStealth = input.viewingPublicKey !== void 0 && input.recipientOwnerPk !== void 0;
3928
+ let memo;
3929
+ if (useStealth) {
3930
+ const recipientOwnerPk = input.recipientOwnerPk;
3931
+ const recipientIvkPacked = input.viewingPublicKey;
3932
+ const ephSk = (0, import_utils2.randomBytes)(32);
3933
+ const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
3934
+ const ivkPoint = (0, import_baby_jubjub4.unpackPoint)(ivkPackedBigint);
3935
+ if (!ivkPoint)
3936
+ throw new Error("NoteBuilder.build: invalid recipient viewing public key");
3937
+ const ephSkScalar = BigInt(toHex(ephSk)) % BABYJUB_SUBORDER || 1n;
3938
+ const sharedPoint = (0, import_baby_jubjub4.mulPointEscalar)(ivkPoint, ephSkScalar);
3939
+ const sharedSecret = bigintTo32Le(sharedPoint[0]);
3940
+ const recipientPkPoint = recoverOwnerPkPoint(recipientOwnerPk);
3941
+ if (!recipientPkPoint)
3942
+ throw new Error(
3943
+ "NoteBuilder.build: recipientOwnerPk is not a valid BJJ x-coordinate"
3944
+ );
3945
+ const effectiveOwnerPk = deriveStealthOwnerPk(
3946
+ sharedSecret,
3947
+ recipientOwnerPk,
3948
+ recipientPkPoint
3949
+ );
3950
+ const stealthCommitment = (0, import_poseidon_lite.poseidon4)([value, assetId, effectiveOwnerPk, blinding]);
3951
+ const stealthCommitmentBytes = bigintTo32Le(stealthCommitment);
3952
+ memo = Array.from(
3953
+ EncryptedMemo.encrypt(
3954
+ value,
3955
+ bigintTo32Le(effectiveOwnerPk),
3956
+ bigintTo32Le(blinding),
3957
+ Number(assetId),
3958
+ stealthCommitmentBytes,
3959
+ recipientIvkPacked,
3960
+ bigintTo32Le(counterpartyPk),
3961
+ ephSk
3962
+ )
3963
+ );
3964
+ const commitment2 = stealthCommitment;
3965
+ const nullifier2 = (0, import_poseidon_lite.poseidon2)([commitment2, spendingKey]);
3966
+ const commitmentBytes2 = stealthCommitmentBytes;
3967
+ const nullifierBytes2 = bigintTo32Le(nullifier2);
3968
+ if (memo.length !== ENCRYPTED_MEMO_SIZE)
3969
+ throw new Error(
3970
+ `NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
3971
+ );
3972
+ return {
3973
+ value,
3974
+ assetId,
3975
+ ownerPk: effectiveOwnerPk,
3976
+ blinding,
3977
+ spendingKey,
3978
+ spent: false,
3979
+ spentAt: null,
3980
+ commitment: commitment2,
3981
+ nullifier: nullifier2,
3982
+ commitmentHex: toHex(commitmentBytes2),
3983
+ nullifierHex: toHex(nullifierBytes2),
3984
+ memo,
3985
+ counterpartyPk
3986
+ };
3987
+ }
3988
+ const commitment = (0, import_poseidon_lite.poseidon4)([value, assetId, ownerPk, blinding]);
3989
+ const nullifier = (0, import_poseidon_lite.poseidon2)([commitment, spendingKey]);
3990
+ const commitmentBytes = bigintTo32Le(commitment);
3991
+ const nullifierBytes = bigintTo32Le(nullifier);
3992
+ memo = input.viewingPublicKey !== void 0 ? Array.from(
3993
+ EncryptedMemo.encrypt(
3994
+ value,
3995
+ bigintTo32Le(ownerPk),
3996
+ bigintTo32Le(blinding),
3997
+ Number(assetId),
3998
+ commitmentBytes,
3999
+ input.viewingPublicKey,
4000
+ bigintTo32Le(counterpartyPk)
4001
+ )
4002
+ ) : Array.from(EncryptedMemo.dummy());
4003
+ if (memo.length !== ENCRYPTED_MEMO_SIZE)
4004
+ throw new Error(
4005
+ `NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
4006
+ );
4007
+ return {
4008
+ value,
4009
+ assetId,
4010
+ ownerPk,
4011
+ blinding,
4012
+ spendingKey,
4013
+ spent: false,
4014
+ spentAt: null,
4015
+ commitment,
4016
+ nullifier,
4017
+ commitmentHex: toHex(commitmentBytes),
4018
+ nullifierHex: toHex(nullifierBytes),
4019
+ memo,
4020
+ counterpartyPk
4021
+ };
4022
+ }
4023
+ /**
4024
+ * Build the 168-byte ECDH-encrypted memo for a note.
4025
+ *
4026
+ * Pure TypeScript implementation — no WASM dependency.
4027
+ * Uses ChaCha20-Poly1305 with ECDH key agreement (BabyJubJub ephemeral keypair).
4028
+ *
4029
+ * @param note The ZkNote whose fields populate the plaintext.
4030
+ * @param recipientIvkPacked 32-byte LE packed BJJ viewing public key of the recipient.
4031
+ * Pass `new Uint8Array(32)` (default) for a public/dummy memo.
4032
+ * @param counterpartyPk 32-byte counterparty BabyJubJub Ax.
4033
+ * Pass `new Uint8Array(32)` (default) for no counterparty.
4034
+ */
4035
+ static buildMemo(note, recipientIvkPacked, counterpartyPk) {
4036
+ return EncryptedMemo.encrypt(
4037
+ note.value,
4038
+ bigintTo32Le(note.ownerPk),
4039
+ bigintTo32Le(note.blinding),
4040
+ Number(note.assetId),
4041
+ bigintTo32Le(note.commitment),
4042
+ recipientIvkPacked ?? new Uint8Array(32),
4043
+ counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n)
4044
+ );
4045
+ }
4046
+ };
4047
+
4048
+ // src/shielded-pool/protocol/NoteDecryptor.ts
3188
4049
  var import_poseidon_lite2 = require("poseidon-lite");
3189
- function tryDecryptNote(commitment, viewingKey, spendingKey) {
3190
- if (!commitment.encryptedMemo) return null;
4050
+ function computeNullifier(commitment, spendingKey) {
4051
+ return (0, import_poseidon_lite2.poseidon2)([commitment, spendingKey]);
4052
+ }
4053
+ function tryDecryptNote(commitment, viewingSecretKey, spendingKey, ownOwnerPk = 0n) {
4054
+ return tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwnerPk).note;
4055
+ }
4056
+ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwnerPk = 0n) {
4057
+ if (!commitment.encryptedMemo) return { note: null, reason: "no_memo" };
3191
4058
  let commitmentBytes;
3192
4059
  let memoBytes;
3193
4060
  try {
3194
4061
  commitmentBytes = fromHex(commitment.commitmentHex);
3195
4062
  memoBytes = fromHex(commitment.encryptedMemo);
3196
4063
  } catch {
3197
- return null;
4064
+ return { note: null, reason: "hex_parse_error" };
4065
+ }
4066
+ if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) {
4067
+ return {
4068
+ note: null,
4069
+ reason: `memo_size_mismatch:got_${memoBytes.length}_expected_${ENCRYPTED_MEMO_SIZE}`
4070
+ };
4071
+ }
4072
+ const plaintext = EncryptedMemo.decrypt(memoBytes, commitmentBytes, viewingSecretKey);
4073
+ if (!plaintext) return { note: null, reason: "decrypt_failed:wrong_key_or_corrupt_mac" };
4074
+ let effectiveOwnerPk = plaintext.ownerPk;
4075
+ let effectiveSpendingKey = spendingKey;
4076
+ if (ownOwnerPk !== 0n && plaintext.ownerPk !== ownOwnerPk) {
4077
+ const sharedSecret = EncryptedMemo.extractSharedSecret(memoBytes, viewingSecretKey);
4078
+ if (!sharedSecret) return { note: null, reason: "stealth_shared_secret_failed" };
4079
+ const ownPkPoint = recoverOwnerPkPoint(ownOwnerPk);
4080
+ if (!ownPkPoint) return { note: null, reason: "stealth_invalid_own_owner_pk" };
4081
+ const stealthOwnerPk = deriveStealthOwnerPk(sharedSecret, ownOwnerPk, ownPkPoint);
4082
+ if (stealthOwnerPk !== plaintext.ownerPk) {
4083
+ return { note: null, reason: "commitment_mismatch" };
4084
+ }
4085
+ effectiveOwnerPk = stealthOwnerPk;
4086
+ effectiveSpendingKey = deriveStealthSk(sharedSecret, ownOwnerPk, spendingKey);
3198
4087
  }
3199
- const plaintext = EncryptedMemo.decrypt(memoBytes, commitmentBytes, viewingKey);
3200
- if (!plaintext) return null;
3201
4088
  const recomputed = (0, import_poseidon_lite2.poseidon4)([
3202
4089
  plaintext.value,
3203
4090
  plaintext.assetId,
3204
- plaintext.ownerPk,
4091
+ effectiveOwnerPk,
3205
4092
  plaintext.blinding
3206
4093
  ]);
3207
- if (recomputed !== bytesToBigintLE(commitmentBytes)) return null;
3208
- const nullifier = (0, import_poseidon_lite2.poseidon2)([recomputed, spendingKey]);
4094
+ if (recomputed !== bytesToBigintLE(commitmentBytes)) {
4095
+ return { note: null, reason: "commitment_mismatch" };
4096
+ }
4097
+ const nullifier = (0, import_poseidon_lite2.poseidon2)([recomputed, effectiveSpendingKey]);
3209
4098
  return {
3210
- value: plaintext.value,
3211
- assetId: plaintext.assetId,
3212
- ownerPk: plaintext.ownerPk,
3213
- blinding: plaintext.blinding,
3214
- spendingKey,
3215
- spent: false,
3216
- spentAt: null,
3217
- commitment: recomputed,
3218
- nullifier,
3219
- commitmentHex: toHex(bigintTo32Le(recomputed)),
3220
- nullifierHex: toHex(bigintTo32Le(nullifier)),
3221
- memo: Array.from(memoBytes)
4099
+ note: {
4100
+ value: plaintext.value,
4101
+ assetId: plaintext.assetId,
4102
+ ownerPk: effectiveOwnerPk,
4103
+ blinding: plaintext.blinding,
4104
+ spendingKey: effectiveSpendingKey,
4105
+ spent: false,
4106
+ spentAt: null,
4107
+ commitment: recomputed,
4108
+ nullifier,
4109
+ commitmentHex: toHex(bigintTo32Le(recomputed)),
4110
+ nullifierHex: toHex(bigintTo32Le(nullifier)),
4111
+ memo: Array.from(memoBytes),
4112
+ counterpartyPk: plaintext.counterpartyPk
4113
+ }
3222
4114
  };
3223
4115
  }
3224
4116
 
3225
- // src/shielded-pool/PrivacyKeys.ts
3226
- var import_hkdf = require("@noble/hashes/hkdf.js");
3227
- var import_sha22 = require("@noble/hashes/sha2.js");
3228
- var import_baby_jubjub = require("@zk-kit/baby-jubjub");
4117
+ // src/shielded-pool/protocol/coinSelection.ts
4118
+ var TRANSFER_TREE_DEPTH = 20;
4119
+ function selectNotes(notes, needed) {
4120
+ const unspent = notes.filter((n) => !n.spent && n.value > 0n);
4121
+ const sorted = [...unspent].sort((a, b) => a.value < b.value ? -1 : 1);
4122
+ const single = sorted.find((n) => n.value >= needed);
4123
+ if (single) return [single, null];
4124
+ for (let i = 0; i < sorted.length; i++) {
4125
+ for (let j = i + 1; j < sorted.length; j++) {
4126
+ const a = sorted[i];
4127
+ const b = sorted[j];
4128
+ if (a !== void 0 && b !== void 0 && a.value + b.value >= needed) {
4129
+ return [a, b];
4130
+ }
4131
+ }
4132
+ }
4133
+ return null;
4134
+ }
4135
+ function buildDummyTransferInput(assetId) {
4136
+ const zeroSibling = "0x" + "00".repeat(32);
4137
+ return {
4138
+ nullifier: 0n,
4139
+ // Constraint 9: nullifier * is_dummy.out === 0 → must be 0
4140
+ value: 0n,
4141
+ // triggers is_dummy[i].out = 1 in the circuit
4142
+ assetId,
4143
+ // must match real note (Constraint 7)
4144
+ ownerPk: 0n,
4145
+ blinding: 0n,
4146
+ spendingKey: 1n,
4147
+ // arbitrary; EdDSA is disabled (enabled = 0) for dummy inputs
4148
+ pathSiblings: Array(TRANSFER_TREE_DEPTH).fill(zeroSibling),
4149
+ leafIndex: 0
4150
+ };
4151
+ }
3229
4152
 
3230
- // src/shielded-pool/constants.ts
3231
- var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
4153
+ // src/shielded-pool/protocol/disclosure.ts
4154
+ var import_proof_generator = require("@orbinum/proof-generator");
4155
+ var import_baby_jubjub5 = require("@zk-kit/baby-jubjub");
4156
+ var import_poseidon_lite3 = require("poseidon-lite");
4157
+ function hexFieldToBytes32(hex) {
4158
+ const clean = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
4159
+ return fromHex(clean.padStart(64, "0"));
4160
+ }
4161
+ function deriveBabyJubjubKeypair(substrateSigningKey) {
4162
+ let keyBigInt = 0n;
4163
+ for (let i = 0; i < substrateSigningKey.length; i++) {
4164
+ keyBigInt = keyBigInt << 8n | BigInt(substrateSigningKey[i]);
4165
+ }
4166
+ const sk = (0, import_poseidon_lite3.poseidon1)([keyBigInt]);
4167
+ const pk = (0, import_baby_jubjub5.mulPointEscalar)(import_baby_jubjub5.Base8, sk);
4168
+ return { sk, pkX: pk[0], pkY: pk[1] };
4169
+ }
4170
+ function buildDisclosurePublicSignals(commitment, auditorPkX, auditorPkY, proofOutput) {
4171
+ const buf = new Uint8Array(256);
4172
+ buf.set(hexFieldToBytes32(commitment), 0);
4173
+ buf.set(bigintTo32Le(auditorPkX), 32);
4174
+ buf.set(bigintTo32Le(auditorPkY), 64);
4175
+ buf.set(hexFieldToBytes32(proofOutput.encryptedData.epkX), 96);
4176
+ buf.set(hexFieldToBytes32(proofOutput.encryptedData.epkY), 128);
4177
+ buf.set(hexFieldToBytes32(proofOutput.encryptedData.encValue), 160);
4178
+ buf.set(hexFieldToBytes32(proofOutput.encryptedData.encAssetId), 192);
4179
+ buf.set(hexFieldToBytes32(proofOutput.encryptedData.encOwnerHash), 224);
4180
+ return Array.from(buf);
4181
+ }
4182
+ function decryptDisclosureSignals(auditorBjjSk, enc) {
4183
+ const shared = (0, import_baby_jubjub5.mulPointEscalar)([enc.epkX, enc.epkY], auditorBjjSk);
4184
+ const sharedX = shared[0];
4185
+ const sharedY = shared[1];
4186
+ const k0 = (0, import_poseidon_lite3.poseidon3)([sharedX, sharedY, 0n]);
4187
+ const k1 = (0, import_poseidon_lite3.poseidon3)([sharedX, sharedY, 1n]);
4188
+ const k2 = (0, import_poseidon_lite3.poseidon3)([sharedX, sharedY, 2n]);
4189
+ return {
4190
+ value: (enc.encValue - k0 + BN254_R) % BN254_R,
4191
+ assetId: (enc.encAssetId - k1 + BN254_R) % BN254_R,
4192
+ ownerHash: (enc.encOwnerHash - k2 + BN254_R) % BN254_R
4193
+ };
4194
+ }
4195
+
4196
+ // src/utils/blinding.ts
4197
+ function randomBlinding() {
4198
+ const buf = new Uint8Array(32);
4199
+ crypto.getRandomValues(buf);
4200
+ const n = bytesToBigintLE(buf);
4201
+ return n === 0n ? 1n : n % BN254_R;
4202
+ }
3232
4203
 
3233
- // src/shielded-pool/PrivacyKeys.ts
4204
+ // src/privacy-keys/PrivacyKeys.ts
4205
+ var import_hkdf2 = require("@noble/hashes/hkdf.js");
4206
+ var import_sha23 = require("@noble/hashes/sha2.js");
4207
+ var import_baby_jubjub6 = require("@zk-kit/baby-jubjub");
3234
4208
  var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
3235
4209
  function deriveSpendingKeyMessage(chainId, address) {
3236
4210
  return `orbinum-spending-key-v1
3237
4211
  ${chainId}
3238
4212
  ${address.toLowerCase()}`;
3239
4213
  }
3240
- async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
3241
- const hex = signatureHex.startsWith("0x") ? signatureHex.slice(2) : signatureHex;
3242
- const sigBytes = new Uint8Array((hex.match(/.{2}/g) ?? []).map((b) => parseInt(b, 16)));
4214
+ async function deriveMasterKeyBytes(signatureHex, chainId, address) {
4215
+ const sigBytes = fromHex(signatureHex);
3243
4216
  const info = new TextEncoder().encode(`orbinum-sk-v1:${chainId}:${address.toLowerCase()}`);
3244
- const skBytes = (0, import_hkdf.hkdf)(import_sha22.sha256, sigBytes, new Uint8Array(0), info, 32);
3245
- const skBigint = BigInt(
3246
- "0x" + Array.from(skBytes).map((b) => b.toString(16).padStart(2, "0")).join("")
3247
- ) % BN254_R;
4217
+ return (0, import_hkdf2.hkdf)(import_sha23.sha256, sigBytes, new Uint8Array(0), info, 32);
4218
+ }
4219
+ async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
4220
+ const masterBytes = await deriveMasterKeyBytes(signatureHex, chainId, address);
4221
+ const skBigint = BigInt(toHex(masterBytes)) % BABYJUB_SUBORDER;
3248
4222
  return skBigint === 0n ? 1n : skBigint;
3249
4223
  }
3250
- function deriveViewingKey(spendingKey) {
4224
+ function deriveViewingSecretKey(spendingKey) {
3251
4225
  const ikm = bigintTo32Le(spendingKey);
3252
- return (0, import_hkdf.hkdf)(import_sha22.sha256, ikm, void 0, IVK_DOMAIN, 32);
4226
+ return (0, import_hkdf2.hkdf)(import_sha23.sha256, ikm, void 0, IVK_DOMAIN, 32);
4227
+ }
4228
+ function deriveViewingPublicKey(ivsk) {
4229
+ const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
4230
+ const ivkPoint = (0, import_baby_jubjub6.mulPointEscalar)(import_baby_jubjub6.Base8, ivskScalar);
4231
+ const packed = (0, import_baby_jubjub6.packPoint)(ivkPoint);
4232
+ return bigintTo32Le(packed);
3253
4233
  }
3254
4234
  function deriveOwnerPk(spendingKey) {
3255
4235
  try {
3256
- const pubPoint = (0, import_baby_jubjub.mulPointEscalar)(import_baby_jubjub.Base8, spendingKey);
4236
+ const pubPoint = (0, import_baby_jubjub6.mulPointEscalar)(import_baby_jubjub6.Base8, spendingKey);
3257
4237
  return pubPoint[0];
3258
4238
  } catch {
3259
4239
  return 0n;
3260
4240
  }
3261
4241
  }
3262
4242
 
3263
- // src/shielded-pool/PrivacyKeyManager.ts
4243
+ // src/privacy-keys/PrivacyKeyManager.ts
3264
4244
  var PrivacyKeyManager = class {
3265
4245
  _state = {
3266
4246
  spendingKey: null,
3267
- viewingKey: null,
4247
+ masterBytes: null,
4248
+ viewingSecretKey: null,
4249
+ viewingPublicKeyPacked: null,
3268
4250
  ownerPk: null
3269
4251
  };
3270
4252
  /**
3271
- * Load a spending key into the in-memory session.
3272
- * Derives viewingKey and ownerPk immediately.
4253
+ * Load a spending key and its corresponding master bytes into the in-memory session.
4254
+ * Derives viewingSecretKey, viewingPublicKeyPacked, and ownerPk immediately.
3273
4255
  * Replaces any previously loaded key.
4256
+ *
4257
+ * @param spendingKey Circuit scalar: BigInt(masterBytes) % BABYJUB_SUBORDER, clamped to [1, ∞).
4258
+ * @param masterBytes Raw 32-byte HKDF output before modular reduction. Used to derive
4259
+ * the stable vault key (HKDF(masterBytes, info="orbinum-vault-key-v1")).
3274
4260
  */
3275
- async load(spendingKey) {
3276
- const viewingKey = deriveViewingKey(spendingKey);
4261
+ async load(spendingKey, masterBytes) {
4262
+ const viewingSecretKey = deriveViewingSecretKey(spendingKey);
4263
+ const viewingPublicKeyPacked = deriveViewingPublicKey(viewingSecretKey);
3277
4264
  const ownerPk = deriveOwnerPk(spendingKey);
3278
- this._state = { spendingKey, viewingKey, ownerPk };
4265
+ this._state = {
4266
+ spendingKey,
4267
+ masterBytes,
4268
+ viewingSecretKey,
4269
+ viewingPublicKeyPacked,
4270
+ ownerPk
4271
+ };
3279
4272
  }
3280
4273
  /** Clear all key material from memory. Call on vault lock / sign-out. */
3281
4274
  clear() {
3282
- this._state = { spendingKey: null, viewingKey: null, ownerPk: null };
4275
+ this._state = {
4276
+ spendingKey: null,
4277
+ masterBytes: null,
4278
+ viewingSecretKey: null,
4279
+ viewingPublicKeyPacked: null,
4280
+ ownerPk: null
4281
+ };
3283
4282
  }
3284
4283
  /** Returns true if a spending key has been loaded. */
3285
4284
  isLoaded() {
@@ -3292,12 +4291,29 @@ var PrivacyKeyManager = class {
3292
4291
  }
3293
4292
  return this._state.spendingKey;
3294
4293
  }
3295
- /** Returns the 32-byte viewing key. Throws if not loaded. */
3296
- getViewingKey() {
3297
- if (this._state.viewingKey === null) {
4294
+ /**
4295
+ * Returns the 32-byte viewing secret key (ivsk).
4296
+ * Used internally for decrypting received notes during rescan.
4297
+ * SECURITY: never expose this in addresses or network requests.
4298
+ * Throws if not loaded.
4299
+ */
4300
+ getViewingSecretKey() {
4301
+ if (this._state.viewingSecretKey === null) {
4302
+ throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
4303
+ }
4304
+ return this._state.viewingSecretKey;
4305
+ }
4306
+ /**
4307
+ * Returns the 32-byte LE-encoded packed BJJ viewing public key (ivk).
4308
+ * This is the component embedded in the privacy address and passed to senders
4309
+ * so they can encrypt memos only the recipient can decrypt.
4310
+ * Throws if not loaded.
4311
+ */
4312
+ getViewingPublicKeyPacked() {
4313
+ if (this._state.viewingPublicKeyPacked === null) {
3298
4314
  throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
3299
4315
  }
3300
- return this._state.viewingKey;
4316
+ return this._state.viewingPublicKeyPacked;
3301
4317
  }
3302
4318
  /** Returns the BabyJubJub owner public key (x-coordinate). Throws if not loaded. */
3303
4319
  getOwnerPk() {
@@ -3310,24 +4326,86 @@ var PrivacyKeyManager = class {
3310
4326
  getSpendingKeyBytes() {
3311
4327
  return bigintTo32Le(this.getSpendingKey());
3312
4328
  }
3313
- /** Exports the spending key as a 0x-prefixed 64-char hex string. Throws if not loaded. */
4329
+ /**
4330
+ * Returns the 32-byte master key bytes (pre-modulus HKDF output).
4331
+ * Used to derive the stable vault AES key. Throws if not loaded.
4332
+ */
4333
+ getMasterBytes() {
4334
+ if (this._state.masterBytes === null) {
4335
+ throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
4336
+ }
4337
+ return this._state.masterBytes;
4338
+ }
4339
+ /**
4340
+ * Exports the master key bytes as a "mk:0x{hex}" string.
4341
+ * Storing masterBytes (not the sk scalar) ensures the vault key and
4342
+ * rescan can always be reconstructed regardless of any future modulus change.
4343
+ * Throws if not loaded.
4344
+ */
3314
4345
  exportHex() {
3315
- return "0x" + this.getSpendingKey().toString(16).padStart(64, "0");
4346
+ const mb = this.getMasterBytes();
4347
+ return "mk:0x" + Array.from(mb, (b) => b.toString(16).padStart(2, "0")).join("");
4348
+ }
4349
+ /**
4350
+ * Exports a shareable privacy address encoding the owner public key and
4351
+ * viewing PUBLIC key of the currently loaded identity.
4352
+ *
4353
+ * Format: `orbpriv1:{ownerPk_hex}:{viewingPublicKey_hex}`
4354
+ *
4355
+ * The recipient uses this address so the sender can:
4356
+ * 1. Embed `ownerPk` in the note commitment (Poseidon4 input).
4357
+ * 2. Encrypt the memo via ECDH with the recipient's `viewingPublicKey`.
4358
+ *
4359
+ * SECURITY: Only the viewing PUBLIC key is embedded — the viewing secret key
4360
+ * (used for decryption) is never exported. Holders of this address cannot
4361
+ * decrypt the recipient's notes.
4362
+ *
4363
+ * Throws if no key is loaded.
4364
+ */
4365
+ encodePrivacyAddress() {
4366
+ const ownerPk = this.getOwnerPk();
4367
+ const ivkPacked = this.getViewingPublicKeyPacked();
4368
+ const ownerPkHex = "0x" + ownerPk.toString(16).padStart(64, "0");
4369
+ const ivkHex = "0x" + Array.from(ivkPacked, (b) => b.toString(16).padStart(2, "0")).join("");
4370
+ return `orbpriv1:${ownerPkHex}:${ivkHex}`;
4371
+ }
4372
+ /**
4373
+ * Decode a privacy address of the form `orbpriv1:{ownerPk_hex}:{viewingPublicKey_hex}`.
4374
+ * Returns `{ ownerPkHex, viewingPublicKeyHex }` on success, or `null` if the input
4375
+ * does not match the expected format.
4376
+ */
4377
+ static decodePrivacyAddress(address) {
4378
+ if (!address.startsWith("orbpriv1:")) return null;
4379
+ const parts = address.split(":");
4380
+ if (parts.length !== 3) return null;
4381
+ const ownerPkHex = parts[1];
4382
+ const viewingPublicKeyHex = parts[2];
4383
+ if (!ownerPkHex || !viewingPublicKeyHex) return null;
4384
+ return { ownerPkHex, viewingPublicKeyHex };
3316
4385
  }
3317
4386
  /**
3318
- * Load a spending key from a 0x-prefixed or bare hex string.
3319
- * Validates the key is in the valid range [1, BN254_R).
4387
+ * Load keys from a cached "mk:0x{masterBytes_hex}" string produced by exportHex().
4388
+ * Throws if the format is invalid or masterBytes length is not 32 bytes.
3320
4389
  */
3321
4390
  async importFromHex(hex) {
3322
- const key = BigInt(hex.startsWith("0x") ? hex : "0x" + hex);
3323
- if (key === 0n || key >= BN254_R) {
3324
- throw new Error("PrivacyKeyManager: invalid spending key \u2014 out of BN254 range.");
4391
+ if (!hex.startsWith("mk:")) {
4392
+ throw new Error(
4393
+ 'PrivacyKeyManager: invalid cache format. Expected "mk:0x{masterBytes_hex}".'
4394
+ );
3325
4395
  }
3326
- await this.load(key);
4396
+ const raw = hex.slice(3);
4397
+ const h = raw.startsWith("0x") ? raw.slice(2) : raw;
4398
+ const masterBytes = new Uint8Array((h.match(/.{2}/g) ?? []).map((b) => parseInt(b, 16)));
4399
+ if (masterBytes.length !== 32) {
4400
+ throw new Error("PrivacyKeyManager: invalid master bytes \u2014 expected 32 bytes.");
4401
+ }
4402
+ const masterBigint = BigInt("0x" + h);
4403
+ const sk = masterBigint % BABYJUB_SUBORDER || 1n;
4404
+ await this.load(sk, masterBytes);
3327
4405
  }
3328
4406
  };
3329
4407
 
3330
- // src/shielded-pool/VaultCrypto.ts
4408
+ // src/vault/VaultJson.ts
3331
4409
  function vaultReplacer(_key, value) {
3332
4410
  if (typeof value === "bigint") return { __bigint: value.toString() };
3333
4411
  return value;
@@ -3338,16 +4416,28 @@ function vaultReviver(_key, value) {
3338
4416
  }
3339
4417
  return value;
3340
4418
  }
4419
+
4420
+ // src/utils/encoding.ts
4421
+ function toBase64(buf) {
4422
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
4423
+ let str = "";
4424
+ for (const b of bytes) str += String.fromCharCode(b);
4425
+ return btoa(str);
4426
+ }
4427
+ function fromBase64(b64) {
4428
+ const bin = atob(b64);
4429
+ const out = new Uint8Array(bin.length);
4430
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
4431
+ return out;
4432
+ }
4433
+
4434
+ // src/vault/VaultCrypto.ts
3341
4435
  var VAULT_KEY_INFO = new TextEncoder().encode("orbinum-vault-key-v1");
3342
4436
  var IV_BYTES = 12;
3343
- async function deriveVaultKey(spendingKeyBytes) {
3344
- const keyMaterial = await crypto.subtle.importKey(
3345
- "raw",
3346
- spendingKeyBytes.slice(0),
3347
- "HKDF",
3348
- false,
3349
- ["deriveKey"]
3350
- );
4437
+ async function deriveVaultKey(masterBytes) {
4438
+ const keyMaterial = await crypto.subtle.importKey("raw", masterBytes.slice(0), "HKDF", false, [
4439
+ "deriveKey"
4440
+ ]);
3351
4441
  return crypto.subtle.deriveKey(
3352
4442
  {
3353
4443
  name: "HKDF",
@@ -3376,6 +4466,167 @@ async function decryptJson(key, iv, ciphertext) {
3376
4466
  return JSON.parse(new TextDecoder().decode(plainBuf), vaultReviver);
3377
4467
  }
3378
4468
 
4469
+ // src/vault/errors.ts
4470
+ var VaultLockedError = class extends Error {
4471
+ constructor(message = "Vault is locked. Connect your wallet to unlock it.") {
4472
+ super(message);
4473
+ this.name = "VaultLockedError";
4474
+ }
4475
+ };
4476
+
4477
+ // src/vault/noteOps.ts
4478
+ function applyNoteStatus(note, status) {
4479
+ return {
4480
+ ...note,
4481
+ spent: status?.spent ?? note.spent ?? false,
4482
+ spentAt: status?.spentAt ?? note.spentAt ?? null
4483
+ };
4484
+ }
4485
+ async function encryptNote(key, note) {
4486
+ const { iv, ciphertext } = await encryptJson(key, note);
4487
+ return {
4488
+ commitmentHex: note.commitmentHex,
4489
+ iv,
4490
+ ciphertext,
4491
+ nullifierHex: note.nullifierHex,
4492
+ assetId: note.assetId.toString(),
4493
+ spent: note.spent,
4494
+ spentAt: note.spentAt,
4495
+ updatedAt: Date.now()
4496
+ };
4497
+ }
4498
+ async function decryptNoteRecord(key, rec) {
4499
+ const note = await decryptJson(key, rec.iv, rec.ciphertext);
4500
+ return applyNoteStatus(note, {
4501
+ ...rec.spent !== void 0 && { spent: rec.spent },
4502
+ spentAt: rec.spentAt ?? null
4503
+ });
4504
+ }
4505
+
4506
+ // src/proof-generator/unshield.ts
4507
+ var import_proof_generator2 = require("@orbinum/proof-generator");
4508
+ var import_utils3 = require("@noble/ciphers/utils.js");
4509
+ var import_baby_jubjub7 = require("@zk-kit/baby-jubjub");
4510
+ var import_poseidon_lite4 = require("poseidon-lite");
4511
+
4512
+ // src/proof-generator/merkle.ts
4513
+ function merkleProofToCircuit(siblings, leafIndex) {
4514
+ const elements = siblings.map((h) => leHexToBigint(h).toString());
4515
+ const depth = siblings.length;
4516
+ const indices = computePathIndices(leafIndex, depth).map(String);
4517
+ return { elements, indices };
4518
+ }
4519
+
4520
+ // src/proof-generator/unshield.ts
4521
+ async function generateUnshieldProof(inputs, options = {}) {
4522
+ const { elements, indices } = merkleProofToCircuit(inputs.pathSiblings, inputs.leafIndex);
4523
+ const fee = inputs.fee ?? 0n;
4524
+ const changeValue = inputs.changeValue ?? 0n;
4525
+ const noteValue = inputs.amount + fee + changeValue;
4526
+ if (inputs.amount <= 0n) {
4527
+ throw new Error("Unshield amount must be greater than zero.");
4528
+ }
4529
+ if (changeValue < 0n) {
4530
+ throw new Error("changeValue must be >= 0.");
4531
+ }
4532
+ const changeOwnerPubkey = inputs.changeOwnerPubkey ?? (0, import_baby_jubjub7.mulPointEscalar)(import_baby_jubjub7.Base8, inputs.spendingKey)[0];
4533
+ const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE((0, import_utils3.randomBytes)(32)) : 0n);
4534
+ const changeCommitment = changeValue > 0n ? (0, import_poseidon_lite4.poseidon4)([changeValue, inputs.assetId, changeOwnerPubkey, changeBlinding]) : 0n;
4535
+ const circuitInputs = {
4536
+ merkle_root: leHexToBigint(inputs.merkleRoot).toString(),
4537
+ nullifier: inputs.nullifier.toString(),
4538
+ amount: inputs.amount.toString(),
4539
+ recipient: inputs.recipient.toString(),
4540
+ asset_id: inputs.assetId.toString(),
4541
+ fee: fee.toString(),
4542
+ change_commitment: changeCommitment.toString(),
4543
+ note_value: noteValue.toString(),
4544
+ note_asset_id: inputs.assetId.toString(),
4545
+ note_blinding: inputs.blinding.toString(),
4546
+ spending_key: inputs.spendingKey.toString(),
4547
+ path_elements: elements,
4548
+ path_indices: indices,
4549
+ change_value: changeValue.toString(),
4550
+ change_blinding: changeBlinding.toString(),
4551
+ change_owner_pubkey: changeOwnerPubkey.toString()
4552
+ };
4553
+ const provider = options.provider ?? new import_proof_generator2.WebArtifactProvider();
4554
+ const opts = { provider };
4555
+ if (options.verbose !== void 0) opts.verbose = options.verbose;
4556
+ const proofResult = await (0, import_proof_generator2.generateProof)(import_proof_generator2.CircuitType.Unshield, circuitInputs, opts);
4557
+ return { ...proofResult, changeCommitment, changeValue, changeBlinding, changeOwnerPubkey };
4558
+ }
4559
+
4560
+ // src/proof-generator/transfer.ts
4561
+ var import_proof_generator3 = require("@orbinum/proof-generator");
4562
+ async function generateTransferProof(params, options = {}) {
4563
+ const root = leHexToBigint(params.merkleRoot).toString();
4564
+ const [i0, i1] = params.inputs;
4565
+ const [o0, o1] = params.outputs;
4566
+ const fee = params.fee ?? 0n;
4567
+ const path0 = merkleProofToCircuit(i0.pathSiblings, i0.leafIndex);
4568
+ const path1 = merkleProofToCircuit(i1.pathSiblings, i1.leafIndex);
4569
+ const circuitInputs = {
4570
+ merkle_root: root,
4571
+ nullifiers: [i0.nullifier.toString(), i1.nullifier.toString()],
4572
+ commitments: [o0.commitment.toString(), o1.commitment.toString()],
4573
+ asset_id: i0.assetId.toString(),
4574
+ fee: fee.toString(),
4575
+ input_values: [i0.value.toString(), i1.value.toString()],
4576
+ input_asset_ids: [i0.assetId.toString(), i1.assetId.toString()],
4577
+ input_blindings: [i0.blinding.toString(), i1.blinding.toString()],
4578
+ spending_keys: [i0.spendingKey.toString(), i1.spendingKey.toString()],
4579
+ input_path_elements: [path0.elements, path1.elements],
4580
+ input_path_indices: [path0.indices, path1.indices],
4581
+ output_values: [o0.value.toString(), o1.value.toString()],
4582
+ output_asset_ids: [o0.assetId.toString(), o1.assetId.toString()],
4583
+ output_owner_pubkeys: [o0.ownerPk.toString(), o1.ownerPk.toString()],
4584
+ output_blindings: [o0.blinding.toString(), o1.blinding.toString()]
4585
+ };
4586
+ const provider = options.provider ?? new import_proof_generator3.WebArtifactProvider();
4587
+ const opts = { provider };
4588
+ if (options.verbose !== void 0) opts.verbose = options.verbose;
4589
+ return (0, import_proof_generator3.generateProof)(import_proof_generator3.CircuitType.Transfer, circuitInputs, opts);
4590
+ }
4591
+
4592
+ // src/proof-generator/fee-claim.ts
4593
+ var import_proof_generator4 = require("@orbinum/proof-generator");
4594
+ function hexSignalToBytes(hex) {
4595
+ const clean = hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
4596
+ const padded = clean.padStart(64, "0");
4597
+ const bytes = new Uint8Array(32);
4598
+ for (let i = 0; i < 32; i++) {
4599
+ bytes[i] = parseInt(padded.slice(i * 2, i * 2 + 2), 16);
4600
+ }
4601
+ return bytes;
4602
+ }
4603
+ async function generateFeeClaimProof(inputs, options = {}) {
4604
+ const result = await (0, import_proof_generator4.generateDisclosureProof)(
4605
+ inputs.amount,
4606
+ inputs.ownerPubkey,
4607
+ inputs.blinding,
4608
+ inputs.assetId,
4609
+ inputs.commitment,
4610
+ // Baby Jubjub base point G — placeholder, no real auditor for fee claiming
4611
+ 5299619240641551281634865583518297030282874472190772894086521144482721001553n,
4612
+ 16950150798460657717958625567821834550301663161624707787222815936182638968203n,
4613
+ 1n,
4614
+ // r — placeholder ephemeral scalar (NOT cryptographically secure)
4615
+ { discloseValue: true, discloseAssetId: true, discloseOwner: false },
4616
+ options
4617
+ );
4618
+ const [, , sigEncValue, sigEncAssetId, sigEncOwnerHash, sigCommitment] = result.publicSignals.map(hexSignalToBytes);
4619
+ const compact = new Uint8Array(76);
4620
+ compact.set(sigCommitment);
4621
+ compact.set(sigEncValue.subarray(0, 8), 32);
4622
+ compact.set(sigEncAssetId.subarray(0, 4), 40);
4623
+ compact.set(sigEncOwnerHash, 44);
4624
+ return {
4625
+ proof: result.proof,
4626
+ publicSignals: Array.from(compact)
4627
+ };
4628
+ }
4629
+
3379
4630
  // src/account-mapping/types/index.ts
3380
4631
  var SignatureScheme = {
3381
4632
  Eip191: "Eip191",
@@ -3392,7 +4643,7 @@ function decodePrecompileCalldata(address, input) {
3392
4643
  if (!fnSig) return null;
3393
4644
  if (fnSig.startsWith("registerAlias")) {
3394
4645
  try {
3395
- const data = hexToBytes(input.slice(10));
4646
+ const data = fromHex(input.slice(10));
3396
4647
  const alias = decodeString(data, 0);
3397
4648
  return { fnSig, args: { alias } };
3398
4649
  } catch {
@@ -3401,37 +4652,43 @@ function decodePrecompileCalldata(address, input) {
3401
4652
  }
3402
4653
  if (fnSig.startsWith("shield(")) {
3403
4654
  try {
3404
- const data = hexToBytes(input.slice(10));
4655
+ const data = fromHex(input.slice(10));
3405
4656
  const assetId = decodeUint(data, 0);
3406
- const amount = decodeUint(data, 32);
3407
- const commitment = toHex(data.slice(64, 96));
3408
- return { fnSig, args: { assetId, amount, commitment } };
4657
+ const commitment = toHex(data.slice(32, 64));
4658
+ return { fnSig, args: { assetId, commitment } };
3409
4659
  } catch {
3410
4660
  return { fnSig, args: {} };
3411
4661
  }
3412
4662
  }
3413
4663
  if (fnSig.startsWith("unshield(")) {
3414
4664
  try {
3415
- const data = hexToBytes(input.slice(10));
4665
+ const data = fromHex(input.slice(10));
3416
4666
  const root = toHex(data.slice(32, 64));
3417
4667
  const nullifier = toHex(data.slice(64, 96));
3418
4668
  const assetId = decodeUint(data, 96);
3419
4669
  const amount = decodeUint(data, 128);
3420
4670
  const recipient = toHex(data.slice(160, 192));
3421
- return { fnSig, args: { root, nullifier, assetId, amount, recipient } };
4671
+ const fee = decodeUint(data, 192);
4672
+ const changeCommitment = toHex(data.slice(224, 256));
4673
+ return {
4674
+ fnSig,
4675
+ args: { root, nullifier, assetId, amount, recipient, fee, changeCommitment }
4676
+ };
3422
4677
  } catch {
3423
4678
  return { fnSig, args: {} };
3424
4679
  }
3425
4680
  }
3426
4681
  if (fnSig.startsWith("privateTransfer(")) {
3427
4682
  try {
3428
- const data = hexToBytes(input.slice(10));
4683
+ const data = fromHex(input.slice(10));
3429
4684
  const root = toHex(data.slice(32, 64));
3430
4685
  const nullOffset = Number(decodeUint(data, 64));
3431
4686
  const commOffset = Number(decodeUint(data, 96));
3432
4687
  const nullifiers = Number(decodeUint(data, nullOffset));
3433
4688
  const commitments = Number(decodeUint(data, commOffset));
3434
- return { fnSig, args: { root, nullifiers, commitments } };
4689
+ const assetId = decodeUint(data, 160);
4690
+ const fee = decodeUint(data, 192);
4691
+ return { fnSig, args: { root, nullifiers, commitments, assetId, fee } };
3435
4692
  } catch {
3436
4693
  return { fnSig, args: {} };
3437
4694
  }
@@ -3644,7 +4901,11 @@ function mapExtrinsicArgs(section, method, args) {
3644
4901
  if (m_norm === "requestdisclosure") {
3645
4902
  return {
3646
4903
  target: get(0, "target"),
3647
- reason: get(1, "reason")
4904
+ commitment: get(1, "commitment"),
4905
+ required_fields: get(2, "required_fields"),
4906
+ reason: get(3, "reason"),
4907
+ auditor_bjj_pk_x: get(4, "auditor_bjj_pk_x"),
4908
+ auditor_bjj_pk_y: get(5, "auditor_bjj_pk_y")
3648
4909
  };
3649
4910
  }
3650
4911
  if (m_norm === "disclose") {
@@ -3658,7 +4919,8 @@ function mapExtrinsicArgs(section, method, args) {
3658
4919
  if (m_norm === "rejectdisclosure") {
3659
4920
  return {
3660
4921
  auditor: get(0, "auditor"),
3661
- reason: get(1, "reason")
4922
+ commitment: get(1, "commitment"),
4923
+ reason: get(2, "reason")
3662
4924
  };
3663
4925
  }
3664
4926
  if (m_norm === "registerasset") {
@@ -3681,7 +4943,8 @@ function mapExtrinsicArgs(section, method, args) {
3681
4943
  if (m_norm === "pruneexpiredrequest") {
3682
4944
  return {
3683
4945
  target: get(0, "target"),
3684
- auditor: get(1, "auditor")
4946
+ auditor: get(1, "auditor"),
4947
+ commitment: get(2, "commitment")
3685
4948
  };
3686
4949
  }
3687
4950
  if (m_norm === "revokedisclosurerecord") {
@@ -3918,29 +5181,35 @@ function mapZkEventData(method, data) {
3918
5181
  }
3919
5182
  if (m_norm === "disclosed") {
3920
5183
  return {
3921
- who: get(0, "who"),
3922
- commitment: get(1, "commitment"),
3923
- auditor: get(2, "auditor")
5184
+ target: get(0, "target"),
5185
+ auditor: get(1, "auditor"),
5186
+ commitment: get(2, "commitment"),
5187
+ signals: get(3, "signals")
3924
5188
  };
3925
5189
  }
3926
5190
  if (m_norm === "disclosurerequested") {
3927
5191
  return {
3928
5192
  target: get(0, "target"),
3929
5193
  auditor: get(1, "auditor"),
3930
- reason: get(2, "reason")
5194
+ commitment: get(2, "commitment"),
5195
+ required_fields: get(3, "required_fields"),
5196
+ auditor_bjj_pk_x: get(4, "auditor_bjj_pk_x"),
5197
+ auditor_bjj_pk_y: get(5, "auditor_bjj_pk_y")
3931
5198
  };
3932
5199
  }
3933
5200
  if (m_norm === "disclosurerejected") {
3934
5201
  return {
3935
5202
  target: get(0, "target"),
3936
5203
  auditor: get(1, "auditor"),
3937
- reason: get(2, "reason")
5204
+ commitment: get(2, "commitment"),
5205
+ reason: get(3, "reason")
3938
5206
  };
3939
5207
  }
3940
5208
  if (m_norm === "disclosurerequestexpired") {
3941
5209
  return {
3942
5210
  target: get(0, "target"),
3943
- auditor: get(1, "auditor")
5211
+ auditor: get(1, "auditor"),
5212
+ commitment: get(2, "commitment")
3944
5213
  };
3945
5214
  }
3946
5215
  if (m_norm === "disclosurerecordrevoked") {
@@ -4192,9 +5461,13 @@ var import_polkadot_api4 = require("polkadot-api");
4192
5461
  AccountId,
4193
5462
  AccountMappingModule,
4194
5463
  AccountMappingPrecompile,
5464
+ BABYJUB_SUBORDER,
5465
+ BN254_R,
4195
5466
  Blake2256,
4196
5467
  CircuitId,
5468
+ CircuitType,
4197
5469
  CryptoPrecompiles,
5470
+ ENCRYPTED_MEMO_SIZE,
4198
5471
  EncryptedMemo,
4199
5472
  EvmClient,
4200
5473
  EvmExplorer,
@@ -4207,30 +5480,45 @@ var import_polkadot_api4 = require("polkadot-api");
4207
5480
  PRECOMPILE_ADDR,
4208
5481
  PrivacyKeyManager,
4209
5482
  PrivacyModule,
5483
+ RelayerStatusModule,
4210
5484
  SLIP0044_NAMESPACE,
4211
5485
  ShieldedPoolModule,
4212
5486
  ShieldedPoolPrecompile,
4213
5487
  SignatureScheme,
4214
5488
  Storage,
4215
5489
  SubstrateClient,
5490
+ VaultLockedError,
5491
+ WebArtifactProvider,
4216
5492
  ZkVerifierModule,
4217
5493
  accountIdHexToSs58,
4218
5494
  addressToAccountIdHex,
5495
+ applyNoteStatus,
4219
5496
  base58,
4220
5497
  bigintTo32Be,
4221
5498
  bigintTo32Le,
4222
5499
  bigintTo32LeArr,
5500
+ buildDisclosurePublicSignals,
5501
+ buildDummyTransferInput,
4223
5502
  bytesToBigintLE,
5503
+ computeNullifier,
4224
5504
  computePathIndices,
4225
5505
  connectInjectedExtension,
4226
5506
  decodePrecompileCalldata,
5507
+ decryptDisclosureSignals,
4227
5508
  decryptJson,
5509
+ decryptNoteRecord,
5510
+ deriveBabyJubjubKeypair,
5511
+ deriveMasterKeyBytes,
4228
5512
  deriveOwnerPk,
4229
5513
  deriveSpendingKeyFromSignature,
4230
5514
  deriveSpendingKeyMessage,
5515
+ deriveStealthOwnerPk,
5516
+ deriveStealthSk,
4231
5517
  deriveVaultKey,
4232
- deriveViewingKey,
5518
+ deriveViewingPublicKey,
5519
+ deriveViewingSecretKey,
4233
5520
  encryptJson,
5521
+ encryptNote,
4234
5522
  ensureHexPrefix,
4235
5523
  evmAddressToAccountId,
4236
5524
  evmToImplicitSubstrate,
@@ -4240,6 +5528,10 @@ var import_polkadot_api4 = require("polkadot-api");
4240
5528
  formatORB,
4241
5529
  fromBase64,
4242
5530
  fromHex,
5531
+ generateDisclosureProof,
5532
+ generateFeeClaimProof,
5533
+ generateTransferProof,
5534
+ generateUnshieldProof,
4243
5535
  getInjectedExtensions,
4244
5536
  getPolkadotSigner,
4245
5537
  getPolkadotSignerFromPjs,
@@ -4257,6 +5549,9 @@ var import_polkadot_api4 = require("polkadot-api");
4257
5549
  mapExtrinsicArgs,
4258
5550
  mapZkEventData,
4259
5551
  normalizeEvmAddress,
5552
+ randomBlinding,
5553
+ recoverOwnerPkPoint,
5554
+ selectNotes,
4260
5555
  shortHash,
4261
5556
  substrateSs58ToAccountIdHex,
4262
5557
  substrateToEvm,
@@ -4265,6 +5560,7 @@ var import_polkadot_api4 = require("polkadot-api");
4265
5560
  toTxResult,
4266
5561
  truncateMiddle,
4267
5562
  tryDecryptNote,
5563
+ tryDecryptNoteVerbose,
4268
5564
  u128,
4269
5565
  u64,
4270
5566
  vaultReplacer,