@orbinum/sdk 0.10.0 → 0.12.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
@@ -26,8 +26,10 @@ __export(index_exports, {
26
26
  BABYJUB_SUBORDER: () => BABYJUB_SUBORDER,
27
27
  BN254_R: () => BN254_R,
28
28
  Blake2256: () => import_substrate_bindings3.Blake2256,
29
+ CURRENT_CIRCUIT_VERSION: () => CURRENT_CIRCUIT_VERSION,
29
30
  CircuitId: () => CircuitId,
30
- CircuitType: () => import_proof_generator.CircuitType,
31
+ CircuitType: () => import_proof_generator2.CircuitType,
32
+ CircuitVersionResolver: () => CircuitVersionResolver,
31
33
  CryptoPrecompiles: () => CryptoPrecompiles,
32
34
  ENCRYPTED_MEMO_SIZE: () => ENCRYPTED_MEMO_SIZE,
33
35
  EncryptedMemo: () => EncryptedMemo,
@@ -50,7 +52,7 @@ __export(index_exports, {
50
52
  Storage: () => import_substrate_bindings3.Storage,
51
53
  SubstrateClient: () => SubstrateClient,
52
54
  VaultLockedError: () => VaultLockedError,
53
- WebArtifactProvider: () => import_proof_generator.WebArtifactProvider,
55
+ WebArtifactProvider: () => import_proof_generator2.WebArtifactProvider,
54
56
  ZkVerifierModule: () => ZkVerifierModule,
55
57
  accountIdHexToSs58: () => accountIdHexToSs58,
56
58
  addressToAccountIdHex: () => addressToAccountIdHex,
@@ -1150,11 +1152,27 @@ var EvmExplorer = class _EvmExplorer {
1150
1152
  };
1151
1153
 
1152
1154
  // src/indexer/IndexerClient.ts
1155
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
1156
+ function normalizeBaseUrl(baseUrl) {
1157
+ let url;
1158
+ try {
1159
+ url = new URL(baseUrl);
1160
+ } catch {
1161
+ throw new Error(`IndexerClient: invalid baseUrl ${JSON.stringify(baseUrl)}`);
1162
+ }
1163
+ const isLocal = LOCAL_HOSTS.has(url.hostname);
1164
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocal)) {
1165
+ throw new Error(
1166
+ `IndexerClient: baseUrl must use https:// (got ${url.protocol}//${url.hostname}). Plain http is only allowed for localhost.`
1167
+ );
1168
+ }
1169
+ return baseUrl.replace(/\/$/, "");
1170
+ }
1153
1171
  var IndexerClient = class _IndexerClient {
1154
1172
  baseUrl;
1155
1173
  timeoutMs;
1156
1174
  constructor(config) {
1157
- this.baseUrl = config.baseUrl.replace(/\/$/, "");
1175
+ this.baseUrl = normalizeBaseUrl(config.baseUrl);
1158
1176
  this.timeoutMs = config.timeoutMs ?? 1e4;
1159
1177
  }
1160
1178
  // ─── Internal helpers ──────────────────────────────────────────────────────
@@ -1212,7 +1230,7 @@ var IndexerClient = class _IndexerClient {
1212
1230
  /**
1213
1231
  * Returns a paginated list of stealth scan hints ordered ascending by leafIndex.
1214
1232
  * Each hint contains only the fields required for ECDH triage and decryption:
1215
- * leafIndex, commitmentHex, assetId, ephPkHex, encryptedMemo.
1233
+ * leafIndex, commitmentHex, assetId, ephPkHex, encryptedMemo, circuitVersion.
1216
1234
  *
1217
1235
  * Use `sinceLeafIndex` for incremental scans (cursor = last seen leafIndex + 1).
1218
1236
  */
@@ -1632,8 +1650,8 @@ var BABYJUB_SUBORDER = 273603035897990940278080071815715938607681397215856725920
1632
1650
  // src/shielded-pool/protocol/memo.ts
1633
1651
  var import_sha2 = require("@noble/hashes/sha2.js");
1634
1652
  var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
1635
- var MEMO_PLAINTEXT_SIZE = 116;
1636
- function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk) {
1653
+ var MEMO_PLAINTEXT_SIZE = 120;
1654
+ function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion) {
1637
1655
  const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
1638
1656
  const view = new DataView(buf.buffer);
1639
1657
  view.setBigUint64(0, value & 0xffffffffffffffffn, true);
@@ -1642,6 +1660,7 @@ function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk) {
1642
1660
  buf.set(blinding.slice(0, 32), 48);
1643
1661
  view.setUint32(80, assetId >>> 0, true);
1644
1662
  buf.set(counterpartyPk.slice(0, 32), 84);
1663
+ view.setUint32(116, circuitVersion >>> 0, true);
1645
1664
  return buf;
1646
1665
  }
1647
1666
  function deriveEncryptionKey(sharedSecret, commitment) {
@@ -1654,7 +1673,7 @@ function deriveEncryptionKey(sharedSecret, commitment) {
1654
1673
 
1655
1674
  // src/shielded-pool/protocol/EncryptedMemo.ts
1656
1675
  var NONCE_SIZE = 12;
1657
- var CIPHERTEXT_SIZE = 132;
1676
+ var CIPHERTEXT_SIZE = 136;
1658
1677
  var EPH_PK_SIZE = 32;
1659
1678
  var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
1660
1679
  function bytesToBjjScalar(bytes) {
@@ -1673,14 +1692,15 @@ function parsePlaintext(nonce, ciphertextWithMac, encKey) {
1673
1692
  const blinding = bytesToBigintLE(plaintext.slice(48, 80));
1674
1693
  const assetId = BigInt(view.getUint32(80, true));
1675
1694
  const counterpartyPk = bytesToBigintLE(plaintext.slice(84, 116));
1676
- return { value, ownerPk, blinding, assetId, counterpartyPk };
1695
+ const circuitVersion = view.getUint32(116, true);
1696
+ return { value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion };
1677
1697
  } catch {
1678
1698
  return null;
1679
1699
  }
1680
1700
  }
1681
1701
  var EncryptedMemo = {
1682
1702
  /**
1683
- * Build and encrypt a memo for a note using ECDH (v2, 168 bytes).
1703
+ * Build and encrypt a memo for a note using ECDH (v2, 180 bytes).
1684
1704
  *
1685
1705
  * @param value Note value in planck.
1686
1706
  * @param ownerPk 32-byte owner public key (LE).
@@ -1692,11 +1712,20 @@ var EncryptedMemo = {
1692
1712
  * decoded from a privacy address).
1693
1713
  * Pass `new Uint8Array(32)` (all zeros) for a publicly-readable memo.
1694
1714
  * @param counterpartyPk 32-byte counterparty BJJ Ax. Default: all zeros.
1695
- * @returns 168-byte encrypted memo: nonce(12) || ciphertext+MAC(124) || ephPk(32).
1715
+ * @param circuitVersion ZK circuit version the note is spent under. Default: 0.
1716
+ * @param ephSkOverride 32-byte ephemeral secret key (stealth coordination). Optional.
1717
+ * @returns 180-byte encrypted memo: nonce(12) || ciphertext+MAC(136) || ephPk(32).
1696
1718
  */
1697
- encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, counterpartyPk = new Uint8Array(32), ephSkOverride) {
1719
+ encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, counterpartyPk = new Uint8Array(32), circuitVersion = 0, ephSkOverride) {
1698
1720
  const nonce = (0, import_utils.randomBytes)(NONCE_SIZE);
1699
- const plaintext = serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk);
1721
+ const plaintext = serializeMemo(
1722
+ value,
1723
+ ownerPk,
1724
+ blinding,
1725
+ assetId,
1726
+ counterpartyPk,
1727
+ circuitVersion
1728
+ );
1700
1729
  const isZeroKey = recipientIvkPacked.every((b) => b === 0);
1701
1730
  let sharedSecret;
1702
1731
  let ephPkPackedBytes;
@@ -1727,29 +1756,31 @@ var EncryptedMemo = {
1727
1756
  return result;
1728
1757
  },
1729
1758
  /**
1730
- * Returns a 168-byte public memo encrypted with a zero viewing key.
1759
+ * Returns a 180-byte public memo encrypted with a zero viewing key.
1731
1760
  * Decryptable by anyone with `decrypt(memo, commitment, new Uint8Array(32))`.
1732
1761
  * Convenience alias for `encrypt(..., new Uint8Array(32))`.
1733
1762
  */
1734
- encryptPublic(value, ownerPk, blinding, assetId, commitment) {
1763
+ encryptPublic(value, ownerPk, blinding, assetId, commitment, circuitVersion = 0) {
1735
1764
  return EncryptedMemo.encrypt(
1736
1765
  value,
1737
1766
  ownerPk,
1738
1767
  blinding,
1739
1768
  assetId,
1740
1769
  commitment,
1741
- new Uint8Array(32)
1770
+ new Uint8Array(32),
1771
+ new Uint8Array(32),
1772
+ circuitVersion
1742
1773
  );
1743
1774
  },
1744
1775
  /**
1745
- * Returns a 168-byte zeroed dummy memo (no information, always valid on-chain).
1776
+ * Returns a 180-byte zeroed dummy memo (no information, always valid on-chain).
1746
1777
  */
1747
1778
  dummy() {
1748
1779
  return new Uint8Array(ENCRYPTED_MEMO_SIZE);
1749
1780
  },
1750
1781
  /**
1751
1782
  * Validates that `bytes` is a properly-sized encrypted memo.
1752
- * Throws an Error if the length is not ENCRYPTED_MEMO_SIZE (168 bytes).
1783
+ * Throws an Error if the length is not ENCRYPTED_MEMO_SIZE (180 bytes).
1753
1784
  *
1754
1785
  * Call this at system boundaries (extrinsic builders, precompile encoders)
1755
1786
  * to catch malformed memos before they reach the chain and fail on-chain.
@@ -1770,7 +1801,7 @@ var EncryptedMemo = {
1770
1801
  * Returns null if decryption fails — wrong key, bad MAC, or malformed memo.
1771
1802
  * Never throws; safe for scan loops.
1772
1803
  *
1773
- * @param memoBytes 168-byte encrypted memo.
1804
+ * @param memoBytes 180-byte encrypted memo.
1774
1805
  * @param commitment 32-byte note commitment (LE).
1775
1806
  * @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
1776
1807
  */
@@ -1782,13 +1813,13 @@ var EncryptedMemo = {
1782
1813
  * Extract the ECDH shared secret from an encrypted memo using the recipient's viewing secret key.
1783
1814
  *
1784
1815
  * Used by NoteDecryptor to obtain the shared secret needed for stealth address derivation
1785
- * without re-running the full decrypt path. Safe to call on any 168-byte memo.
1816
+ * without re-running the full decrypt path. Safe to call on any 180-byte memo.
1786
1817
  *
1787
1818
  * Returns `new Uint8Array(32)` (all zeros) for public/dummy memos (zero ephPk).
1788
1819
  * Returns `null` if the memo is malformed or the ephPk is not a valid BJJ point.
1789
1820
  * Never throws; safe for scan loops.
1790
1821
  *
1791
- * @param memoBytes 168-byte encrypted memo.
1822
+ * @param memoBytes 180-byte encrypted memo.
1792
1823
  * @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
1793
1824
  */
1794
1825
  extractSharedSecret(memoBytes, viewingSecretKey) {
@@ -1992,7 +2023,7 @@ var ShieldedPoolModule = class {
1992
2023
  * Withdraws tokens from the shielded pool to a public address.
1993
2024
  * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
1994
2025
  * Pass a `signer` to fall back to signed submission (e.g. for testing).
1995
- * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee)
2026
+ * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee, changeCommitment, changeEncryptedMemo, relayer, circuitVersion)
1996
2027
  */
1997
2028
  async unshield(params, signer, txOptions) {
1998
2029
  const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "unshield");
@@ -2016,8 +2047,9 @@ var ShieldedPoolModule = class {
2016
2047
  fee: params.fee ?? 0n,
2017
2048
  change_commitment: changeCommitment,
2018
2049
  change_encrypted_memo: changeEncryptedMemo,
2019
- relayer: void 0
2050
+ relayer: void 0,
2020
2051
  // Option<H160> — None for direct Substrate submissions
2052
+ circuit_version: params.circuitVersion
2021
2053
  });
2022
2054
  if (signer) {
2023
2055
  return toTxResult(
@@ -2030,7 +2062,7 @@ var ShieldedPoolModule = class {
2030
2062
  * Performs a private (shielded) transfer between two notes.
2031
2063
  * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
2032
2064
  * Pass a `signer` to fall back to signed submission (e.g. for testing).
2033
- * Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee)
2065
+ * Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee, relayer, circuitVersion)
2034
2066
  */
2035
2067
  async privateTransfer(params, signer, txOptions) {
2036
2068
  const nullifiers = params.inputs.map((inp) => inp.nullifier);
@@ -2051,8 +2083,9 @@ var ShieldedPoolModule = class {
2051
2083
  encrypted_memos: memos,
2052
2084
  asset_id: params.assetId,
2053
2085
  fee: params.fee ?? 0n,
2054
- relayer: void 0
2086
+ relayer: void 0,
2055
2087
  // Option<H160> — None for direct Substrate submissions
2088
+ circuit_version: params.circuitVersion
2056
2089
  });
2057
2090
  if (signer) {
2058
2091
  return toTxResult(
@@ -2086,7 +2119,7 @@ var ShieldedPoolModule = class {
2086
2119
  * This is a SIGNED transaction — the relayer must sign it with their wallet.
2087
2120
  * Before calling this, generate a ZK value proof with generateFeeClaimProof() (not yet implemented).
2088
2121
  *
2089
- * Extrinsic: shieldedPool.claim_shielded_fees(commitment, amount, asset_id, memo, proof, public_signals)
2122
+ * Extrinsic: shieldedPool.claim_shielded_fees(commitment, amount, asset_id, memo, proof, public_signals, circuit_version)
2090
2123
  */
2091
2124
  async claimShieldedFees(params, signer, txOptions) {
2092
2125
  EncryptedMemo.validate(params.encryptedMemo, "claimShieldedFees.encryptedMemo");
@@ -2097,7 +2130,8 @@ var ShieldedPoolModule = class {
2097
2130
  asset_id: params.assetId,
2098
2131
  encrypted_memo: params.encryptedMemo,
2099
2132
  proof: params.proof,
2100
- public_signals: params.publicSignals
2133
+ public_signals: params.publicSignals,
2134
+ circuit_version: params.circuitVersion
2101
2135
  });
2102
2136
  return toTxResult(
2103
2137
  await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
@@ -2615,6 +2649,74 @@ var ZkVerifierModule = class {
2615
2649
  }
2616
2650
  };
2617
2651
 
2652
+ // src/shielded-pool/CircuitVersionResolver.ts
2653
+ var import_proof_generator = require("@orbinum/proof-generator");
2654
+ var CircuitVersionResolver = class {
2655
+ constructor(zkVerifier, baseUrl, providerFactory) {
2656
+ this.zkVerifier = zkVerifier;
2657
+ this.makeProvider = providerFactory ?? ((circuit, noteVersion) => new import_proof_generator.WebArtifactProvider({
2658
+ ...baseUrl ? { baseUrl } : {},
2659
+ circuitVersions: { [circuit]: noteVersion }
2660
+ }));
2661
+ }
2662
+ zkVerifier;
2663
+ makeProvider;
2664
+ /**
2665
+ * Resolves the prover + on-chain version for spending a note of `circuit`
2666
+ * created under `noteVersion`. Fail-closed: throws on unsupported version or
2667
+ * VK-hash mismatch (CDN vs chain), before generating any proof.
2668
+ */
2669
+ async resolve(circuit, noteVersion) {
2670
+ if (!Number.isInteger(noteVersion) || noteVersion <= 0) {
2671
+ throw new Error(
2672
+ `CircuitVersionResolver: invalid note circuitVersion ${noteVersion} for ${circuit}`
2673
+ );
2674
+ }
2675
+ const provider = this.makeProvider(circuit, noteVersion);
2676
+ const resolved = await provider.getResolvedVersion(circuit);
2677
+ if (resolved.version !== noteVersion) {
2678
+ throw new Error(
2679
+ `CircuitVersionResolver: prover resolved ${circuit} to v${resolved.version}, expected v${noteVersion}`
2680
+ );
2681
+ }
2682
+ const circuitId = (0, import_proof_generator.circuitTypeToId)(circuit);
2683
+ const info = await this.zkVerifier.getCircuitVersionInfo(circuitId);
2684
+ if (!info) {
2685
+ throw new Error(
2686
+ `CircuitVersionResolver: chain reports no version info for ${circuit} (id ${circuitId})`
2687
+ );
2688
+ }
2689
+ if (!info.supportedVersions.includes(noteVersion)) {
2690
+ throw new Error(
2691
+ `CircuitVersionResolver: chain does not support ${circuit} v${noteVersion} (supported: ${info.supportedVersions.join(", ")})`
2692
+ );
2693
+ }
2694
+ const chainVk = info.vkHashes.find((h) => h.version === noteVersion);
2695
+ if (!chainVk) {
2696
+ throw new Error(
2697
+ `CircuitVersionResolver: chain has no VK hash for ${circuit} v${noteVersion}`
2698
+ );
2699
+ }
2700
+ if (!vkHashEquals(chainVk.vkHash, resolved.vkHash)) {
2701
+ throw new Error(
2702
+ `CircuitVersionResolver: VK hash mismatch for ${circuit} v${noteVersion} \u2014 prover ${resolved.vkHash}, chain ${chainVk.vkHash}`
2703
+ );
2704
+ }
2705
+ return { provider, version: noteVersion };
2706
+ }
2707
+ };
2708
+ function vkHashEquals(a, b) {
2709
+ const na = normalizeHash(a);
2710
+ const nb = normalizeHash(b);
2711
+ if (na === null || nb === null) return false;
2712
+ return na === nb;
2713
+ }
2714
+ function normalizeHash(h) {
2715
+ if (typeof h !== "string") return null;
2716
+ const s = h.toLowerCase().startsWith("0x") ? h.toLowerCase().slice(2) : h.toLowerCase();
2717
+ return /^[0-9a-f]{64}$/.test(s) ? s : null;
2718
+ }
2719
+
2618
2720
  // src/relayer/RelayerStatusModule.ts
2619
2721
  var RelayerStatusModule = class {
2620
2722
  constructor(substrate) {
@@ -2860,12 +2962,12 @@ var AM_SEL = {
2860
2962
  var SP_SEL = {
2861
2963
  // shield(uint32,bytes32,bytes) → 0x9feb22ea (payable, amount = msg.value)
2862
2964
  SHIELD: new Uint8Array([159, 235, 34, 234]),
2863
- // privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256) 0x8c0f5d24
2864
- PRIVATE_TRANSFER: new Uint8Array([140, 15, 93, 36]),
2865
- // unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32) 0xd21d9a79
2866
- UNSHIELD: new Uint8Array([210, 29, 154, 121]),
2867
- // claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes) 0x42e1e74c
2868
- CLAIM_SHIELDED_FEES: new Uint8Array([66, 225, 231, 76])
2965
+ // privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256,uint32) 0x66ed2cd4
2966
+ PRIVATE_TRANSFER: new Uint8Array([102, 237, 44, 212]),
2967
+ // unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes,uint32) 0x4e505348
2968
+ UNSHIELD: new Uint8Array([78, 80, 83, 72]),
2969
+ // claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes,uint32) 0x88d9deba
2970
+ CLAIM_SHIELDED_FEES: new Uint8Array([136, 217, 222, 186])
2869
2971
  };
2870
2972
  var KNOWN_PRECOMPILES = {
2871
2973
  // ── Ethereum standard (EIP) ─────────────────────────────────────────────
@@ -2909,9 +3011,9 @@ var KNOWN_PRECOMPILES = {
2909
3011
  name: "ShieldedPool",
2910
3012
  functions: {
2911
3013
  "9feb22ea": "shield(uint32,bytes32,bytes)",
2912
- "8c0f5d24": "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256)",
2913
- d21d9a79: "unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)",
2914
- "42e1e74c": "claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes)"
3014
+ "66ed2cd4": "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256,uint32)",
3015
+ "4e505348": "unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes,uint32)",
3016
+ "88d9deba": "claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes,uint32)"
2915
3017
  }
2916
3018
  }
2917
3019
  };
@@ -2965,7 +3067,8 @@ var ShieldedPoolPrecompile = class {
2965
3067
  // ─── privateTransfer ───────────────────────────────────────────────────────
2966
3068
  /**
2967
3069
  * Returns the ABI-encoded calldata for
2968
- * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[], uint32, uint256)`.
3070
+ * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[], uint32, uint256, uint32)`.
3071
+ * The trailing `uint32` is the circuit version the input notes were created under.
2969
3072
  */
2970
3073
  buildPrivateTransferCalldata(params) {
2971
3074
  const nullifiers = params.inputs.map((i) => fromHex(i.nullifier));
@@ -2986,7 +3089,8 @@ var ShieldedPoolPrecompile = class {
2986
3089
  { type: "bytes32[]", value: commitments },
2987
3090
  { type: "bytes[]", value: memos },
2988
3091
  { type: "uint", value: BigInt(params.assetId) },
2989
- { type: "uint", value: params.fee ?? 0n }
3092
+ { type: "uint", value: params.fee ?? 0n },
3093
+ { type: "uint", value: BigInt(params.circuitVersion) }
2990
3094
  );
2991
3095
  }
2992
3096
  /**
@@ -3027,7 +3131,8 @@ var ShieldedPoolPrecompile = class {
3027
3131
  { type: "bytes32", value: recipientBytes },
3028
3132
  { type: "uint", value: params.fee ?? 0n },
3029
3133
  { type: "bytes32", value: changeCommitment },
3030
- { type: "bytes", value: changeEncryptedMemo }
3134
+ { type: "bytes", value: changeEncryptedMemo },
3135
+ { type: "uint", value: BigInt(params.circuitVersion) }
3031
3136
  );
3032
3137
  }
3033
3138
  /**
@@ -3077,7 +3182,7 @@ var ShieldedPoolPrecompile = class {
3077
3182
  // ─── claimShieldedFees ───────────────────────────────────────────────────────────────────
3078
3183
  /**
3079
3184
  * Returns the ABI-encoded calldata for
3080
- * `claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes)`.
3185
+ * `claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes,uint32)`.
3081
3186
  *
3082
3187
  * ABI layout (params after selector):
3083
3188
  * - `commitment` — bytes32 (fixed)
@@ -3086,6 +3191,7 @@ var ShieldedPoolPrecompile = class {
3086
3191
  * - `memo` — bytes (dynamic)
3087
3192
  * - `proof` — bytes (dynamic, 128 bytes Groth16)
3088
3193
  * - `publicSignals` — bytes (dynamic, 76 bytes)
3194
+ * - `circuitVersion` — uint32 (fixed, right-aligned)
3089
3195
  *
3090
3196
  * The validator identity is derived from `msg.sender` in the precompile —
3091
3197
  * do NOT include it in the calldata.
@@ -3111,7 +3217,8 @@ var ShieldedPoolPrecompile = class {
3111
3217
  { type: "uint", value: BigInt(params.assetId) },
3112
3218
  { type: "bytes", value: params.encryptedMemo },
3113
3219
  { type: "bytes", value: params.proof },
3114
- { type: "bytes", value: params.publicSignals }
3220
+ { type: "bytes", value: params.publicSignals },
3221
+ { type: "uint", value: BigInt(params.circuitVersion) }
3115
3222
  );
3116
3223
  }
3117
3224
  /**
@@ -3549,6 +3656,11 @@ var OrbinumClient = class _OrbinumClient {
3549
3656
  chain;
3550
3657
  /** Typed access to `zkVerifier_*` custom RPC endpoints. */
3551
3658
  zkVerifier;
3659
+ /**
3660
+ * Resolves a note's circuit version to a pinned prover + on-chain version
3661
+ * before spending it (fail-closed: throws on unsupported version / VK mismatch).
3662
+ */
3663
+ circuitVersionResolver;
3552
3664
  /** Typed access to `relayer_*` RPC endpoints (registry lookup and pending fee queries). */
3553
3665
  relayerStatus;
3554
3666
  /**
@@ -3557,7 +3669,7 @@ var OrbinumClient = class _OrbinumClient {
3557
3669
  */
3558
3670
  precompiles;
3559
3671
  /** @internal Use `OrbinumClient.connect()` to obtain an instance. */
3560
- constructor(substrate, evm, indexer) {
3672
+ constructor(substrate, evm, indexer, circuitsBaseUrl) {
3561
3673
  this.substrate = substrate;
3562
3674
  this.evm = evm;
3563
3675
  this.evmExplorer = evm ? new EvmExplorer(evm) : null;
@@ -3567,6 +3679,7 @@ var OrbinumClient = class _OrbinumClient {
3567
3679
  this.privacy = new PrivacyModule(substrate);
3568
3680
  this.chain = new ChainModule(substrate);
3569
3681
  this.zkVerifier = new ZkVerifierModule(substrate);
3682
+ this.circuitVersionResolver = new CircuitVersionResolver(this.zkVerifier, circuitsBaseUrl);
3570
3683
  this.relayerStatus = new RelayerStatusModule(substrate);
3571
3684
  this.precompiles = evm ? {
3572
3685
  shieldedPool: new ShieldedPoolPrecompile(evm),
@@ -3587,7 +3700,7 @@ var OrbinumClient = class _OrbinumClient {
3587
3700
  );
3588
3701
  const evm = config.evmRpc ? new EvmClient(config.evmRpc) : null;
3589
3702
  const indexer = config.indexerUrl ? new IndexerClient({ baseUrl: config.indexerUrl }) : null;
3590
- return new _OrbinumClient(substrate, evm, indexer);
3703
+ return new _OrbinumClient(substrate, evm, indexer, config.circuitsBaseUrl);
3591
3704
  }
3592
3705
  /** Closes the underlying Substrate WebSocket connection and releases all resources. */
3593
3706
  destroy() {
@@ -3709,6 +3822,8 @@ var OrbinumClientProvider = class {
3709
3822
  };
3710
3823
  if (this.config.evmRpc) connectConfig.evmRpc = this.config.evmRpc;
3711
3824
  if (this.config.indexerUrl) connectConfig.indexerUrl = this.config.indexerUrl;
3825
+ if (this.config.circuitsBaseUrl)
3826
+ connectConfig.circuitsBaseUrl = this.config.circuitsBaseUrl;
3712
3827
  const clientPromise = OrbinumClient.connect(connectConfig);
3713
3828
  const client = await Promise.race([clientPromise, timeoutPromise]);
3714
3829
  orphanClient = client;
@@ -3895,6 +4010,9 @@ var OrbinumClientProvider = class {
3895
4010
  }
3896
4011
  };
3897
4012
 
4013
+ // src/shielded-pool/protocol/types.ts
4014
+ var CURRENT_CIRCUIT_VERSION = 1;
4015
+
3898
4016
  // src/utils/stealth.ts
3899
4017
  var import_sha22 = require("@noble/hashes/sha2.js");
3900
4018
  var import_hkdf = require("@noble/hashes/hkdf.js");
@@ -4003,6 +4121,7 @@ var NoteBuilder = class {
4003
4121
  const blinding = input.blinding ?? BigInt(Date.now());
4004
4122
  const spendingKey = input.spendingKey ?? 0n;
4005
4123
  const counterpartyPk = input.counterpartyPk ?? 0n;
4124
+ const circuitVersion = input.circuitVersion ?? CURRENT_CIRCUIT_VERSION;
4006
4125
  const useStealth = input.viewingPublicKey !== void 0 && input.recipientOwnerPk !== void 0;
4007
4126
  let memo;
4008
4127
  if (useStealth) {
@@ -4037,6 +4156,7 @@ var NoteBuilder = class {
4037
4156
  stealthCommitmentBytes,
4038
4157
  recipientIvkPacked,
4039
4158
  bigintTo32Le(counterpartyPk),
4159
+ circuitVersion,
4040
4160
  ephSk
4041
4161
  )
4042
4162
  );
@@ -4054,6 +4174,7 @@ var NoteBuilder = class {
4054
4174
  ownerPk: effectiveOwnerPk,
4055
4175
  blinding,
4056
4176
  spendingKey,
4177
+ circuitVersion,
4057
4178
  spent: false,
4058
4179
  spentAt: null,
4059
4180
  commitment: commitment2,
@@ -4076,7 +4197,8 @@ var NoteBuilder = class {
4076
4197
  Number(assetId),
4077
4198
  commitmentBytes,
4078
4199
  input.viewingPublicKey,
4079
- bigintTo32Le(counterpartyPk)
4200
+ bigintTo32Le(counterpartyPk),
4201
+ circuitVersion
4080
4202
  )
4081
4203
  ) : Array.from(EncryptedMemo.dummy());
4082
4204
  if (memo.length !== ENCRYPTED_MEMO_SIZE)
@@ -4089,6 +4211,7 @@ var NoteBuilder = class {
4089
4211
  ownerPk,
4090
4212
  blinding,
4091
4213
  spendingKey,
4214
+ circuitVersion,
4092
4215
  spent: false,
4093
4216
  spentAt: null,
4094
4217
  commitment,
@@ -4100,7 +4223,7 @@ var NoteBuilder = class {
4100
4223
  };
4101
4224
  }
4102
4225
  /**
4103
- * Build the 168-byte ECDH-encrypted memo for a note.
4226
+ * Build the 180-byte ECDH-encrypted memo for a note.
4104
4227
  *
4105
4228
  * Pure TypeScript implementation — no WASM dependency.
4106
4229
  * Uses ChaCha20-Poly1305 with ECDH key agreement (BabyJubJub ephemeral keypair).
@@ -4119,7 +4242,8 @@ var NoteBuilder = class {
4119
4242
  Number(note.assetId),
4120
4243
  bigintTo32Le(note.commitment),
4121
4244
  recipientIvkPacked ?? new Uint8Array(32),
4122
- counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n)
4245
+ counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n),
4246
+ note.circuitVersion
4123
4247
  );
4124
4248
  }
4125
4249
  };
@@ -4181,6 +4305,7 @@ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwn
4181
4305
  ownerPk: effectiveOwnerPk,
4182
4306
  blinding: plaintext.blinding,
4183
4307
  spendingKey: effectiveSpendingKey,
4308
+ circuitVersion: plaintext.circuitVersion,
4184
4309
  spent: false,
4185
4310
  spentAt: null,
4186
4311
  commitment: recomputed,
@@ -4254,7 +4379,7 @@ function selectNotes(notes, needed) {
4254
4379
  for (let j = i + 1; j < sorted.length; j++) {
4255
4380
  const a = sorted[i];
4256
4381
  const b = sorted[j];
4257
- if (a !== void 0 && b !== void 0 && a.value + b.value >= needed) {
4382
+ if (a !== void 0 && b !== void 0 && a.circuitVersion === b.circuitVersion && a.value + b.value >= needed) {
4258
4383
  return [a, b];
4259
4384
  }
4260
4385
  }
@@ -4611,6 +4736,11 @@ async function encryptNote(key, blindKey, note) {
4611
4736
  }
4612
4737
  async function decryptNoteRecord(key, rec) {
4613
4738
  const note = await decryptJson(key, rec.iv, rec.ciphertext);
4739
+ if (typeof note.circuitVersion !== "number") {
4740
+ throw new Error(
4741
+ "decryptNoteRecord: note is missing circuitVersion (invalid or corrupt record)"
4742
+ );
4743
+ }
4614
4744
  return applyNoteStatus(note, {
4615
4745
  ...rec.spent !== void 0 && { spent: rec.spent },
4616
4746
  spentAt: rec.spentAt ?? null
@@ -4621,7 +4751,7 @@ async function noteBlindTag(blindKey, hex) {
4621
4751
  }
4622
4752
 
4623
4753
  // src/proof-generator/unshield.ts
4624
- var import_proof_generator = require("@orbinum/proof-generator");
4754
+ var import_proof_generator2 = require("@orbinum/proof-generator");
4625
4755
  var import_utils3 = require("@noble/ciphers/utils.js");
4626
4756
  var import_baby_jubjub6 = require("@zk-kit/baby-jubjub");
4627
4757
  var import_poseidon_lite4 = require("poseidon-lite");
@@ -4667,15 +4797,15 @@ async function generateUnshieldProof(inputs, options = {}) {
4667
4797
  change_blinding: changeBlinding.toString(),
4668
4798
  change_owner_pubkey: changeOwnerPubkey.toString()
4669
4799
  };
4670
- const provider = options.provider ?? new import_proof_generator.WebArtifactProvider();
4800
+ const provider = options.provider ?? new import_proof_generator2.WebArtifactProvider();
4671
4801
  const opts = { provider };
4672
4802
  if (options.verbose !== void 0) opts.verbose = options.verbose;
4673
- const proofResult = await (0, import_proof_generator.generateProof)(import_proof_generator.CircuitType.Unshield, circuitInputs, opts);
4803
+ const proofResult = await (0, import_proof_generator2.generateProof)(import_proof_generator2.CircuitType.Unshield, circuitInputs, opts);
4674
4804
  return { ...proofResult, changeCommitment, changeValue, changeBlinding, changeOwnerPubkey };
4675
4805
  }
4676
4806
 
4677
4807
  // src/proof-generator/transfer.ts
4678
- var import_proof_generator2 = require("@orbinum/proof-generator");
4808
+ var import_proof_generator3 = require("@orbinum/proof-generator");
4679
4809
  async function generateTransferProof(params, options = {}) {
4680
4810
  const root = leHexToBigint(params.merkleRoot).toString();
4681
4811
  const [i0, i1] = params.inputs;
@@ -4700,14 +4830,14 @@ async function generateTransferProof(params, options = {}) {
4700
4830
  output_owner_pubkeys: [o0.ownerPk.toString(), o1.ownerPk.toString()],
4701
4831
  output_blindings: [o0.blinding.toString(), o1.blinding.toString()]
4702
4832
  };
4703
- const provider = options.provider ?? new import_proof_generator2.WebArtifactProvider();
4833
+ const provider = options.provider ?? new import_proof_generator3.WebArtifactProvider();
4704
4834
  const opts = { provider };
4705
4835
  if (options.verbose !== void 0) opts.verbose = options.verbose;
4706
- return (0, import_proof_generator2.generateProof)(import_proof_generator2.CircuitType.Transfer, circuitInputs, opts);
4836
+ return (0, import_proof_generator3.generateProof)(import_proof_generator3.CircuitType.Transfer, circuitInputs, opts);
4707
4837
  }
4708
4838
 
4709
4839
  // src/proof-generator/fee-claim.ts
4710
- var import_proof_generator3 = require("@orbinum/proof-generator");
4840
+ var import_proof_generator4 = require("@orbinum/proof-generator");
4711
4841
  async function generateFeeClaimProof(inputs, options = {}) {
4712
4842
  if (inputs.amount <= 0n) {
4713
4843
  throw new Error("Fee claim amount must be greater than zero.");
@@ -4719,10 +4849,10 @@ async function generateFeeClaimProof(inputs, options = {}) {
4719
4849
  owner_pubkey: inputs.ownerPubkey.toString(),
4720
4850
  blinding: inputs.blinding.toString()
4721
4851
  };
4722
- const provider = options.provider ?? new import_proof_generator3.WebArtifactProvider();
4852
+ const provider = options.provider ?? new import_proof_generator4.WebArtifactProvider();
4723
4853
  const opts = { provider };
4724
4854
  if (options.verbose !== void 0) opts.verbose = options.verbose;
4725
- const proofResult = await (0, import_proof_generator3.generateProof)(import_proof_generator3.CircuitType.ValueProof, circuitInputs, opts);
4855
+ const proofResult = await (0, import_proof_generator4.generateProof)(import_proof_generator4.CircuitType.ValueProof, circuitInputs, opts);
4726
4856
  const [sigCommitment, sigValue, sigAssetId, sigOwnerHash] = proofResult.publicSignals.map(BigInt);
4727
4857
  const buf = new Uint8Array(76);
4728
4858
  buf.set(bigintTo32Le(sigCommitment), 0);
@@ -4778,9 +4908,19 @@ function decodePrecompileCalldata(address, input) {
4778
4908
  const recipient = toHex(data.slice(160, 192));
4779
4909
  const fee = decodeUint(data, 192);
4780
4910
  const changeCommitment = toHex(data.slice(224, 256));
4911
+ const circuitVersion = decodeUint(data, 288);
4781
4912
  return {
4782
4913
  fnSig,
4783
- args: { root, nullifier, assetId, amount, recipient, fee, changeCommitment }
4914
+ args: {
4915
+ root,
4916
+ nullifier,
4917
+ assetId,
4918
+ amount,
4919
+ recipient,
4920
+ fee,
4921
+ changeCommitment,
4922
+ circuitVersion
4923
+ }
4784
4924
  };
4785
4925
  } catch {
4786
4926
  return { fnSig, args: {} };
@@ -4796,7 +4936,8 @@ function decodePrecompileCalldata(address, input) {
4796
4936
  const commitments = Number(decodeUint(data, commOffset));
4797
4937
  const assetId = decodeUint(data, 160);
4798
4938
  const fee = decodeUint(data, 192);
4799
- return { fnSig, args: { root, nullifiers, commitments, assetId, fee } };
4939
+ const circuitVersion = decodeUint(data, 224);
4940
+ return { fnSig, args: { root, nullifiers, commitments, assetId, fee, circuitVersion } };
4800
4941
  } catch {
4801
4942
  return { fnSig, args: {} };
4802
4943
  }
@@ -4807,7 +4948,8 @@ function decodePrecompileCalldata(address, input) {
4807
4948
  const commitment = toHex(data.slice(0, 32));
4808
4949
  const amount = decodeUint(data, 32);
4809
4950
  const assetId = decodeUint(data, 64);
4810
- return { fnSig, args: { commitment, amount, assetId } };
4951
+ const circuitVersion = decodeUint(data, 192);
4952
+ return { fnSig, args: { commitment, amount, assetId, circuitVersion } };
4811
4953
  } catch {
4812
4954
  return { fnSig, args: {} };
4813
4955
  }
@@ -4819,8 +4961,8 @@ function decodePrecompileCalldata(address, input) {
4819
4961
  var CircuitId = {
4820
4962
  Transfer: 1,
4821
4963
  Unshield: 2,
4822
- ValueProof: 4,
4823
- PrivateLink: 5
4964
+ PrivateLink: 5,
4965
+ ValueProof: 6
4824
4966
  };
4825
4967
 
4826
4968
  // src/utils/string.ts
@@ -5492,8 +5634,10 @@ var import_polkadot_api4 = require("polkadot-api");
5492
5634
  BABYJUB_SUBORDER,
5493
5635
  BN254_R,
5494
5636
  Blake2256,
5637
+ CURRENT_CIRCUIT_VERSION,
5495
5638
  CircuitId,
5496
5639
  CircuitType,
5640
+ CircuitVersionResolver,
5497
5641
  CryptoPrecompiles,
5498
5642
  ENCRYPTED_MEMO_SIZE,
5499
5643
  EncryptedMemo,