@orbinum/sdk 0.4.1 → 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,19 +429,25 @@ 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 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") {
442
+ const obj = v;
443
+ if (typeof obj["asHex"] === "function") {
444
+ try {
445
+ return obj["asHex"]();
446
+ } catch {
447
+ }
448
+ }
407
449
  return Object.fromEntries(
408
- Object.entries(v).filter(([, val]) => typeof val !== "function").map(([k, val]) => [k, jsonifyValue(val)])
450
+ Object.entries(obj).filter(([, val]) => typeof val !== "function").map(([k, val]) => [k, jsonifyValue(val)])
409
451
  );
410
452
  }
411
453
  return v;
@@ -498,11 +540,14 @@ var SubstrateClient = class _SubstrateClient {
498
540
 
499
541
  // src/evm/EvmClient.ts
500
542
  var EvmClient = class {
543
+ /** @param rpcUrl - HTTP URL of the EVM JSON-RPC endpoint (e.g. `"http://localhost:9933"`). */
501
544
  constructor(rpcUrl) {
502
545
  this.rpcUrl = rpcUrl;
503
546
  }
547
+ rpcUrl;
504
548
  /**
505
- * 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.
506
551
  */
507
552
  async request(method, params = []) {
508
553
  const res = await fetch(this.rpcUrl, {
@@ -522,6 +567,7 @@ var EvmClient = class {
522
567
  }
523
568
  /**
524
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.
525
571
  */
526
572
  async batchRequest(calls) {
527
573
  const body = calls.map((c, i) => ({
@@ -566,30 +612,22 @@ var EvmClient = class {
566
612
  const hex = await this.request("eth_gasPrice", []);
567
613
  return hexToBigint(hex);
568
614
  }
569
- /**
570
- * Submits a signed raw transaction. Returns the transaction hash.
571
- */
615
+ /** Submits a signed raw transaction. Returns the transaction hash. */
572
616
  async sendRawTransaction(signedHex) {
573
617
  return this.request("eth_sendRawTransaction", [signedHex]);
574
618
  }
575
- /**
576
- * Executes a read-only call without creating a transaction.
577
- */
619
+ /** Executes a read-only call without creating a transaction. Returns the raw ABI-encoded response. */
578
620
  async call(to, data, from) {
579
621
  const txObj = { to, data };
580
622
  if (from) txObj["from"] = from;
581
623
  return this.request("eth_call", [txObj, "latest"]);
582
624
  }
583
- /**
584
- * Estimates the gas for a transaction.
585
- */
625
+ /** Estimates the gas required for a transaction. Returns the estimate in wei as a `bigint`. */
586
626
  async estimateGas(params) {
587
627
  const hex = await this.request("eth_estimateGas", [params]);
588
628
  return hexToBigint(hex);
589
629
  }
590
- /**
591
- * Returns a transaction receipt by hash, or null if not yet mined.
592
- */
630
+ /** Returns a transaction receipt by hash, or `null` if the transaction has not been mined yet. */
593
631
  async getTransactionReceipt(txHash) {
594
632
  const res = await fetch(this.rpcUrl, {
595
633
  method: "POST",
@@ -608,6 +646,56 @@ var EvmClient = class {
608
646
  }
609
647
  return json.result ?? null;
610
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
+ }
611
699
  };
612
700
 
613
701
  // src/utils/format.ts
@@ -680,10 +768,13 @@ function formatORB(raw, precision = 6) {
680
768
 
681
769
  // src/evm-explorer/EvmExplorer.ts
682
770
  var EvmExplorer = class _EvmExplorer {
771
+ /** @param evm - Underlying `EvmClient` used for all RPC calls. */
683
772
  constructor(evm) {
684
773
  this.evm = evm;
685
774
  }
775
+ evm;
686
776
  // --- Blocks ---
777
+ /** Returns the `count` most recent blocks in descending order (latest first). */
687
778
  async getLatestBlocks(count = 10) {
688
779
  const latest = await this.evm.getBlockNumber();
689
780
  const nums = Array.from({ length: Math.min(count, latest + 1) }, (_, i) => latest - i);
@@ -697,10 +788,12 @@ var EvmExplorer = class _EvmExplorer {
697
788
  );
698
789
  return results.filter((b) => b !== null && !!b.hash).map((b) => this.parseBlock(b));
699
790
  }
791
+ /** Returns a single block by number or hash, or `null` if not found. */
700
792
  async getBlock(hashOrNumber) {
701
793
  const b = await this.fetchBlock(hashOrNumber, false);
702
794
  return b ? this.parseBlock(b) : null;
703
795
  }
796
+ /** Returns all transactions in a block (with receipts), or `[]` if the block is not found. */
704
797
  async getBlockTransactions(hashOrNumber) {
705
798
  try {
706
799
  const b = await this.fetchBlock(hashOrNumber, true);
@@ -713,6 +806,7 @@ var EvmExplorer = class _EvmExplorer {
713
806
  }
714
807
  }
715
808
  // --- Transactions ---
809
+ /** Returns a single transaction with its receipt, or `null` if not found. */
716
810
  async getTransaction(hash) {
717
811
  try {
718
812
  const [tx, receipt] = await Promise.all([
@@ -725,6 +819,10 @@ var EvmExplorer = class _EvmExplorer {
725
819
  return null;
726
820
  }
727
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
+ */
728
826
  async getTransactionsByAddress(address, maxBlocks = 300) {
729
827
  const addr = address.toLowerCase();
730
828
  const latest = await this.evm.getBlockNumber();
@@ -776,6 +874,10 @@ var EvmExplorer = class _EvmExplorer {
776
874
  return results;
777
875
  }
778
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
+ */
779
881
  async getAddressInfo(address) {
780
882
  const latest = await this.evm.getBlockNumber().catch(() => 0);
781
883
  const fromBlock = `0x${Math.max(0, latest - 5e3).toString(16)}`;
@@ -806,6 +908,7 @@ var EvmExplorer = class _EvmExplorer {
806
908
  recentLogs
807
909
  };
808
910
  }
911
+ /** Returns the native token balance of `address`, formatted as a decimal string (no symbol). */
809
912
  async getBalance(address) {
810
913
  try {
811
914
  const val = await this.evm.getBalance(address);
@@ -814,6 +917,7 @@ var EvmExplorer = class _EvmExplorer {
814
917
  return "0";
815
918
  }
816
919
  }
920
+ /** Returns the current transaction count (nonce) for `address`, or `0` on error. */
817
921
  async getNonce(address) {
818
922
  try {
819
923
  return await this.evm.getTransactionCount(address);
@@ -821,6 +925,7 @@ var EvmExplorer = class _EvmExplorer {
821
925
  return 0;
822
926
  }
823
927
  }
928
+ /** Returns `true` when `address` has non-empty deployed bytecode. */
824
929
  async getIsContract(address) {
825
930
  try {
826
931
  const code = await this.evm.request("eth_getCode", [address, "latest"]);
@@ -830,6 +935,10 @@ var EvmExplorer = class _EvmExplorer {
830
935
  }
831
936
  }
832
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
+ */
833
942
  async getTokenInfo(address) {
834
943
  const addr = address.toLowerCase();
835
944
  const [name, symbol, decimals, totalSupply] = await this.evm.batchRequest([
@@ -849,6 +958,10 @@ var EvmExplorer = class _EvmExplorer {
849
958
  isErc20
850
959
  };
851
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
+ */
852
965
  async getTokenTransfers(address, holderAddress) {
853
966
  const TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
854
967
  const latest = await this.evm.getBlockNumber().catch(() => 0);
@@ -876,12 +989,14 @@ var EvmExplorer = class _EvmExplorer {
876
989
  logIndex: hexToNumber(l.logIndex)
877
990
  }));
878
991
  }
992
+ /** Returns the raw ERC-20 balance of `holderAddress` for the token at `tokenAddress` (0x-prefixed hex). */
879
993
  async getTokenBalance(tokenAddress, holderAddress) {
880
994
  const padded = holderAddress.replace(/^0x/, "").toLowerCase().padStart(64, "0");
881
995
  const result = await this.ethCall(tokenAddress, `0x70a08231${padded}`);
882
996
  return result ?? "0x0";
883
997
  }
884
998
  // --- Private: parsers ---
999
+ /** Maps a raw RPC block object to the public `EvmBlock` shape. */
885
1000
  parseBlock(b) {
886
1001
  return {
887
1002
  hash: b.hash,
@@ -894,6 +1009,7 @@ var EvmExplorer = class _EvmExplorer {
894
1009
  parentHash: b.parentHash
895
1010
  };
896
1011
  }
1012
+ /** Maps a raw RPC transaction + optional receipt to the public `EvmTransaction` shape. */
897
1013
  parseTx(tx, receipt) {
898
1014
  const parsed = {
899
1015
  hash: tx.hash,
@@ -913,6 +1029,10 @@ var EvmExplorer = class _EvmExplorer {
913
1029
  return parsed;
914
1030
  }
915
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
+ */
916
1036
  async fetchBlock(hashOrNumber, withTxObjects) {
917
1037
  try {
918
1038
  if (typeof hashOrNumber === "number" || /^\d+$/.test(String(hashOrNumber))) {
@@ -930,6 +1050,7 @@ var EvmExplorer = class _EvmExplorer {
930
1050
  return null;
931
1051
  }
932
1052
  }
1053
+ /** Executes a read-only `eth_call` and returns the raw hex result, or `null` on error. */
933
1054
  async ethCall(to, data) {
934
1055
  try {
935
1056
  return await this.evm.call(to, data);
@@ -938,6 +1059,7 @@ var EvmExplorer = class _EvmExplorer {
938
1059
  }
939
1060
  }
940
1061
  // --- Private static: ABI decoders ---
1062
+ /** Decodes an ABI-encoded `string` return value from a raw 0x-prefixed hex string. */
941
1063
  static decodeAbiString(hex) {
942
1064
  if (!hex || hex === "0x") return "";
943
1065
  const data = hex.startsWith("0x") ? hex.slice(2) : hex;
@@ -952,11 +1074,13 @@ var EvmExplorer = class _EvmExplorer {
952
1074
  return "";
953
1075
  }
954
1076
  }
1077
+ /** Decodes an ABI-encoded `uint256` return value to a `bigint`. */
955
1078
  static decodeAbiUint(hex) {
956
1079
  if (!hex || hex === "0x") return 0n;
957
1080
  const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
958
1081
  return BigInt(`0x${clean || "0"}`);
959
1082
  }
1083
+ /** Converts a 0x-prefixed hex number to its decimal string representation. Returns `'0'` on parse error. */
960
1084
  static hexToDecimalStr(hex) {
961
1085
  try {
962
1086
  return BigInt(hex).toString();
@@ -991,6 +1115,24 @@ var IndexerClient = class {
991
1115
  }
992
1116
  return res.json();
993
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
+ }
994
1136
  async getOrNull(path) {
995
1137
  const res = await this._fetchResponse(path);
996
1138
  if (res.status === 404) return null;
@@ -1026,6 +1168,21 @@ var IndexerClient = class {
1026
1168
  `/shielded/commitments/${encodeURIComponent(hex)}`
1027
1169
  );
1028
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
+ }
1029
1186
  // ─── Nullifiers ────────────────────────────────────────────────────────────
1030
1187
  /** Returns a paginated list of spent nullifiers. */
1031
1188
  async getNullifiers(params) {
@@ -1038,11 +1195,50 @@ var IndexerClient = class {
1038
1195
  `/shielded/nullifier/${encodeURIComponent(hex)}/status`
1039
1196
  );
1040
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
+ }
1041
1210
  // ─── Private transfers ─────────────────────────────────────────────────────
1042
- /** Returns a paginated list of private transfer events. */
1043
- async getTransfers(params) {
1044
- const qs = this.buildQuery({ page: params?.page, limit: params?.limit });
1045
- 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;
1046
1242
  }
1047
1243
  // ─── Unshields ─────────────────────────────────────────────────────────────
1048
1244
  /** Returns a paginated list of unshield events. */
@@ -1102,6 +1298,16 @@ var IndexerClient = class {
1102
1298
  `/address/${encodeURIComponent(address.toLowerCase())}/shielded${qs}`
1103
1299
  );
1104
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
+ }
1105
1311
  /**
1106
1312
  * Returns a paginated list of all shielded activity (commitments, unshields,
1107
1313
  * private transfers) associated with the given address.
@@ -1137,10 +1343,24 @@ var IndexerClient = class {
1137
1343
  }
1138
1344
  };
1139
1345
 
1140
- // src/shielded-pool/ShieldedPoolModule.ts
1346
+ // src/shielded-pool/pallet/ShieldedPoolModule.ts
1141
1347
  var import_polkadot_api2 = require("polkadot-api");
1142
1348
 
1143
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
+ }
1144
1364
  function toTxResult(payload) {
1145
1365
  const base = {
1146
1366
  txHash: payload.txHash,
@@ -1149,13 +1369,18 @@ function toTxResult(payload) {
1149
1369
  ok: payload.ok
1150
1370
  };
1151
1371
  if (!payload.ok) {
1152
- return { ...base, error: payload.dispatchError.type };
1372
+ return { ...base, error: formatDispatchError(payload.dispatchError) };
1153
1373
  }
1154
1374
  return base;
1155
1375
  }
1156
1376
  function callUnsafeTx(txEntry, ...args) {
1157
1377
  return txEntry(...args);
1158
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
+ }
1159
1384
  function resolveTx(unsafe, pallet, call) {
1160
1385
  const u = unsafe;
1161
1386
  const p = u["tx"]?.[pallet];
@@ -1166,9 +1391,10 @@ function resolveTx(unsafe, pallet, call) {
1166
1391
  return entry;
1167
1392
  }
1168
1393
 
1169
- // src/shielded-pool/EncryptedMemo.ts
1394
+ // src/shielded-pool/protocol/EncryptedMemo.ts
1170
1395
  var import_chacha = require("@noble/ciphers/chacha.js");
1171
1396
  var import_utils = require("@noble/ciphers/utils.js");
1397
+ var import_baby_jubjub = require("@zk-kit/baby-jubjub");
1172
1398
 
1173
1399
  // src/utils/bytes.ts
1174
1400
  function bigintTo32Le(n) {
@@ -1215,77 +1441,114 @@ function computePathIndices(leafIndex, depth) {
1215
1441
  return indices;
1216
1442
  }
1217
1443
  function leHexToBigint(hex) {
1218
- const h = hex.startsWith("0x") ? hex.slice(2) : hex;
1219
- const bytes = new Uint8Array(h.length / 2);
1220
- for (let i = 0; i < bytes.length; i++) {
1221
- bytes[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
1222
- }
1223
- return bytesToBigintLE(bytes);
1444
+ return bytesToBigintLE(fromHex(hex));
1224
1445
  }
1225
1446
 
1226
- // 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
1227
1452
  var import_sha2 = require("@noble/hashes/sha2.js");
1228
1453
  var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
1229
- var MEMO_PLAINTEXT_SIZE = 76;
1230
- function serializeMemo(value, ownerPk, blinding, assetId) {
1454
+ var MEMO_PLAINTEXT_SIZE = 116;
1455
+ function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk) {
1231
1456
  const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
1232
1457
  const view = new DataView(buf.buffer);
1233
1458
  view.setBigUint64(0, value & 0xffffffffffffffffn, true);
1234
- buf.set(ownerPk.slice(0, 32), 8);
1235
- buf.set(blinding.slice(0, 32), 40);
1236
- 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);
1237
1464
  return buf;
1238
1465
  }
1239
- function deriveEncryptionKey(viewingKey, commitment) {
1466
+ function deriveEncryptionKey(sharedSecret, commitment) {
1240
1467
  const h = import_sha2.sha256.create();
1241
- h.update(viewingKey);
1468
+ h.update(sharedSecret);
1242
1469
  h.update(commitment);
1243
1470
  h.update(KEY_DOMAIN);
1244
1471
  return h.digest();
1245
1472
  }
1246
- function toBase64(buf) {
1247
- const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
1248
- let str = "";
1249
- for (const b of bytes) str += String.fromCharCode(b);
1250
- return btoa(str);
1251
- }
1252
- function fromBase64(b64) {
1253
- const bin = atob(b64);
1254
- const out = new Uint8Array(bin.length);
1255
- for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1256
- return out;
1257
- }
1258
1473
 
1259
- // src/shielded-pool/EncryptedMemo.ts
1474
+ // src/shielded-pool/protocol/EncryptedMemo.ts
1260
1475
  var NONCE_SIZE = 12;
1261
- 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
+ }
1262
1500
  var EncryptedMemo = {
1263
1501
  /**
1264
- * Build and encrypt a memo for a note.
1502
+ * Build and encrypt a memo for a note using ECDH (v2, 168 bytes).
1265
1503
  *
1266
- * @param value Note value in planck.
1267
- * @param ownerPk 32-byte owner public key (little-endian).
1268
- * @param blinding 32-byte blinding scalar (little-endian).
1269
- * @param assetId Asset identifier.
1270
- * @param commitment 32-byte commitment bytes (little-endian).
1271
- * @param recipientVk 32-byte recipient viewing key pass `new Uint8Array(32)`
1272
- * for a publicly-readable (dummy) memo.
1273
- * @returns 104-byte encrypted memo (nonce || ciphertext).
1274
- */
1275
- 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) {
1276
1517
  const nonce = (0, import_utils.randomBytes)(NONCE_SIZE);
1277
- const key = deriveEncryptionKey(recipientVk, commitment);
1278
- const plaintext = serializeMemo(value, ownerPk, blinding, assetId);
1279
- 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);
1280
1541
  const ciphertext = cipher.encrypt(plaintext);
1281
- const result = new Uint8Array(NONCE_SIZE + ciphertext.length);
1542
+ const result = new Uint8Array(ENCRYPTED_MEMO_SIZE);
1282
1543
  result.set(nonce, 0);
1283
1544
  result.set(ciphertext, NONCE_SIZE);
1545
+ result.set(ephPkPackedBytes, NONCE_SIZE + CIPHERTEXT_SIZE);
1284
1546
  return result;
1285
1547
  },
1286
1548
  /**
1287
- * Returns a 104-byte public memo with a zero recipient viewing key.
1288
- * 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))`.
1289
1552
  */
1290
1553
  encryptPublic(value, ownerPk, blinding, assetId, commitment) {
1291
1554
  return EncryptedMemo.encrypt(
@@ -1298,226 +1561,89 @@ var EncryptedMemo = {
1298
1561
  );
1299
1562
  },
1300
1563
  /**
1301
- * 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).
1302
1565
  */
1303
1566
  dummy() {
1304
1567
  return new Uint8Array(ENCRYPTED_MEMO_SIZE);
1305
1568
  },
1306
1569
  /**
1307
- * 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).
1308
1572
  *
1309
- * Returns `null` if decryption fails wrong key, bad MAC, or malformed memo.
1310
- * 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.
1311
1575
  *
1312
- * @param memoBytes 104-byte encrypted memo.
1313
- * @param commitment 32-byte note commitment (little-endian).
1314
- * @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]').
1315
1578
  */
1316
- decrypt(memoBytes, commitment, recipientVk) {
1317
- if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
1318
- try {
1319
- const nonce = memoBytes.slice(0, NONCE_SIZE);
1320
- const ciphertext = memoBytes.slice(NONCE_SIZE);
1321
- const key = deriveEncryptionKey(recipientVk, commitment);
1322
- const cipher = (0, import_chacha.chacha20poly1305)(key, nonce);
1323
- const plaintext = cipher.decrypt(ciphertext);
1324
- const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
1325
- const value = view.getBigUint64(0, true);
1326
- const ownerPk = bytesToBigintLE(plaintext.slice(8, 40));
1327
- const blinding = bytesToBigintLE(plaintext.slice(40, 72));
1328
- const assetId = BigInt(view.getUint32(72, true));
1329
- return { value, ownerPk, blinding, assetId };
1330
- } catch {
1331
- 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
+ );
1332
1585
  }
1333
- }
1334
- };
1335
-
1336
- // src/shielded-pool/NoteBuilder.ts
1337
- var import_poseidon_lite = require("poseidon-lite");
1338
- var NoteBuilder = class {
1586
+ },
1339
1587
  /**
1340
- * 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.
1341
1591
  *
1342
- * @param input.value Amount in planck (required).
1343
- * @param input.assetId Asset ID default 0n (native ORB-Privacy).
1344
- * @param input.ownerPk BabyJubJub Ax default 0n.
1345
- * @param input.blinding Random scalar — defaults to BigInt(Date.now()).
1346
- * @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().
1347
1595
  */
1348
- static async build(input) {
1349
- const value = input.value;
1350
- const assetId = input.assetId ?? 0n;
1351
- const ownerPk = input.ownerPk ?? 0n;
1352
- const blinding = input.blinding ?? BigInt(Date.now());
1353
- const spendingKey = input.spendingKey ?? 0n;
1354
- const commitment = (0, import_poseidon_lite.poseidon4)([value, assetId, ownerPk, blinding]);
1355
- const nullifier = (0, import_poseidon_lite.poseidon2)([commitment, spendingKey]);
1356
- const commitmentBytes = bigintTo32Le(commitment);
1357
- const nullifierBytes = bigintTo32Le(nullifier);
1358
- const memo = input.viewingKey !== void 0 ? Array.from(
1359
- EncryptedMemo.encrypt(
1360
- value,
1361
- bigintTo32Le(ownerPk),
1362
- bigintTo32Le(blinding),
1363
- Number(assetId),
1364
- commitmentBytes,
1365
- input.viewingKey
1366
- )
1367
- ) : Array.from(EncryptedMemo.dummy());
1368
- const note = {
1369
- value,
1370
- assetId,
1371
- ownerPk,
1372
- blinding,
1373
- spendingKey,
1374
- spent: false,
1375
- spentAt: null,
1376
- commitment,
1377
- nullifier,
1378
- commitmentHex: toHex(commitmentBytes),
1379
- nullifierHex: toHex(nullifierBytes),
1380
- memo
1381
- };
1382
- return note;
1383
- }
1596
+ decrypt(memoBytes, commitment, viewingSecretKey) {
1597
+ if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
1598
+ return EncryptedMemo._decrypt(memoBytes, commitment, viewingSecretKey);
1599
+ },
1384
1600
  /**
1385
- * 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.
1386
1602
  *
1387
- * Pure TypeScript implementation no WASM dependency.
1388
- * Uses ChaCha20-Poly1305 with SHA-256 key derivation.
1389
- *
1390
- * @param note The ZkNote whose fields populate the plaintext.
1391
- * @param recipientVk 32-byte recipient viewing key.
1392
- * Pass `new Uint8Array(32)` (default) for a public/dummy memo.
1393
- */
1394
- static buildMemo(note, recipientVk) {
1395
- return EncryptedMemo.encrypt(
1396
- note.value,
1397
- bigintTo32Le(note.ownerPk),
1398
- bigintTo32Le(note.blinding),
1399
- Number(note.assetId),
1400
- bigintTo32Le(note.commitment),
1401
- recipientVk ?? new Uint8Array(32)
1402
- );
1403
- }
1404
- };
1405
-
1406
- // src/shielded-pool/ShieldedPoolModule.ts
1407
- var ShieldedPoolModule = class {
1408
- constructor(substrate) {
1409
- this.substrate = substrate;
1410
- }
1411
- // ─── Extrinsics ────────────────────────────────────────────────────────────
1412
- /**
1413
- * Deposits tokens into the shielded pool.
1414
- * Extrinsic: shieldedPool.shield(assetId, amount, commitment, encryptedMemo)
1415
- */
1416
- async shield(params, signer) {
1417
- const memo = params.encryptedMemo ?? EncryptedMemo.dummy();
1418
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "shield");
1419
- const tx = callUnsafeTx(
1420
- entry,
1421
- params.assetId,
1422
- params.amount.toString(),
1423
- import_polkadot_api2.Binary.fromHex(params.commitment),
1424
- import_polkadot_api2.Binary.fromBytes(memo)
1425
- );
1426
- return toTxResult(await tx.signAndSubmit(signer));
1427
- }
1428
- /**
1429
- * Build a ZkNote locally and submit shieldedPool.shield in one call.
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.
1430
1605
  *
1431
- * Returns both the on-chain result and the note **save the note locally**,
1432
- * 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.
1433
1609
  *
1434
- * @param params.value Amount in planck (required).
1435
- * @param params.assetId Asset ID default 0 (native ORB-Privacy).
1436
- * @param params.ownerPk BabyJubJub Ax (default 0n).
1437
- * @param params.blinding Random blinding scalar (default BigInt(Date.now())).
1438
- * @param params.spendingKey Secret spending key (default 0n).
1439
- */
1440
- async buildAndShield(params, signer) {
1441
- const noteInput = {
1442
- value: params.value,
1443
- ...params.assetId !== void 0 && { assetId: BigInt(params.assetId) },
1444
- ...params.ownerPk !== void 0 && { ownerPk: params.ownerPk },
1445
- ...params.blinding !== void 0 && { blinding: params.blinding },
1446
- ...params.spendingKey !== void 0 && { spendingKey: params.spendingKey }
1447
- };
1448
- const note = await NoteBuilder.build(noteInput);
1449
- const memo = NoteBuilder.buildMemo(note);
1450
- const txResult = await this.shield(
1451
- {
1452
- assetId: Number(note.assetId),
1453
- amount: note.value,
1454
- commitment: note.commitmentHex,
1455
- encryptedMemo: memo
1456
- },
1457
- signer
1458
- );
1459
- return { txResult, note };
1460
- }
1461
- /**
1462
- * Withdraws tokens from the shielded pool to a public address.
1463
- * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient)
1464
- */
1465
- async unshield(params, signer) {
1466
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "unshield");
1467
- const tx = callUnsafeTx(
1468
- entry,
1469
- import_polkadot_api2.Binary.fromBytes(params.proof),
1470
- import_polkadot_api2.Binary.fromHex(params.merkleRoot),
1471
- import_polkadot_api2.Binary.fromHex(params.nullifier),
1472
- params.assetId,
1473
- params.amount.toString(),
1474
- import_polkadot_api2.Binary.fromHex(params.recipientAddress)
1475
- );
1476
- return toTxResult(await tx.signAndSubmit(signer));
1477
- }
1478
- /**
1479
- * Performs a private (shielded) transfer between two notes.
1480
- * 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().
1481
1612
  */
1482
- async privateTransfer(params, signer) {
1483
- const inputs = params.inputs.map((inp) => ({
1484
- nullifier: import_polkadot_api2.Binary.fromHex(inp.nullifier),
1485
- commitment: import_polkadot_api2.Binary.fromHex(inp.commitment)
1486
- }));
1487
- const outputs = params.outputs.map((out) => ({
1488
- commitment: import_polkadot_api2.Binary.fromHex(out.commitment),
1489
- memo: import_polkadot_api2.Binary.fromBytes(out.encryptedMemo ?? EncryptedMemo.dummy())
1490
- }));
1491
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "privateTransfer");
1492
- const tx = callUnsafeTx(
1493
- entry,
1494
- inputs,
1495
- outputs,
1496
- import_polkadot_api2.Binary.fromBytes(params.proof),
1497
- import_polkadot_api2.Binary.fromHex(params.merkleRoot)
1498
- );
1499
- return toTxResult(await tx.signAndSubmit(signer));
1500
- }
1501
- /**
1502
- * Deposits multiple notes into the shielded pool in a single extrinsic.
1503
- * Extrinsic: shieldedPool.shieldBatch(operations) — max 20 items.
1504
- */
1505
- async shieldBatch(params, signer) {
1506
- const operations = params.items.map((item) => ({
1507
- assetId: item.assetId,
1508
- amount: item.amount.toString(),
1509
- commitment: import_polkadot_api2.Binary.fromHex(item.commitment),
1510
- encryptedMemo: import_polkadot_api2.Binary.fromBytes(item.encryptedMemo ?? EncryptedMemo.dummy())
1511
- }));
1512
- const entry = resolveTx(this.substrate.unsafe, "shieldedPool", "shieldBatch");
1513
- const tx = callUnsafeTx(entry, operations);
1514
- 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);
1515
1644
  }
1516
1645
  };
1517
1646
 
1518
- // src/account-mapping/AccountMappingModule.ts
1519
- var import_polkadot_api3 = require("polkadot-api");
1520
-
1521
1647
  // src/utils/address.ts
1522
1648
  var import_util_crypto = require("@polkadot/util-crypto");
1523
1649
  function normalizeEvmAddress(addr) {
@@ -1654,6 +1780,242 @@ function addressToAccountIdHex(addr) {
1654
1780
  return substrateSs58ToAccountIdHex(addr);
1655
1781
  }
1656
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
+
1657
2019
  // src/account-mapping/helpers.ts
1658
2020
  function mapRawScheme(raw) {
1659
2021
  if (raw === "Eip191" || raw === "eip191") return "Eip191";
@@ -1666,6 +2028,7 @@ var AccountMappingModule = class {
1666
2028
  constructor(substrate) {
1667
2029
  this.substrate = substrate;
1668
2030
  }
2031
+ substrate;
1669
2032
  // ─── Address resolution ─────────────────────────────────────────────────────
1670
2033
  /**
1671
2034
  * Returns the explicitly mapped (or fallback) Substrate AccountId32 hex for
@@ -2051,6 +2414,7 @@ var PrivacyModule = class {
2051
2414
  constructor(substrate) {
2052
2415
  this.substrate = substrate;
2053
2416
  }
2417
+ substrate;
2054
2418
  /** Returns the current Merkle tree root. */
2055
2419
  async getMerkleRoot() {
2056
2420
  return this.substrate.request("privacy_getMerkleRoot", []);
@@ -2068,14 +2432,23 @@ var PrivacyModule = class {
2068
2432
  }
2069
2433
  /**
2070
2434
  * Returns the Merkle inclusion proof for a given commitment hex,
2071
- * 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.
2072
2440
  */
2073
2441
  async getMerkleProofByCommitment(commitmentHex) {
2074
- const [proof, root] = await Promise.all([
2075
- this.getMerkleProof(commitmentHex),
2076
- this.getMerkleRoot()
2077
- ]);
2078
- 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
+ };
2079
2452
  }
2080
2453
  /** Returns the spend status of a nullifier. */
2081
2454
  async getNullifierStatus(nullifier) {
@@ -2094,6 +2467,7 @@ var PrivacyModule = class {
2094
2467
  return {
2095
2468
  merkleRoot: raw.merkle_root,
2096
2469
  commitmentCount: raw.commitment_count,
2470
+ nullifierCount: raw.nullifier_count,
2097
2471
  totalBalance: raw.total_balance.toString(),
2098
2472
  assetBalances: raw.asset_balances.map(mapAssetBalance),
2099
2473
  treeDepth: raw.tree_depth
@@ -2119,6 +2493,7 @@ var ZkVerifierModule = class {
2119
2493
  constructor(substrate) {
2120
2494
  this.substrate = substrate;
2121
2495
  }
2496
+ substrate;
2122
2497
  /** Returns basic version info for all registered circuits. */
2123
2498
  async getAllCircuitVersions() {
2124
2499
  const raw = await this.substrate.request(
@@ -2137,6 +2512,48 @@ var ZkVerifierModule = class {
2137
2512
  }
2138
2513
  };
2139
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
+
2140
2557
  // src/precompiles/helpers.ts
2141
2558
  var STATIC_TYPES = /* @__PURE__ */ new Set(["uint", "bytes32", "address", "bool"]);
2142
2559
  function concat(arrays) {
@@ -2280,10 +2697,6 @@ function decodeBytes(data, slotOffset = 0) {
2280
2697
  function decodeString(data, slotOffset = 0) {
2281
2698
  return new TextDecoder().decode(decodeBytes(data, slotOffset));
2282
2699
  }
2283
- function hexToBytes(hex) {
2284
- if (hex === "0x" || hex === "") return new Uint8Array(0);
2285
- return fromHex(hex.startsWith("0x") ? hex : "0x" + hex);
2286
- }
2287
2700
 
2288
2701
  // src/precompiles/addresses.ts
2289
2702
  var PRECOMPILE_ADDR = {
@@ -2342,12 +2755,20 @@ var AM_SEL = {
2342
2755
  SET_ACCOUNT_METADATA: new Uint8Array([119, 108, 249, 255])
2343
2756
  };
2344
2757
  var SP_SEL = {
2345
- // shield(uint32,uint256,bytes32,bytes) 0x781442b9
2346
- SHIELD: new Uint8Array([120, 20, 66, 185]),
2347
- // privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[]) 0xdcd5b898
2348
- PRIVATE_TRANSFER: new Uint8Array([220, 213, 184, 152]),
2349
- // unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32) 0xdcf1bff2
2350
- 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])
2351
2772
  };
2352
2773
  var KNOWN_PRECOMPILES = {
2353
2774
  // ── Ethereum standard (EIP) ─────────────────────────────────────────────
@@ -2390,9 +2811,13 @@ var KNOWN_PRECOMPILES = {
2390
2811
  "0x0000000000000000000000000000000000000801": {
2391
2812
  name: "ShieldedPool",
2392
2813
  functions: {
2393
- "781442b9": "shield(uint32,uint256,bytes32,bytes)",
2394
- dcd5b898: "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[])",
2395
- 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)"
2396
2821
  }
2397
2822
  }
2398
2823
  };
@@ -2406,43 +2831,58 @@ var ShieldedPoolPrecompile = class {
2406
2831
  constructor(evm) {
2407
2832
  this.evm = evm;
2408
2833
  }
2834
+ evm;
2409
2835
  addr = PRECOMPILE_ADDR.SHIELDED_POOL;
2410
2836
  // ─── shield ────────────────────────────────────────────────────────────────
2411
2837
  /**
2412
- * Returns the ABI-encoded calldata for `shield(uint32, uint256, bytes32, bytes)`.
2413
- * 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.
2414
2841
  */
2415
2842
  buildShieldCalldata(params) {
2416
- const memo = params.encryptedMemo ?? EncryptedMemo.dummy();
2843
+ EncryptedMemo.validate(params.encryptedMemo, "buildShieldCalldata.encryptedMemo");
2417
2844
  const commitment = fromHex(params.commitment);
2418
2845
  return encodeHex(
2419
2846
  SP_SEL.SHIELD,
2420
2847
  { type: "uint", value: BigInt(params.assetId) },
2421
- { type: "uint", value: params.amount },
2422
2848
  { type: "bytes32", value: commitment },
2423
- { type: "bytes", value: memo }
2849
+ { type: "bytes", value: params.encryptedMemo }
2424
2850
  );
2425
2851
  }
2426
2852
  /**
2427
- * Deposits tokens into the shielded pool from an EVM transaction.
2853
+ * Deposits tokens into the shielded pool from a payable EVM transaction.
2428
2854
  *
2429
- * The EVM caller's address is deterministically mapped to a Substrate
2430
- * 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.
2431
2861
  *
2432
2862
  * Extrinsic: `shieldedPool.shield(assetId, amount, commitment, encryptedMemo)`
2433
2863
  */
2434
2864
  async shield(params, signer) {
2435
- 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
+ });
2436
2870
  }
2437
2871
  // ─── privateTransfer ───────────────────────────────────────────────────────
2438
2872
  /**
2439
2873
  * Returns the ABI-encoded calldata for
2440
- * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[])`.
2874
+ * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[], uint32, uint256)`.
2441
2875
  */
2442
2876
  buildPrivateTransferCalldata(params) {
2443
2877
  const nullifiers = params.inputs.map((i) => fromHex(i.nullifier));
2444
2878
  const commitments = params.outputs.map((o) => fromHex(o.commitment));
2445
- 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
+ });
2446
2886
  const root = fromHex(params.merkleRoot);
2447
2887
  return encodeHex(
2448
2888
  SP_SEL.PRIVATE_TRANSFER,
@@ -2450,7 +2890,9 @@ var ShieldedPoolPrecompile = class {
2450
2890
  { type: "bytes32", value: root },
2451
2891
  { type: "bytes32[]", value: nullifiers },
2452
2892
  { type: "bytes32[]", value: commitments },
2453
- { type: "bytes[]", value: memos }
2893
+ { type: "bytes[]", value: memos },
2894
+ { type: "uint", value: BigInt(params.assetId) },
2895
+ { type: "uint", value: params.fee ?? 0n }
2454
2896
  );
2455
2897
  }
2456
2898
  /**
@@ -2478,6 +2920,9 @@ var ShieldedPoolPrecompile = class {
2478
2920
  const recipientBytes = fromHex(
2479
2921
  "0x" + (recipientRaw.length === 64 ? recipientRaw : recipientRaw.padEnd(64, "0"))
2480
2922
  );
2923
+ const changeCommitmentHex = params.changeCommitment ?? "0x" + "00".repeat(32);
2924
+ const changeCommitment = fromHex(changeCommitmentHex);
2925
+ const changeEncryptedMemo = params.changeEncryptedMemo ?? new Uint8Array();
2481
2926
  return encodeHex(
2482
2927
  SP_SEL.UNSHIELD,
2483
2928
  { type: "bytes", value: proof },
@@ -2485,7 +2930,10 @@ var ShieldedPoolPrecompile = class {
2485
2930
  { type: "bytes32", value: nullifier },
2486
2931
  { type: "uint", value: BigInt(params.assetId) },
2487
2932
  { type: "uint", value: params.amount },
2488
- { 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 }
2489
2937
  );
2490
2938
  }
2491
2939
  /**
@@ -2513,24 +2961,151 @@ var ShieldedPoolPrecompile = class {
2513
2961
  });
2514
2962
  }
2515
2963
  /**
2516
- * 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.
2517
3088
  */
2518
- async estimatePrivateTransferGas(params, from) {
2519
- return this.evm.estimateGas({
2520
- from,
2521
- to: this.addr,
2522
- data: this.buildPrivateTransferCalldata(params)
2523
- });
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
+ );
2524
3099
  }
2525
3100
  /**
2526
- * 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)`
2527
3106
  */
2528
- async estimateUnshieldGas(params, from) {
2529
- return this.evm.estimateGas({
2530
- from,
2531
- to: this.addr,
2532
- data: this.buildUnshieldCalldata(params)
2533
- });
3107
+ async pruneExpiredRequest(params, signer) {
3108
+ return signer({ to: this.addr, data: this.buildPruneExpiredRequestCalldata(params) });
2534
3109
  }
2535
3110
  };
2536
3111
 
@@ -2539,6 +3114,7 @@ var AccountMappingPrecompile = class {
2539
3114
  constructor(evm) {
2540
3115
  this.evm = evm;
2541
3116
  }
3117
+ evm;
2542
3118
  addr = PRECOMPILE_ADDR.ACCOUNT_MAPPING;
2543
3119
  // ─── Read-only ─────────────────────────────────────────────────────────────
2544
3120
  /**
@@ -2550,7 +3126,7 @@ var AccountMappingPrecompile = class {
2550
3126
  async resolveAlias(alias) {
2551
3127
  try {
2552
3128
  const data = encodeHex(AM_SEL.RESOLVE_ALIAS, { type: "string", value: alias });
2553
- const raw = hexToBytes(await this.evm.call(this.addr, data));
3129
+ const raw = fromHex(await this.evm.call(this.addr, data));
2554
3130
  if (raw.length < 64) return null;
2555
3131
  const owner = decodeAddress2(raw, 0);
2556
3132
  const evm = decodeAddress2(raw, 32);
@@ -2573,7 +3149,7 @@ var AccountMappingPrecompile = class {
2573
3149
  type: "address",
2574
3150
  value: normalizeEvmAddress(evmAddress)
2575
3151
  });
2576
- const raw = hexToBytes(await this.evm.call(this.addr, data));
3152
+ const raw = fromHex(await this.evm.call(this.addr, data));
2577
3153
  if (raw.length === 0) return null;
2578
3154
  const alias = decodeString(raw, 0);
2579
3155
  return alias.length > 0 ? alias : null;
@@ -2593,7 +3169,7 @@ var AccountMappingPrecompile = class {
2593
3169
  { type: "string", value: alias },
2594
3170
  { type: "bytes32", value: commitmentBytes }
2595
3171
  );
2596
- const raw = hexToBytes(await this.evm.call(this.addr, data));
3172
+ const raw = fromHex(await this.evm.call(this.addr, data));
2597
3173
  if (raw.length < 32) return false;
2598
3174
  return decodeBool(raw, 0);
2599
3175
  } catch {
@@ -2798,6 +3374,7 @@ var CryptoPrecompiles = class {
2798
3374
  constructor(evm) {
2799
3375
  this.evm = evm;
2800
3376
  }
3377
+ evm;
2801
3378
  // ─── ECRecover (0x0001) ───────────────────────────────────────────────────
2802
3379
  /**
2803
3380
  * Recovers the Ethereum address from an ECDSA signature.
@@ -2813,7 +3390,7 @@ var CryptoPrecompiles = class {
2813
3390
  input[63] = v;
2814
3391
  input.set(r.slice(0, 32), 64);
2815
3392
  input.set(s.slice(0, 32), 96);
2816
- 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)));
2817
3394
  if (raw.length < 32) return "0x" + "00".repeat(20);
2818
3395
  return "0x" + toHex(raw.slice(12, 32)).slice(2);
2819
3396
  }
@@ -2829,7 +3406,7 @@ var CryptoPrecompiles = class {
2829
3406
  input[63] = v;
2830
3407
  input.set(r.slice(0, 32), 64);
2831
3408
  input.set(s.slice(0, 32), 96);
2832
- 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)));
2833
3410
  }
2834
3411
  // ─── SHA-256 (0x0002) ─────────────────────────────────────────────────────
2835
3412
  /**
@@ -2837,7 +3414,7 @@ var CryptoPrecompiles = class {
2837
3414
  * Returns a 32-byte digest.
2838
3415
  */
2839
3416
  async sha256(data) {
2840
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.SHA256, toHex(data)));
3417
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.SHA256, toHex(data)));
2841
3418
  }
2842
3419
  // ─── RIPEMD-160 (0x0003) ──────────────────────────────────────────────────
2843
3420
  /**
@@ -2845,7 +3422,7 @@ var CryptoPrecompiles = class {
2845
3422
  * Returns the 20-byte digest right-padded to 32 bytes (standard ABI output).
2846
3423
  */
2847
3424
  async ripemd160(data) {
2848
- 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)));
2849
3426
  return raw.length >= 32 ? raw.slice(12, 32) : raw;
2850
3427
  }
2851
3428
  // ─── Identity (0x0004) ────────────────────────────────────────────────────
@@ -2854,7 +3431,7 @@ var CryptoPrecompiles = class {
2854
3431
  * Mainly useful for gas benchmarking.
2855
3432
  */
2856
3433
  async identity(data) {
2857
- return hexToBytes(await this.evm.call(PRECOMPILE_ADDR.IDENTITY, toHex(data)));
3434
+ return fromHex(await this.evm.call(PRECOMPILE_ADDR.IDENTITY, toHex(data)));
2858
3435
  }
2859
3436
  // ─── SHA3-FIPS-256 / Keccak-256 (0x0400) ─────────────────────────────────
2860
3437
  /**
@@ -2862,7 +3439,7 @@ var CryptoPrecompiles = class {
2862
3439
  * Returns a 32-byte digest.
2863
3440
  */
2864
3441
  async keccak256(data) {
2865
- 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)));
2866
3443
  }
2867
3444
  // ─── Curve25519 / Ristretto (0x0402, 0x0403) ─────────────────────────────
2868
3445
  /**
@@ -2885,7 +3462,7 @@ var CryptoPrecompiles = class {
2885
3462
  }
2886
3463
  input.set(pt, i * 32);
2887
3464
  }
2888
- 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)));
2889
3466
  }
2890
3467
  /**
2891
3468
  * Multiplies a Ristretto compressed point by a scalar via EVM precompile.
@@ -2901,39 +3478,44 @@ var CryptoPrecompiles = class {
2901
3478
  const input = new Uint8Array(64);
2902
3479
  input.set(scalar, 0);
2903
3480
  input.set(point, 32);
2904
- 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)));
2905
3482
  }
2906
3483
  };
2907
3484
 
2908
3485
  // src/client/OrbinumClient.ts
2909
3486
  var OrbinumClient = class _OrbinumClient {
2910
- /** Raw access to the Substrate WebSocket connection and RPC. */
3487
+ /** Raw Substrate WebSocket connection use for custom RPC calls or low-level access. */
2911
3488
  substrate;
2912
- /** Raw access to the EVM HTTP JSON-RPC endpoint (if configured). */
3489
+ /** Raw EVM HTTP JSON-RPC client. `null` when `evmRpc` is not configured. */
2913
3490
  evm;
2914
3491
  /**
2915
- * High-level EVM block and transaction explorer (if `evmRpc` is configured).
3492
+ * High-level EVM block and transaction explorer.
2916
3493
  * Provides enriched queries for blocks, transactions, addresses, and token transfers.
3494
+ * `null` when `evmRpc` is not configured.
2917
3495
  */
2918
3496
  evmExplorer;
2919
3497
  /**
2920
- * HTTP client for the Orbinum indexer REST API (if `indexerUrl` is configured).
3498
+ * HTTP client for the Orbinum indexer REST API.
2921
3499
  * Provides paginated access to indexed blocks, extrinsics, shielded events, and nullifiers.
3500
+ * `null` when `indexerUrl` is not configured.
2922
3501
  */
2923
3502
  indexer;
2924
- /** Shielded-pool operations: shield, unshield, privateTransfer, and merkle queries. */
3503
+ /** Shielded-pool extrinsics and Merkle tree queries (`shield`, `unshield`, `privateTransfer`, …). */
2925
3504
  shieldedPool;
2926
- /** Account mapping: aliases, chain links, metadata, marketplace, and identity extrinsics. */
3505
+ /** Account-mapping extrinsics: aliases, chain links, metadata, marketplace, and identity. */
2927
3506
  accountMapping;
2928
- /** Typed access to Orbinum `privacy_*` RPC endpoints. */
3507
+ /** Typed access to `privacy_*` custom RPC endpoints. */
2929
3508
  privacy;
2930
- /** Typed access to zkVerifier_* RPC endpoints. */
3509
+ /** Typed access to `zkVerifier_*` custom RPC endpoints. */
2931
3510
  zkVerifier;
3511
+ /** Typed access to `relayer_*` RPC endpoints (registry lookup and pending fee queries). */
3512
+ relayerStatus;
2932
3513
  /**
2933
- * EVM precompiles: shielded pool + account mapping callable from an EVM wallet.
2934
- * 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`.
2935
3516
  */
2936
3517
  precompiles;
3518
+ /** @internal Use `OrbinumClient.connect()` to obtain an instance. */
2937
3519
  constructor(substrate, evm, indexer) {
2938
3520
  this.substrate = substrate;
2939
3521
  this.evm = evm;
@@ -2943,6 +3525,7 @@ var OrbinumClient = class _OrbinumClient {
2943
3525
  this.accountMapping = new AccountMappingModule(substrate);
2944
3526
  this.privacy = new PrivacyModule(substrate);
2945
3527
  this.zkVerifier = new ZkVerifierModule(substrate);
3528
+ this.relayerStatus = new RelayerStatusModule(substrate);
2946
3529
  this.precompiles = evm ? {
2947
3530
  shieldedPool: new ShieldedPoolPrecompile(evm),
2948
3531
  accountMapping: new AccountMappingPrecompile(evm),
@@ -2950,8 +3533,10 @@ var OrbinumClient = class _OrbinumClient {
2950
3533
  } : null;
2951
3534
  }
2952
3535
  /**
2953
- * Connects to an Orbinum node and returns a ready-to-use `OrbinumClient`.
2954
- * 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`.
2955
3540
  */
2956
3541
  static async connect(config) {
2957
3542
  const substrate = await SubstrateClient.connect(
@@ -2962,7 +3547,7 @@ var OrbinumClient = class _OrbinumClient {
2962
3547
  const indexer = config.indexerUrl ? new IndexerClient({ baseUrl: config.indexerUrl }) : null;
2963
3548
  return new _OrbinumClient(substrate, evm, indexer);
2964
3549
  }
2965
- /** Closes the WebSocket connection to the Substrate node. */
3550
+ /** Closes the underlying Substrate WebSocket connection and releases all resources. */
2966
3551
  destroy() {
2967
3552
  this.substrate.destroy();
2968
3553
  }
@@ -2991,6 +3576,7 @@ var OrbinumClientProvider = class {
2991
3576
  _reconnectAttempt = 0;
2992
3577
  // ─── Events ─────────────────────────────────────────────────────────────
2993
3578
  _listeners = /* @__PURE__ */ new Set();
3579
+ /** Creates a new provider with the given configuration. Does not connect automatically — call `connect()` to initiate. */
2994
3580
  constructor(config) {
2995
3581
  this.config = config;
2996
3582
  this.connectTimeoutMs = config.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
@@ -3000,9 +3586,11 @@ var OrbinumClientProvider = class {
3000
3586
  this.reconnectMaxMs = config.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
3001
3587
  }
3002
3588
  // ─── Status ─────────────────────────────────────────────────────────────
3589
+ /** Current connection status. Reflects the last state set by the provider internals. */
3003
3590
  get status() {
3004
3591
  return this._status;
3005
3592
  }
3593
+ /** Updates internal status and notifies all registered listeners. Swallows listener exceptions to avoid cascading failures. */
3006
3594
  setStatus(status, error) {
3007
3595
  this._status = status;
3008
3596
  const event = { status, ...error ? { error } : {} };
@@ -3013,6 +3601,10 @@ var OrbinumClientProvider = class {
3013
3601
  }
3014
3602
  });
3015
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
+ */
3016
3608
  onStatusChange(listener) {
3017
3609
  this._listeners.add(listener);
3018
3610
  return () => {
@@ -3020,10 +3612,18 @@ var OrbinumClientProvider = class {
3020
3612
  };
3021
3613
  }
3022
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
+ */
3023
3619
  connect() {
3024
3620
  if (this._status !== "idle") return;
3025
3621
  this.startConnectAttempt();
3026
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
+ */
3027
3627
  reset() {
3028
3628
  this.cancelReconnect();
3029
3629
  this.teardownClient();
@@ -3031,6 +3631,7 @@ var OrbinumClientProvider = class {
3031
3631
  this.setStatus("idle");
3032
3632
  }
3033
3633
  // ─── Internal connection flow ───────────────────────────────────────────
3634
+ /** Transitions to `'connecting'`, kicks off `attemptConnect`, and schedules a reconnect if it fails. */
3034
3635
  startConnectAttempt() {
3035
3636
  this.setStatus("connecting");
3036
3637
  this._connectingPromise = this.attemptConnect();
@@ -3038,6 +3639,11 @@ var OrbinumClientProvider = class {
3038
3639
  if (this._status !== "idle") this.scheduleReconnect();
3039
3640
  });
3040
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
+ */
3041
3647
  async attemptConnect() {
3042
3648
  let timeoutId = null;
3043
3649
  let orphanClient = null;
@@ -3085,6 +3691,7 @@ var OrbinumClientProvider = class {
3085
3691
  }
3086
3692
  }
3087
3693
  // ─── Heartbeat ──────────────────────────────────────────────────────────
3694
+ /** Starts the periodic heartbeat loop. Replaces any existing timer. */
3088
3695
  startHeartbeat() {
3089
3696
  this.stopHeartbeat();
3090
3697
  this._heartbeatTimer = setInterval(async () => {
@@ -3097,12 +3704,17 @@ var OrbinumClientProvider = class {
3097
3704
  }
3098
3705
  }, this.heartbeatIntervalMs);
3099
3706
  }
3707
+ /** Clears the heartbeat interval timer if active. */
3100
3708
  stopHeartbeat() {
3101
3709
  if (this._heartbeatTimer) {
3102
3710
  clearInterval(this._heartbeatTimer);
3103
3711
  this._heartbeatTimer = null;
3104
3712
  }
3105
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
+ */
3106
3718
  async probe() {
3107
3719
  if (!this._orbinumClient) return false;
3108
3720
  try {
@@ -3118,6 +3730,10 @@ var OrbinumClientProvider = class {
3118
3730
  }
3119
3731
  }
3120
3732
  // ─── Reconnection ───────────────────────────────────────────────────────
3733
+ /**
3734
+ * Schedules the next connection attempt using exponential backoff
3735
+ * (capped at `reconnectMaxMs`), then transitions to `'reconnecting'`.
3736
+ */
3121
3737
  scheduleReconnect() {
3122
3738
  if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
3123
3739
  const delay = Math.min(
@@ -3131,6 +3747,7 @@ var OrbinumClientProvider = class {
3131
3747
  if (this._status !== "idle") this.startConnectAttempt();
3132
3748
  }, delay);
3133
3749
  }
3750
+ /** Clears any pending reconnect timer without triggering a new attempt. */
3134
3751
  cancelReconnect() {
3135
3752
  if (this._reconnectTimer) {
3136
3753
  clearTimeout(this._reconnectTimer);
@@ -3138,6 +3755,7 @@ var OrbinumClientProvider = class {
3138
3755
  }
3139
3756
  }
3140
3757
  // ─── Client teardown ────────────────────────────────────────────────────
3758
+ /** Stops the heartbeat, destroys the active client, and clears all in-progress promises. */
3141
3759
  teardownClient() {
3142
3760
  this.stopHeartbeat();
3143
3761
  try {
@@ -3148,11 +3766,19 @@ var OrbinumClientProvider = class {
3148
3766
  this._connectingPromise = null;
3149
3767
  }
3150
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
+ */
3151
3773
  async getOrbinumClient() {
3152
3774
  if (this._orbinumClient) return this._orbinumClient;
3153
3775
  if (this._connectingPromise) return this._connectingPromise;
3154
3776
  throw new Error(`OrbinumClientProvider: cannot get client in status '${this._status}'`);
3155
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
+ */
3156
3782
  async tryGetOrbinumClient() {
3157
3783
  try {
3158
3784
  return await this.getOrbinumClient();
@@ -3161,15 +3787,28 @@ var OrbinumClientProvider = class {
3161
3787
  }
3162
3788
  }
3163
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
+ */
3164
3794
  async rpcSend(method, params = []) {
3165
3795
  const client = await this.getOrbinumClient();
3166
3796
  return client.substrate.request(method, params);
3167
3797
  }
3798
+ /**
3799
+ * Sends a single EVM JSON-RPC request and returns the typed result.
3800
+ * Throws if `evmRpc` was not configured.
3801
+ */
3168
3802
  async evmRpc(method, params = []) {
3169
3803
  const client = await this.getOrbinumClient();
3170
3804
  if (!client.evm) throw new Error("EVM RPC not configured");
3171
3805
  return client.evm.request(method, params);
3172
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
+ */
3173
3812
  async evmRpcBatch(calls) {
3174
3813
  const client = await this.getOrbinumClient();
3175
3814
  if (!client.evm) throw new Error("EVM RPC not configured");
@@ -3177,102 +3816,469 @@ var OrbinumClientProvider = class {
3177
3816
  }
3178
3817
  };
3179
3818
 
3180
- // 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
3181
4049
  var import_poseidon_lite2 = require("poseidon-lite");
3182
- function tryDecryptNote(commitment, viewingKey, spendingKey) {
3183
- 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" };
3184
4058
  let commitmentBytes;
3185
4059
  let memoBytes;
3186
4060
  try {
3187
4061
  commitmentBytes = fromHex(commitment.commitmentHex);
3188
4062
  memoBytes = fromHex(commitment.encryptedMemo);
3189
4063
  } catch {
3190
- 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);
3191
4087
  }
3192
- const plaintext = EncryptedMemo.decrypt(memoBytes, commitmentBytes, viewingKey);
3193
- if (!plaintext) return null;
3194
4088
  const recomputed = (0, import_poseidon_lite2.poseidon4)([
3195
4089
  plaintext.value,
3196
4090
  plaintext.assetId,
3197
- plaintext.ownerPk,
4091
+ effectiveOwnerPk,
3198
4092
  plaintext.blinding
3199
4093
  ]);
3200
- if (recomputed !== bytesToBigintLE(commitmentBytes)) return null;
3201
- 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]);
3202
4098
  return {
3203
- value: plaintext.value,
3204
- assetId: plaintext.assetId,
3205
- ownerPk: plaintext.ownerPk,
3206
- blinding: plaintext.blinding,
3207
- spendingKey,
3208
- spent: false,
3209
- spentAt: null,
3210
- commitment: recomputed,
3211
- nullifier,
3212
- commitmentHex: toHex(bigintTo32Le(recomputed)),
3213
- nullifierHex: toHex(bigintTo32Le(nullifier)),
3214
- 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
+ }
3215
4114
  };
3216
4115
  }
3217
4116
 
3218
- // src/shielded-pool/PrivacyKeys.ts
3219
- var import_hkdf = require("@noble/hashes/hkdf.js");
3220
- var import_sha22 = require("@noble/hashes/sha2.js");
3221
- 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
+ }
3222
4152
 
3223
- // src/shielded-pool/constants.ts
3224
- 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
+ }
3225
4203
 
3226
- // 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");
3227
4208
  var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
3228
4209
  function deriveSpendingKeyMessage(chainId, address) {
3229
4210
  return `orbinum-spending-key-v1
3230
4211
  ${chainId}
3231
4212
  ${address.toLowerCase()}`;
3232
4213
  }
3233
- async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
3234
- const hex = signatureHex.startsWith("0x") ? signatureHex.slice(2) : signatureHex;
3235
- 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);
3236
4216
  const info = new TextEncoder().encode(`orbinum-sk-v1:${chainId}:${address.toLowerCase()}`);
3237
- const skBytes = (0, import_hkdf.hkdf)(import_sha22.sha256, sigBytes, new Uint8Array(0), info, 32);
3238
- const skBigint = BigInt(
3239
- "0x" + Array.from(skBytes).map((b) => b.toString(16).padStart(2, "0")).join("")
3240
- ) % 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;
3241
4222
  return skBigint === 0n ? 1n : skBigint;
3242
4223
  }
3243
- function deriveViewingKey(spendingKey) {
4224
+ function deriveViewingSecretKey(spendingKey) {
3244
4225
  const ikm = bigintTo32Le(spendingKey);
3245
- 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);
3246
4233
  }
3247
4234
  function deriveOwnerPk(spendingKey) {
3248
4235
  try {
3249
- 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);
3250
4237
  return pubPoint[0];
3251
4238
  } catch {
3252
4239
  return 0n;
3253
4240
  }
3254
4241
  }
3255
4242
 
3256
- // src/shielded-pool/PrivacyKeyManager.ts
4243
+ // src/privacy-keys/PrivacyKeyManager.ts
3257
4244
  var PrivacyKeyManager = class {
3258
4245
  _state = {
3259
4246
  spendingKey: null,
3260
- viewingKey: null,
4247
+ masterBytes: null,
4248
+ viewingSecretKey: null,
4249
+ viewingPublicKeyPacked: null,
3261
4250
  ownerPk: null
3262
4251
  };
3263
4252
  /**
3264
- * Load a spending key into the in-memory session.
3265
- * 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.
3266
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")).
3267
4260
  */
3268
- async load(spendingKey) {
3269
- const viewingKey = deriveViewingKey(spendingKey);
4261
+ async load(spendingKey, masterBytes) {
4262
+ const viewingSecretKey = deriveViewingSecretKey(spendingKey);
4263
+ const viewingPublicKeyPacked = deriveViewingPublicKey(viewingSecretKey);
3270
4264
  const ownerPk = deriveOwnerPk(spendingKey);
3271
- this._state = { spendingKey, viewingKey, ownerPk };
4265
+ this._state = {
4266
+ spendingKey,
4267
+ masterBytes,
4268
+ viewingSecretKey,
4269
+ viewingPublicKeyPacked,
4270
+ ownerPk
4271
+ };
3272
4272
  }
3273
4273
  /** Clear all key material from memory. Call on vault lock / sign-out. */
3274
4274
  clear() {
3275
- 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
+ };
3276
4282
  }
3277
4283
  /** Returns true if a spending key has been loaded. */
3278
4284
  isLoaded() {
@@ -3285,12 +4291,29 @@ var PrivacyKeyManager = class {
3285
4291
  }
3286
4292
  return this._state.spendingKey;
3287
4293
  }
3288
- /** Returns the 32-byte viewing key. Throws if not loaded. */
3289
- getViewingKey() {
3290
- 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) {
3291
4314
  throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
3292
4315
  }
3293
- return this._state.viewingKey;
4316
+ return this._state.viewingPublicKeyPacked;
3294
4317
  }
3295
4318
  /** Returns the BabyJubJub owner public key (x-coordinate). Throws if not loaded. */
3296
4319
  getOwnerPk() {
@@ -3303,24 +4326,86 @@ var PrivacyKeyManager = class {
3303
4326
  getSpendingKeyBytes() {
3304
4327
  return bigintTo32Le(this.getSpendingKey());
3305
4328
  }
3306
- /** 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
+ */
3307
4345
  exportHex() {
3308
- 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 };
3309
4385
  }
3310
4386
  /**
3311
- * Load a spending key from a 0x-prefixed or bare hex string.
3312
- * 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.
3313
4389
  */
3314
4390
  async importFromHex(hex) {
3315
- const key = BigInt(hex.startsWith("0x") ? hex : "0x" + hex);
3316
- if (key === 0n || key >= BN254_R) {
3317
- 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
+ );
3318
4395
  }
3319
- 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);
3320
4405
  }
3321
4406
  };
3322
4407
 
3323
- // src/shielded-pool/VaultCrypto.ts
4408
+ // src/vault/VaultJson.ts
3324
4409
  function vaultReplacer(_key, value) {
3325
4410
  if (typeof value === "bigint") return { __bigint: value.toString() };
3326
4411
  return value;
@@ -3331,16 +4416,28 @@ function vaultReviver(_key, value) {
3331
4416
  }
3332
4417
  return value;
3333
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
3334
4435
  var VAULT_KEY_INFO = new TextEncoder().encode("orbinum-vault-key-v1");
3335
4436
  var IV_BYTES = 12;
3336
- async function deriveVaultKey(spendingKeyBytes) {
3337
- const keyMaterial = await crypto.subtle.importKey(
3338
- "raw",
3339
- spendingKeyBytes.slice(0),
3340
- "HKDF",
3341
- false,
3342
- ["deriveKey"]
3343
- );
4437
+ async function deriveVaultKey(masterBytes) {
4438
+ const keyMaterial = await crypto.subtle.importKey("raw", masterBytes.slice(0), "HKDF", false, [
4439
+ "deriveKey"
4440
+ ]);
3344
4441
  return crypto.subtle.deriveKey(
3345
4442
  {
3346
4443
  name: "HKDF",
@@ -3369,6 +4466,167 @@ async function decryptJson(key, iv, ciphertext) {
3369
4466
  return JSON.parse(new TextDecoder().decode(plainBuf), vaultReviver);
3370
4467
  }
3371
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
+
3372
4630
  // src/account-mapping/types/index.ts
3373
4631
  var SignatureScheme = {
3374
4632
  Eip191: "Eip191",
@@ -3385,7 +4643,7 @@ function decodePrecompileCalldata(address, input) {
3385
4643
  if (!fnSig) return null;
3386
4644
  if (fnSig.startsWith("registerAlias")) {
3387
4645
  try {
3388
- const data = hexToBytes(input.slice(10));
4646
+ const data = fromHex(input.slice(10));
3389
4647
  const alias = decodeString(data, 0);
3390
4648
  return { fnSig, args: { alias } };
3391
4649
  } catch {
@@ -3394,37 +4652,43 @@ function decodePrecompileCalldata(address, input) {
3394
4652
  }
3395
4653
  if (fnSig.startsWith("shield(")) {
3396
4654
  try {
3397
- const data = hexToBytes(input.slice(10));
4655
+ const data = fromHex(input.slice(10));
3398
4656
  const assetId = decodeUint(data, 0);
3399
- const amount = decodeUint(data, 32);
3400
- const commitment = toHex(data.slice(64, 96));
3401
- return { fnSig, args: { assetId, amount, commitment } };
4657
+ const commitment = toHex(data.slice(32, 64));
4658
+ return { fnSig, args: { assetId, commitment } };
3402
4659
  } catch {
3403
4660
  return { fnSig, args: {} };
3404
4661
  }
3405
4662
  }
3406
4663
  if (fnSig.startsWith("unshield(")) {
3407
4664
  try {
3408
- const data = hexToBytes(input.slice(10));
4665
+ const data = fromHex(input.slice(10));
3409
4666
  const root = toHex(data.slice(32, 64));
3410
4667
  const nullifier = toHex(data.slice(64, 96));
3411
4668
  const assetId = decodeUint(data, 96);
3412
4669
  const amount = decodeUint(data, 128);
3413
4670
  const recipient = toHex(data.slice(160, 192));
3414
- 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
+ };
3415
4677
  } catch {
3416
4678
  return { fnSig, args: {} };
3417
4679
  }
3418
4680
  }
3419
4681
  if (fnSig.startsWith("privateTransfer(")) {
3420
4682
  try {
3421
- const data = hexToBytes(input.slice(10));
4683
+ const data = fromHex(input.slice(10));
3422
4684
  const root = toHex(data.slice(32, 64));
3423
4685
  const nullOffset = Number(decodeUint(data, 64));
3424
4686
  const commOffset = Number(decodeUint(data, 96));
3425
4687
  const nullifiers = Number(decodeUint(data, nullOffset));
3426
4688
  const commitments = Number(decodeUint(data, commOffset));
3427
- 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 } };
3428
4692
  } catch {
3429
4693
  return { fnSig, args: {} };
3430
4694
  }
@@ -3637,7 +4901,11 @@ function mapExtrinsicArgs(section, method, args) {
3637
4901
  if (m_norm === "requestdisclosure") {
3638
4902
  return {
3639
4903
  target: get(0, "target"),
3640
- 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")
3641
4909
  };
3642
4910
  }
3643
4911
  if (m_norm === "disclose") {
@@ -3651,7 +4919,8 @@ function mapExtrinsicArgs(section, method, args) {
3651
4919
  if (m_norm === "rejectdisclosure") {
3652
4920
  return {
3653
4921
  auditor: get(0, "auditor"),
3654
- reason: get(1, "reason")
4922
+ commitment: get(1, "commitment"),
4923
+ reason: get(2, "reason")
3655
4924
  };
3656
4925
  }
3657
4926
  if (m_norm === "registerasset") {
@@ -3674,7 +4943,8 @@ function mapExtrinsicArgs(section, method, args) {
3674
4943
  if (m_norm === "pruneexpiredrequest") {
3675
4944
  return {
3676
4945
  target: get(0, "target"),
3677
- auditor: get(1, "auditor")
4946
+ auditor: get(1, "auditor"),
4947
+ commitment: get(2, "commitment")
3678
4948
  };
3679
4949
  }
3680
4950
  if (m_norm === "revokedisclosurerecord") {
@@ -3911,29 +5181,35 @@ function mapZkEventData(method, data) {
3911
5181
  }
3912
5182
  if (m_norm === "disclosed") {
3913
5183
  return {
3914
- who: get(0, "who"),
3915
- commitment: get(1, "commitment"),
3916
- auditor: get(2, "auditor")
5184
+ target: get(0, "target"),
5185
+ auditor: get(1, "auditor"),
5186
+ commitment: get(2, "commitment"),
5187
+ signals: get(3, "signals")
3917
5188
  };
3918
5189
  }
3919
5190
  if (m_norm === "disclosurerequested") {
3920
5191
  return {
3921
5192
  target: get(0, "target"),
3922
5193
  auditor: get(1, "auditor"),
3923
- 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")
3924
5198
  };
3925
5199
  }
3926
5200
  if (m_norm === "disclosurerejected") {
3927
5201
  return {
3928
5202
  target: get(0, "target"),
3929
5203
  auditor: get(1, "auditor"),
3930
- reason: get(2, "reason")
5204
+ commitment: get(2, "commitment"),
5205
+ reason: get(3, "reason")
3931
5206
  };
3932
5207
  }
3933
5208
  if (m_norm === "disclosurerequestexpired") {
3934
5209
  return {
3935
5210
  target: get(0, "target"),
3936
- auditor: get(1, "auditor")
5211
+ auditor: get(1, "auditor"),
5212
+ commitment: get(2, "commitment")
3937
5213
  };
3938
5214
  }
3939
5215
  if (m_norm === "disclosurerecordrevoked") {
@@ -4185,9 +5461,13 @@ var import_polkadot_api4 = require("polkadot-api");
4185
5461
  AccountId,
4186
5462
  AccountMappingModule,
4187
5463
  AccountMappingPrecompile,
5464
+ BABYJUB_SUBORDER,
5465
+ BN254_R,
4188
5466
  Blake2256,
4189
5467
  CircuitId,
5468
+ CircuitType,
4190
5469
  CryptoPrecompiles,
5470
+ ENCRYPTED_MEMO_SIZE,
4191
5471
  EncryptedMemo,
4192
5472
  EvmClient,
4193
5473
  EvmExplorer,
@@ -4200,30 +5480,45 @@ var import_polkadot_api4 = require("polkadot-api");
4200
5480
  PRECOMPILE_ADDR,
4201
5481
  PrivacyKeyManager,
4202
5482
  PrivacyModule,
5483
+ RelayerStatusModule,
4203
5484
  SLIP0044_NAMESPACE,
4204
5485
  ShieldedPoolModule,
4205
5486
  ShieldedPoolPrecompile,
4206
5487
  SignatureScheme,
4207
5488
  Storage,
4208
5489
  SubstrateClient,
5490
+ VaultLockedError,
5491
+ WebArtifactProvider,
4209
5492
  ZkVerifierModule,
4210
5493
  accountIdHexToSs58,
4211
5494
  addressToAccountIdHex,
5495
+ applyNoteStatus,
4212
5496
  base58,
4213
5497
  bigintTo32Be,
4214
5498
  bigintTo32Le,
4215
5499
  bigintTo32LeArr,
5500
+ buildDisclosurePublicSignals,
5501
+ buildDummyTransferInput,
4216
5502
  bytesToBigintLE,
5503
+ computeNullifier,
4217
5504
  computePathIndices,
4218
5505
  connectInjectedExtension,
4219
5506
  decodePrecompileCalldata,
5507
+ decryptDisclosureSignals,
4220
5508
  decryptJson,
5509
+ decryptNoteRecord,
5510
+ deriveBabyJubjubKeypair,
5511
+ deriveMasterKeyBytes,
4221
5512
  deriveOwnerPk,
4222
5513
  deriveSpendingKeyFromSignature,
4223
5514
  deriveSpendingKeyMessage,
5515
+ deriveStealthOwnerPk,
5516
+ deriveStealthSk,
4224
5517
  deriveVaultKey,
4225
- deriveViewingKey,
5518
+ deriveViewingPublicKey,
5519
+ deriveViewingSecretKey,
4226
5520
  encryptJson,
5521
+ encryptNote,
4227
5522
  ensureHexPrefix,
4228
5523
  evmAddressToAccountId,
4229
5524
  evmToImplicitSubstrate,
@@ -4233,6 +5528,10 @@ var import_polkadot_api4 = require("polkadot-api");
4233
5528
  formatORB,
4234
5529
  fromBase64,
4235
5530
  fromHex,
5531
+ generateDisclosureProof,
5532
+ generateFeeClaimProof,
5533
+ generateTransferProof,
5534
+ generateUnshieldProof,
4236
5535
  getInjectedExtensions,
4237
5536
  getPolkadotSigner,
4238
5537
  getPolkadotSignerFromPjs,
@@ -4250,6 +5549,9 @@ var import_polkadot_api4 = require("polkadot-api");
4250
5549
  mapExtrinsicArgs,
4251
5550
  mapZkEventData,
4252
5551
  normalizeEvmAddress,
5552
+ randomBlinding,
5553
+ recoverOwnerPkPoint,
5554
+ selectNotes,
4253
5555
  shortHash,
4254
5556
  substrateSs58ToAccountIdHex,
4255
5557
  substrateToEvm,
@@ -4258,6 +5560,7 @@ var import_polkadot_api4 = require("polkadot-api");
4258
5560
  toTxResult,
4259
5561
  truncateMiddle,
4260
5562
  tryDecryptNote,
5563
+ tryDecryptNoteVerbose,
4261
5564
  u128,
4262
5565
  u64,
4263
5566
  vaultReplacer,