@orbinum/sdk 0.11.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,
@@ -1228,7 +1230,7 @@ var IndexerClient = class _IndexerClient {
1228
1230
  /**
1229
1231
  * Returns a paginated list of stealth scan hints ordered ascending by leafIndex.
1230
1232
  * Each hint contains only the fields required for ECDH triage and decryption:
1231
- * leafIndex, commitmentHex, assetId, ephPkHex, encryptedMemo.
1233
+ * leafIndex, commitmentHex, assetId, ephPkHex, encryptedMemo, circuitVersion.
1232
1234
  *
1233
1235
  * Use `sinceLeafIndex` for incremental scans (cursor = last seen leafIndex + 1).
1234
1236
  */
@@ -1648,8 +1650,8 @@ var BABYJUB_SUBORDER = 273603035897990940278080071815715938607681397215856725920
1648
1650
  // src/shielded-pool/protocol/memo.ts
1649
1651
  var import_sha2 = require("@noble/hashes/sha2.js");
1650
1652
  var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
1651
- var MEMO_PLAINTEXT_SIZE = 116;
1652
- function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk) {
1653
+ var MEMO_PLAINTEXT_SIZE = 120;
1654
+ function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion) {
1653
1655
  const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
1654
1656
  const view = new DataView(buf.buffer);
1655
1657
  view.setBigUint64(0, value & 0xffffffffffffffffn, true);
@@ -1658,6 +1660,7 @@ function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk) {
1658
1660
  buf.set(blinding.slice(0, 32), 48);
1659
1661
  view.setUint32(80, assetId >>> 0, true);
1660
1662
  buf.set(counterpartyPk.slice(0, 32), 84);
1663
+ view.setUint32(116, circuitVersion >>> 0, true);
1661
1664
  return buf;
1662
1665
  }
1663
1666
  function deriveEncryptionKey(sharedSecret, commitment) {
@@ -1670,7 +1673,7 @@ function deriveEncryptionKey(sharedSecret, commitment) {
1670
1673
 
1671
1674
  // src/shielded-pool/protocol/EncryptedMemo.ts
1672
1675
  var NONCE_SIZE = 12;
1673
- var CIPHERTEXT_SIZE = 132;
1676
+ var CIPHERTEXT_SIZE = 136;
1674
1677
  var EPH_PK_SIZE = 32;
1675
1678
  var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
1676
1679
  function bytesToBjjScalar(bytes) {
@@ -1689,14 +1692,15 @@ function parsePlaintext(nonce, ciphertextWithMac, encKey) {
1689
1692
  const blinding = bytesToBigintLE(plaintext.slice(48, 80));
1690
1693
  const assetId = BigInt(view.getUint32(80, true));
1691
1694
  const counterpartyPk = bytesToBigintLE(plaintext.slice(84, 116));
1692
- return { value, ownerPk, blinding, assetId, counterpartyPk };
1695
+ const circuitVersion = view.getUint32(116, true);
1696
+ return { value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion };
1693
1697
  } catch {
1694
1698
  return null;
1695
1699
  }
1696
1700
  }
1697
1701
  var EncryptedMemo = {
1698
1702
  /**
1699
- * 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).
1700
1704
  *
1701
1705
  * @param value Note value in planck.
1702
1706
  * @param ownerPk 32-byte owner public key (LE).
@@ -1708,11 +1712,20 @@ var EncryptedMemo = {
1708
1712
  * decoded from a privacy address).
1709
1713
  * Pass `new Uint8Array(32)` (all zeros) for a publicly-readable memo.
1710
1714
  * @param counterpartyPk 32-byte counterparty BJJ Ax. Default: all zeros.
1711
- * @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).
1712
1718
  */
1713
- 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) {
1714
1720
  const nonce = (0, import_utils.randomBytes)(NONCE_SIZE);
1715
- 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
+ );
1716
1729
  const isZeroKey = recipientIvkPacked.every((b) => b === 0);
1717
1730
  let sharedSecret;
1718
1731
  let ephPkPackedBytes;
@@ -1743,29 +1756,31 @@ var EncryptedMemo = {
1743
1756
  return result;
1744
1757
  },
1745
1758
  /**
1746
- * 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.
1747
1760
  * Decryptable by anyone with `decrypt(memo, commitment, new Uint8Array(32))`.
1748
1761
  * Convenience alias for `encrypt(..., new Uint8Array(32))`.
1749
1762
  */
1750
- encryptPublic(value, ownerPk, blinding, assetId, commitment) {
1763
+ encryptPublic(value, ownerPk, blinding, assetId, commitment, circuitVersion = 0) {
1751
1764
  return EncryptedMemo.encrypt(
1752
1765
  value,
1753
1766
  ownerPk,
1754
1767
  blinding,
1755
1768
  assetId,
1756
1769
  commitment,
1757
- new Uint8Array(32)
1770
+ new Uint8Array(32),
1771
+ new Uint8Array(32),
1772
+ circuitVersion
1758
1773
  );
1759
1774
  },
1760
1775
  /**
1761
- * 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).
1762
1777
  */
1763
1778
  dummy() {
1764
1779
  return new Uint8Array(ENCRYPTED_MEMO_SIZE);
1765
1780
  },
1766
1781
  /**
1767
1782
  * Validates that `bytes` is a properly-sized encrypted memo.
1768
- * 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).
1769
1784
  *
1770
1785
  * Call this at system boundaries (extrinsic builders, precompile encoders)
1771
1786
  * to catch malformed memos before they reach the chain and fail on-chain.
@@ -1786,7 +1801,7 @@ var EncryptedMemo = {
1786
1801
  * Returns null if decryption fails — wrong key, bad MAC, or malformed memo.
1787
1802
  * Never throws; safe for scan loops.
1788
1803
  *
1789
- * @param memoBytes 168-byte encrypted memo.
1804
+ * @param memoBytes 180-byte encrypted memo.
1790
1805
  * @param commitment 32-byte note commitment (LE).
1791
1806
  * @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
1792
1807
  */
@@ -1798,13 +1813,13 @@ var EncryptedMemo = {
1798
1813
  * Extract the ECDH shared secret from an encrypted memo using the recipient's viewing secret key.
1799
1814
  *
1800
1815
  * Used by NoteDecryptor to obtain the shared secret needed for stealth address derivation
1801
- * 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.
1802
1817
  *
1803
1818
  * Returns `new Uint8Array(32)` (all zeros) for public/dummy memos (zero ephPk).
1804
1819
  * Returns `null` if the memo is malformed or the ephPk is not a valid BJJ point.
1805
1820
  * Never throws; safe for scan loops.
1806
1821
  *
1807
- * @param memoBytes 168-byte encrypted memo.
1822
+ * @param memoBytes 180-byte encrypted memo.
1808
1823
  * @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
1809
1824
  */
1810
1825
  extractSharedSecret(memoBytes, viewingSecretKey) {
@@ -2008,7 +2023,7 @@ var ShieldedPoolModule = class {
2008
2023
  * Withdraws tokens from the shielded pool to a public address.
2009
2024
  * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
2010
2025
  * Pass a `signer` to fall back to signed submission (e.g. for testing).
2011
- * 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)
2012
2027
  */
2013
2028
  async unshield(params, signer, txOptions) {
2014
2029
  const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "unshield");
@@ -2032,8 +2047,9 @@ var ShieldedPoolModule = class {
2032
2047
  fee: params.fee ?? 0n,
2033
2048
  change_commitment: changeCommitment,
2034
2049
  change_encrypted_memo: changeEncryptedMemo,
2035
- relayer: void 0
2050
+ relayer: void 0,
2036
2051
  // Option<H160> — None for direct Substrate submissions
2052
+ circuit_version: params.circuitVersion
2037
2053
  });
2038
2054
  if (signer) {
2039
2055
  return toTxResult(
@@ -2046,7 +2062,7 @@ var ShieldedPoolModule = class {
2046
2062
  * Performs a private (shielded) transfer between two notes.
2047
2063
  * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
2048
2064
  * Pass a `signer` to fall back to signed submission (e.g. for testing).
2049
- * Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee)
2065
+ * Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee, relayer, circuitVersion)
2050
2066
  */
2051
2067
  async privateTransfer(params, signer, txOptions) {
2052
2068
  const nullifiers = params.inputs.map((inp) => inp.nullifier);
@@ -2067,8 +2083,9 @@ var ShieldedPoolModule = class {
2067
2083
  encrypted_memos: memos,
2068
2084
  asset_id: params.assetId,
2069
2085
  fee: params.fee ?? 0n,
2070
- relayer: void 0
2086
+ relayer: void 0,
2071
2087
  // Option<H160> — None for direct Substrate submissions
2088
+ circuit_version: params.circuitVersion
2072
2089
  });
2073
2090
  if (signer) {
2074
2091
  return toTxResult(
@@ -2102,7 +2119,7 @@ var ShieldedPoolModule = class {
2102
2119
  * This is a SIGNED transaction — the relayer must sign it with their wallet.
2103
2120
  * Before calling this, generate a ZK value proof with generateFeeClaimProof() (not yet implemented).
2104
2121
  *
2105
- * 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)
2106
2123
  */
2107
2124
  async claimShieldedFees(params, signer, txOptions) {
2108
2125
  EncryptedMemo.validate(params.encryptedMemo, "claimShieldedFees.encryptedMemo");
@@ -2113,7 +2130,8 @@ var ShieldedPoolModule = class {
2113
2130
  asset_id: params.assetId,
2114
2131
  encrypted_memo: params.encryptedMemo,
2115
2132
  proof: params.proof,
2116
- public_signals: params.publicSignals
2133
+ public_signals: params.publicSignals,
2134
+ circuit_version: params.circuitVersion
2117
2135
  });
2118
2136
  return toTxResult(
2119
2137
  await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
@@ -2631,6 +2649,74 @@ var ZkVerifierModule = class {
2631
2649
  }
2632
2650
  };
2633
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
+
2634
2720
  // src/relayer/RelayerStatusModule.ts
2635
2721
  var RelayerStatusModule = class {
2636
2722
  constructor(substrate) {
@@ -2876,12 +2962,12 @@ var AM_SEL = {
2876
2962
  var SP_SEL = {
2877
2963
  // shield(uint32,bytes32,bytes) → 0x9feb22ea (payable, amount = msg.value)
2878
2964
  SHIELD: new Uint8Array([159, 235, 34, 234]),
2879
- // privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256) 0x8c0f5d24
2880
- PRIVATE_TRANSFER: new Uint8Array([140, 15, 93, 36]),
2881
- // unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32) 0xd21d9a79
2882
- UNSHIELD: new Uint8Array([210, 29, 154, 121]),
2883
- // claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes) 0x42e1e74c
2884
- 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])
2885
2971
  };
2886
2972
  var KNOWN_PRECOMPILES = {
2887
2973
  // ── Ethereum standard (EIP) ─────────────────────────────────────────────
@@ -2925,9 +3011,9 @@ var KNOWN_PRECOMPILES = {
2925
3011
  name: "ShieldedPool",
2926
3012
  functions: {
2927
3013
  "9feb22ea": "shield(uint32,bytes32,bytes)",
2928
- "8c0f5d24": "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256)",
2929
- d21d9a79: "unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)",
2930
- "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)"
2931
3017
  }
2932
3018
  }
2933
3019
  };
@@ -2981,7 +3067,8 @@ var ShieldedPoolPrecompile = class {
2981
3067
  // ─── privateTransfer ───────────────────────────────────────────────────────
2982
3068
  /**
2983
3069
  * Returns the ABI-encoded calldata for
2984
- * `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.
2985
3072
  */
2986
3073
  buildPrivateTransferCalldata(params) {
2987
3074
  const nullifiers = params.inputs.map((i) => fromHex(i.nullifier));
@@ -3002,7 +3089,8 @@ var ShieldedPoolPrecompile = class {
3002
3089
  { type: "bytes32[]", value: commitments },
3003
3090
  { type: "bytes[]", value: memos },
3004
3091
  { type: "uint", value: BigInt(params.assetId) },
3005
- { type: "uint", value: params.fee ?? 0n }
3092
+ { type: "uint", value: params.fee ?? 0n },
3093
+ { type: "uint", value: BigInt(params.circuitVersion) }
3006
3094
  );
3007
3095
  }
3008
3096
  /**
@@ -3043,7 +3131,8 @@ var ShieldedPoolPrecompile = class {
3043
3131
  { type: "bytes32", value: recipientBytes },
3044
3132
  { type: "uint", value: params.fee ?? 0n },
3045
3133
  { type: "bytes32", value: changeCommitment },
3046
- { type: "bytes", value: changeEncryptedMemo }
3134
+ { type: "bytes", value: changeEncryptedMemo },
3135
+ { type: "uint", value: BigInt(params.circuitVersion) }
3047
3136
  );
3048
3137
  }
3049
3138
  /**
@@ -3093,7 +3182,7 @@ var ShieldedPoolPrecompile = class {
3093
3182
  // ─── claimShieldedFees ───────────────────────────────────────────────────────────────────
3094
3183
  /**
3095
3184
  * Returns the ABI-encoded calldata for
3096
- * `claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes)`.
3185
+ * `claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes,uint32)`.
3097
3186
  *
3098
3187
  * ABI layout (params after selector):
3099
3188
  * - `commitment` — bytes32 (fixed)
@@ -3102,6 +3191,7 @@ var ShieldedPoolPrecompile = class {
3102
3191
  * - `memo` — bytes (dynamic)
3103
3192
  * - `proof` — bytes (dynamic, 128 bytes Groth16)
3104
3193
  * - `publicSignals` — bytes (dynamic, 76 bytes)
3194
+ * - `circuitVersion` — uint32 (fixed, right-aligned)
3105
3195
  *
3106
3196
  * The validator identity is derived from `msg.sender` in the precompile —
3107
3197
  * do NOT include it in the calldata.
@@ -3127,7 +3217,8 @@ var ShieldedPoolPrecompile = class {
3127
3217
  { type: "uint", value: BigInt(params.assetId) },
3128
3218
  { type: "bytes", value: params.encryptedMemo },
3129
3219
  { type: "bytes", value: params.proof },
3130
- { type: "bytes", value: params.publicSignals }
3220
+ { type: "bytes", value: params.publicSignals },
3221
+ { type: "uint", value: BigInt(params.circuitVersion) }
3131
3222
  );
3132
3223
  }
3133
3224
  /**
@@ -3565,6 +3656,11 @@ var OrbinumClient = class _OrbinumClient {
3565
3656
  chain;
3566
3657
  /** Typed access to `zkVerifier_*` custom RPC endpoints. */
3567
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;
3568
3664
  /** Typed access to `relayer_*` RPC endpoints (registry lookup and pending fee queries). */
3569
3665
  relayerStatus;
3570
3666
  /**
@@ -3573,7 +3669,7 @@ var OrbinumClient = class _OrbinumClient {
3573
3669
  */
3574
3670
  precompiles;
3575
3671
  /** @internal Use `OrbinumClient.connect()` to obtain an instance. */
3576
- constructor(substrate, evm, indexer) {
3672
+ constructor(substrate, evm, indexer, circuitsBaseUrl) {
3577
3673
  this.substrate = substrate;
3578
3674
  this.evm = evm;
3579
3675
  this.evmExplorer = evm ? new EvmExplorer(evm) : null;
@@ -3583,6 +3679,7 @@ var OrbinumClient = class _OrbinumClient {
3583
3679
  this.privacy = new PrivacyModule(substrate);
3584
3680
  this.chain = new ChainModule(substrate);
3585
3681
  this.zkVerifier = new ZkVerifierModule(substrate);
3682
+ this.circuitVersionResolver = new CircuitVersionResolver(this.zkVerifier, circuitsBaseUrl);
3586
3683
  this.relayerStatus = new RelayerStatusModule(substrate);
3587
3684
  this.precompiles = evm ? {
3588
3685
  shieldedPool: new ShieldedPoolPrecompile(evm),
@@ -3603,7 +3700,7 @@ var OrbinumClient = class _OrbinumClient {
3603
3700
  );
3604
3701
  const evm = config.evmRpc ? new EvmClient(config.evmRpc) : null;
3605
3702
  const indexer = config.indexerUrl ? new IndexerClient({ baseUrl: config.indexerUrl }) : null;
3606
- return new _OrbinumClient(substrate, evm, indexer);
3703
+ return new _OrbinumClient(substrate, evm, indexer, config.circuitsBaseUrl);
3607
3704
  }
3608
3705
  /** Closes the underlying Substrate WebSocket connection and releases all resources. */
3609
3706
  destroy() {
@@ -3725,6 +3822,8 @@ var OrbinumClientProvider = class {
3725
3822
  };
3726
3823
  if (this.config.evmRpc) connectConfig.evmRpc = this.config.evmRpc;
3727
3824
  if (this.config.indexerUrl) connectConfig.indexerUrl = this.config.indexerUrl;
3825
+ if (this.config.circuitsBaseUrl)
3826
+ connectConfig.circuitsBaseUrl = this.config.circuitsBaseUrl;
3728
3827
  const clientPromise = OrbinumClient.connect(connectConfig);
3729
3828
  const client = await Promise.race([clientPromise, timeoutPromise]);
3730
3829
  orphanClient = client;
@@ -3911,6 +4010,9 @@ var OrbinumClientProvider = class {
3911
4010
  }
3912
4011
  };
3913
4012
 
4013
+ // src/shielded-pool/protocol/types.ts
4014
+ var CURRENT_CIRCUIT_VERSION = 1;
4015
+
3914
4016
  // src/utils/stealth.ts
3915
4017
  var import_sha22 = require("@noble/hashes/sha2.js");
3916
4018
  var import_hkdf = require("@noble/hashes/hkdf.js");
@@ -4019,6 +4121,7 @@ var NoteBuilder = class {
4019
4121
  const blinding = input.blinding ?? BigInt(Date.now());
4020
4122
  const spendingKey = input.spendingKey ?? 0n;
4021
4123
  const counterpartyPk = input.counterpartyPk ?? 0n;
4124
+ const circuitVersion = input.circuitVersion ?? CURRENT_CIRCUIT_VERSION;
4022
4125
  const useStealth = input.viewingPublicKey !== void 0 && input.recipientOwnerPk !== void 0;
4023
4126
  let memo;
4024
4127
  if (useStealth) {
@@ -4053,6 +4156,7 @@ var NoteBuilder = class {
4053
4156
  stealthCommitmentBytes,
4054
4157
  recipientIvkPacked,
4055
4158
  bigintTo32Le(counterpartyPk),
4159
+ circuitVersion,
4056
4160
  ephSk
4057
4161
  )
4058
4162
  );
@@ -4070,6 +4174,7 @@ var NoteBuilder = class {
4070
4174
  ownerPk: effectiveOwnerPk,
4071
4175
  blinding,
4072
4176
  spendingKey,
4177
+ circuitVersion,
4073
4178
  spent: false,
4074
4179
  spentAt: null,
4075
4180
  commitment: commitment2,
@@ -4092,7 +4197,8 @@ var NoteBuilder = class {
4092
4197
  Number(assetId),
4093
4198
  commitmentBytes,
4094
4199
  input.viewingPublicKey,
4095
- bigintTo32Le(counterpartyPk)
4200
+ bigintTo32Le(counterpartyPk),
4201
+ circuitVersion
4096
4202
  )
4097
4203
  ) : Array.from(EncryptedMemo.dummy());
4098
4204
  if (memo.length !== ENCRYPTED_MEMO_SIZE)
@@ -4105,6 +4211,7 @@ var NoteBuilder = class {
4105
4211
  ownerPk,
4106
4212
  blinding,
4107
4213
  spendingKey,
4214
+ circuitVersion,
4108
4215
  spent: false,
4109
4216
  spentAt: null,
4110
4217
  commitment,
@@ -4116,7 +4223,7 @@ var NoteBuilder = class {
4116
4223
  };
4117
4224
  }
4118
4225
  /**
4119
- * Build the 168-byte ECDH-encrypted memo for a note.
4226
+ * Build the 180-byte ECDH-encrypted memo for a note.
4120
4227
  *
4121
4228
  * Pure TypeScript implementation — no WASM dependency.
4122
4229
  * Uses ChaCha20-Poly1305 with ECDH key agreement (BabyJubJub ephemeral keypair).
@@ -4135,7 +4242,8 @@ var NoteBuilder = class {
4135
4242
  Number(note.assetId),
4136
4243
  bigintTo32Le(note.commitment),
4137
4244
  recipientIvkPacked ?? new Uint8Array(32),
4138
- counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n)
4245
+ counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n),
4246
+ note.circuitVersion
4139
4247
  );
4140
4248
  }
4141
4249
  };
@@ -4197,6 +4305,7 @@ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwn
4197
4305
  ownerPk: effectiveOwnerPk,
4198
4306
  blinding: plaintext.blinding,
4199
4307
  spendingKey: effectiveSpendingKey,
4308
+ circuitVersion: plaintext.circuitVersion,
4200
4309
  spent: false,
4201
4310
  spentAt: null,
4202
4311
  commitment: recomputed,
@@ -4270,7 +4379,7 @@ function selectNotes(notes, needed) {
4270
4379
  for (let j = i + 1; j < sorted.length; j++) {
4271
4380
  const a = sorted[i];
4272
4381
  const b = sorted[j];
4273
- 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) {
4274
4383
  return [a, b];
4275
4384
  }
4276
4385
  }
@@ -4627,6 +4736,11 @@ async function encryptNote(key, blindKey, note) {
4627
4736
  }
4628
4737
  async function decryptNoteRecord(key, rec) {
4629
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
+ }
4630
4744
  return applyNoteStatus(note, {
4631
4745
  ...rec.spent !== void 0 && { spent: rec.spent },
4632
4746
  spentAt: rec.spentAt ?? null
@@ -4637,7 +4751,7 @@ async function noteBlindTag(blindKey, hex) {
4637
4751
  }
4638
4752
 
4639
4753
  // src/proof-generator/unshield.ts
4640
- var import_proof_generator = require("@orbinum/proof-generator");
4754
+ var import_proof_generator2 = require("@orbinum/proof-generator");
4641
4755
  var import_utils3 = require("@noble/ciphers/utils.js");
4642
4756
  var import_baby_jubjub6 = require("@zk-kit/baby-jubjub");
4643
4757
  var import_poseidon_lite4 = require("poseidon-lite");
@@ -4683,15 +4797,15 @@ async function generateUnshieldProof(inputs, options = {}) {
4683
4797
  change_blinding: changeBlinding.toString(),
4684
4798
  change_owner_pubkey: changeOwnerPubkey.toString()
4685
4799
  };
4686
- const provider = options.provider ?? new import_proof_generator.WebArtifactProvider();
4800
+ const provider = options.provider ?? new import_proof_generator2.WebArtifactProvider();
4687
4801
  const opts = { provider };
4688
4802
  if (options.verbose !== void 0) opts.verbose = options.verbose;
4689
- 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);
4690
4804
  return { ...proofResult, changeCommitment, changeValue, changeBlinding, changeOwnerPubkey };
4691
4805
  }
4692
4806
 
4693
4807
  // src/proof-generator/transfer.ts
4694
- var import_proof_generator2 = require("@orbinum/proof-generator");
4808
+ var import_proof_generator3 = require("@orbinum/proof-generator");
4695
4809
  async function generateTransferProof(params, options = {}) {
4696
4810
  const root = leHexToBigint(params.merkleRoot).toString();
4697
4811
  const [i0, i1] = params.inputs;
@@ -4716,14 +4830,14 @@ async function generateTransferProof(params, options = {}) {
4716
4830
  output_owner_pubkeys: [o0.ownerPk.toString(), o1.ownerPk.toString()],
4717
4831
  output_blindings: [o0.blinding.toString(), o1.blinding.toString()]
4718
4832
  };
4719
- const provider = options.provider ?? new import_proof_generator2.WebArtifactProvider();
4833
+ const provider = options.provider ?? new import_proof_generator3.WebArtifactProvider();
4720
4834
  const opts = { provider };
4721
4835
  if (options.verbose !== void 0) opts.verbose = options.verbose;
4722
- 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);
4723
4837
  }
4724
4838
 
4725
4839
  // src/proof-generator/fee-claim.ts
4726
- var import_proof_generator3 = require("@orbinum/proof-generator");
4840
+ var import_proof_generator4 = require("@orbinum/proof-generator");
4727
4841
  async function generateFeeClaimProof(inputs, options = {}) {
4728
4842
  if (inputs.amount <= 0n) {
4729
4843
  throw new Error("Fee claim amount must be greater than zero.");
@@ -4735,10 +4849,10 @@ async function generateFeeClaimProof(inputs, options = {}) {
4735
4849
  owner_pubkey: inputs.ownerPubkey.toString(),
4736
4850
  blinding: inputs.blinding.toString()
4737
4851
  };
4738
- const provider = options.provider ?? new import_proof_generator3.WebArtifactProvider();
4852
+ const provider = options.provider ?? new import_proof_generator4.WebArtifactProvider();
4739
4853
  const opts = { provider };
4740
4854
  if (options.verbose !== void 0) opts.verbose = options.verbose;
4741
- 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);
4742
4856
  const [sigCommitment, sigValue, sigAssetId, sigOwnerHash] = proofResult.publicSignals.map(BigInt);
4743
4857
  const buf = new Uint8Array(76);
4744
4858
  buf.set(bigintTo32Le(sigCommitment), 0);
@@ -4794,9 +4908,19 @@ function decodePrecompileCalldata(address, input) {
4794
4908
  const recipient = toHex(data.slice(160, 192));
4795
4909
  const fee = decodeUint(data, 192);
4796
4910
  const changeCommitment = toHex(data.slice(224, 256));
4911
+ const circuitVersion = decodeUint(data, 288);
4797
4912
  return {
4798
4913
  fnSig,
4799
- 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
+ }
4800
4924
  };
4801
4925
  } catch {
4802
4926
  return { fnSig, args: {} };
@@ -4812,7 +4936,8 @@ function decodePrecompileCalldata(address, input) {
4812
4936
  const commitments = Number(decodeUint(data, commOffset));
4813
4937
  const assetId = decodeUint(data, 160);
4814
4938
  const fee = decodeUint(data, 192);
4815
- return { fnSig, args: { root, nullifiers, commitments, assetId, fee } };
4939
+ const circuitVersion = decodeUint(data, 224);
4940
+ return { fnSig, args: { root, nullifiers, commitments, assetId, fee, circuitVersion } };
4816
4941
  } catch {
4817
4942
  return { fnSig, args: {} };
4818
4943
  }
@@ -4823,7 +4948,8 @@ function decodePrecompileCalldata(address, input) {
4823
4948
  const commitment = toHex(data.slice(0, 32));
4824
4949
  const amount = decodeUint(data, 32);
4825
4950
  const assetId = decodeUint(data, 64);
4826
- return { fnSig, args: { commitment, amount, assetId } };
4951
+ const circuitVersion = decodeUint(data, 192);
4952
+ return { fnSig, args: { commitment, amount, assetId, circuitVersion } };
4827
4953
  } catch {
4828
4954
  return { fnSig, args: {} };
4829
4955
  }
@@ -4835,8 +4961,8 @@ function decodePrecompileCalldata(address, input) {
4835
4961
  var CircuitId = {
4836
4962
  Transfer: 1,
4837
4963
  Unshield: 2,
4838
- ValueProof: 4,
4839
- PrivateLink: 5
4964
+ PrivateLink: 5,
4965
+ ValueProof: 6
4840
4966
  };
4841
4967
 
4842
4968
  // src/utils/string.ts
@@ -5508,8 +5634,10 @@ var import_polkadot_api4 = require("polkadot-api");
5508
5634
  BABYJUB_SUBORDER,
5509
5635
  BN254_R,
5510
5636
  Blake2256,
5637
+ CURRENT_CIRCUIT_VERSION,
5511
5638
  CircuitId,
5512
5639
  CircuitType,
5640
+ CircuitVersionResolver,
5513
5641
  CryptoPrecompiles,
5514
5642
  ENCRYPTED_MEMO_SIZE,
5515
5643
  EncryptedMemo,