@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.mjs CHANGED
@@ -1020,11 +1020,27 @@ var EvmExplorer = class _EvmExplorer {
1020
1020
  };
1021
1021
 
1022
1022
  // src/indexer/IndexerClient.ts
1023
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
1024
+ function normalizeBaseUrl(baseUrl) {
1025
+ let url;
1026
+ try {
1027
+ url = new URL(baseUrl);
1028
+ } catch {
1029
+ throw new Error(`IndexerClient: invalid baseUrl ${JSON.stringify(baseUrl)}`);
1030
+ }
1031
+ const isLocal = LOCAL_HOSTS.has(url.hostname);
1032
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocal)) {
1033
+ throw new Error(
1034
+ `IndexerClient: baseUrl must use https:// (got ${url.protocol}//${url.hostname}). Plain http is only allowed for localhost.`
1035
+ );
1036
+ }
1037
+ return baseUrl.replace(/\/$/, "");
1038
+ }
1023
1039
  var IndexerClient = class _IndexerClient {
1024
1040
  baseUrl;
1025
1041
  timeoutMs;
1026
1042
  constructor(config) {
1027
- this.baseUrl = config.baseUrl.replace(/\/$/, "");
1043
+ this.baseUrl = normalizeBaseUrl(config.baseUrl);
1028
1044
  this.timeoutMs = config.timeoutMs ?? 1e4;
1029
1045
  }
1030
1046
  // ─── Internal helpers ──────────────────────────────────────────────────────
@@ -1082,7 +1098,7 @@ var IndexerClient = class _IndexerClient {
1082
1098
  /**
1083
1099
  * Returns a paginated list of stealth scan hints ordered ascending by leafIndex.
1084
1100
  * Each hint contains only the fields required for ECDH triage and decryption:
1085
- * leafIndex, commitmentHex, assetId, ephPkHex, encryptedMemo.
1101
+ * leafIndex, commitmentHex, assetId, ephPkHex, encryptedMemo, circuitVersion.
1086
1102
  *
1087
1103
  * Use `sinceLeafIndex` for incremental scans (cursor = last seen leafIndex + 1).
1088
1104
  */
@@ -1502,8 +1518,8 @@ var BABYJUB_SUBORDER = 273603035897990940278080071815715938607681397215856725920
1502
1518
  // src/shielded-pool/protocol/memo.ts
1503
1519
  import { sha256 } from "@noble/hashes/sha2.js";
1504
1520
  var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
1505
- var MEMO_PLAINTEXT_SIZE = 116;
1506
- function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk) {
1521
+ var MEMO_PLAINTEXT_SIZE = 120;
1522
+ function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion) {
1507
1523
  const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
1508
1524
  const view = new DataView(buf.buffer);
1509
1525
  view.setBigUint64(0, value & 0xffffffffffffffffn, true);
@@ -1512,6 +1528,7 @@ function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk) {
1512
1528
  buf.set(blinding.slice(0, 32), 48);
1513
1529
  view.setUint32(80, assetId >>> 0, true);
1514
1530
  buf.set(counterpartyPk.slice(0, 32), 84);
1531
+ view.setUint32(116, circuitVersion >>> 0, true);
1515
1532
  return buf;
1516
1533
  }
1517
1534
  function deriveEncryptionKey(sharedSecret, commitment) {
@@ -1524,7 +1541,7 @@ function deriveEncryptionKey(sharedSecret, commitment) {
1524
1541
 
1525
1542
  // src/shielded-pool/protocol/EncryptedMemo.ts
1526
1543
  var NONCE_SIZE = 12;
1527
- var CIPHERTEXT_SIZE = 132;
1544
+ var CIPHERTEXT_SIZE = 136;
1528
1545
  var EPH_PK_SIZE = 32;
1529
1546
  var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
1530
1547
  function bytesToBjjScalar(bytes) {
@@ -1543,14 +1560,15 @@ function parsePlaintext(nonce, ciphertextWithMac, encKey) {
1543
1560
  const blinding = bytesToBigintLE(plaintext.slice(48, 80));
1544
1561
  const assetId = BigInt(view.getUint32(80, true));
1545
1562
  const counterpartyPk = bytesToBigintLE(plaintext.slice(84, 116));
1546
- return { value, ownerPk, blinding, assetId, counterpartyPk };
1563
+ const circuitVersion = view.getUint32(116, true);
1564
+ return { value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion };
1547
1565
  } catch {
1548
1566
  return null;
1549
1567
  }
1550
1568
  }
1551
1569
  var EncryptedMemo = {
1552
1570
  /**
1553
- * Build and encrypt a memo for a note using ECDH (v2, 168 bytes).
1571
+ * Build and encrypt a memo for a note using ECDH (v2, 180 bytes).
1554
1572
  *
1555
1573
  * @param value Note value in planck.
1556
1574
  * @param ownerPk 32-byte owner public key (LE).
@@ -1562,11 +1580,20 @@ var EncryptedMemo = {
1562
1580
  * decoded from a privacy address).
1563
1581
  * Pass `new Uint8Array(32)` (all zeros) for a publicly-readable memo.
1564
1582
  * @param counterpartyPk 32-byte counterparty BJJ Ax. Default: all zeros.
1565
- * @returns 168-byte encrypted memo: nonce(12) || ciphertext+MAC(124) || ephPk(32).
1583
+ * @param circuitVersion ZK circuit version the note is spent under. Default: 0.
1584
+ * @param ephSkOverride 32-byte ephemeral secret key (stealth coordination). Optional.
1585
+ * @returns 180-byte encrypted memo: nonce(12) || ciphertext+MAC(136) || ephPk(32).
1566
1586
  */
1567
- encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, counterpartyPk = new Uint8Array(32), ephSkOverride) {
1587
+ encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, counterpartyPk = new Uint8Array(32), circuitVersion = 0, ephSkOverride) {
1568
1588
  const nonce = randomBytes(NONCE_SIZE);
1569
- const plaintext = serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk);
1589
+ const plaintext = serializeMemo(
1590
+ value,
1591
+ ownerPk,
1592
+ blinding,
1593
+ assetId,
1594
+ counterpartyPk,
1595
+ circuitVersion
1596
+ );
1570
1597
  const isZeroKey = recipientIvkPacked.every((b) => b === 0);
1571
1598
  let sharedSecret;
1572
1599
  let ephPkPackedBytes;
@@ -1597,29 +1624,31 @@ var EncryptedMemo = {
1597
1624
  return result;
1598
1625
  },
1599
1626
  /**
1600
- * Returns a 168-byte public memo encrypted with a zero viewing key.
1627
+ * Returns a 180-byte public memo encrypted with a zero viewing key.
1601
1628
  * Decryptable by anyone with `decrypt(memo, commitment, new Uint8Array(32))`.
1602
1629
  * Convenience alias for `encrypt(..., new Uint8Array(32))`.
1603
1630
  */
1604
- encryptPublic(value, ownerPk, blinding, assetId, commitment) {
1631
+ encryptPublic(value, ownerPk, blinding, assetId, commitment, circuitVersion = 0) {
1605
1632
  return EncryptedMemo.encrypt(
1606
1633
  value,
1607
1634
  ownerPk,
1608
1635
  blinding,
1609
1636
  assetId,
1610
1637
  commitment,
1611
- new Uint8Array(32)
1638
+ new Uint8Array(32),
1639
+ new Uint8Array(32),
1640
+ circuitVersion
1612
1641
  );
1613
1642
  },
1614
1643
  /**
1615
- * Returns a 168-byte zeroed dummy memo (no information, always valid on-chain).
1644
+ * Returns a 180-byte zeroed dummy memo (no information, always valid on-chain).
1616
1645
  */
1617
1646
  dummy() {
1618
1647
  return new Uint8Array(ENCRYPTED_MEMO_SIZE);
1619
1648
  },
1620
1649
  /**
1621
1650
  * Validates that `bytes` is a properly-sized encrypted memo.
1622
- * Throws an Error if the length is not ENCRYPTED_MEMO_SIZE (168 bytes).
1651
+ * Throws an Error if the length is not ENCRYPTED_MEMO_SIZE (180 bytes).
1623
1652
  *
1624
1653
  * Call this at system boundaries (extrinsic builders, precompile encoders)
1625
1654
  * to catch malformed memos before they reach the chain and fail on-chain.
@@ -1640,7 +1669,7 @@ var EncryptedMemo = {
1640
1669
  * Returns null if decryption fails — wrong key, bad MAC, or malformed memo.
1641
1670
  * Never throws; safe for scan loops.
1642
1671
  *
1643
- * @param memoBytes 168-byte encrypted memo.
1672
+ * @param memoBytes 180-byte encrypted memo.
1644
1673
  * @param commitment 32-byte note commitment (LE).
1645
1674
  * @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
1646
1675
  */
@@ -1652,13 +1681,13 @@ var EncryptedMemo = {
1652
1681
  * Extract the ECDH shared secret from an encrypted memo using the recipient's viewing secret key.
1653
1682
  *
1654
1683
  * Used by NoteDecryptor to obtain the shared secret needed for stealth address derivation
1655
- * without re-running the full decrypt path. Safe to call on any 168-byte memo.
1684
+ * without re-running the full decrypt path. Safe to call on any 180-byte memo.
1656
1685
  *
1657
1686
  * Returns `new Uint8Array(32)` (all zeros) for public/dummy memos (zero ephPk).
1658
1687
  * Returns `null` if the memo is malformed or the ephPk is not a valid BJJ point.
1659
1688
  * Never throws; safe for scan loops.
1660
1689
  *
1661
- * @param memoBytes 168-byte encrypted memo.
1690
+ * @param memoBytes 180-byte encrypted memo.
1662
1691
  * @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
1663
1692
  */
1664
1693
  extractSharedSecret(memoBytes, viewingSecretKey) {
@@ -1862,7 +1891,7 @@ var ShieldedPoolModule = class {
1862
1891
  * Withdraws tokens from the shielded pool to a public address.
1863
1892
  * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
1864
1893
  * Pass a `signer` to fall back to signed submission (e.g. for testing).
1865
- * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee)
1894
+ * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee, changeCommitment, changeEncryptedMemo, relayer, circuitVersion)
1866
1895
  */
1867
1896
  async unshield(params, signer, txOptions) {
1868
1897
  const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "unshield");
@@ -1886,8 +1915,9 @@ var ShieldedPoolModule = class {
1886
1915
  fee: params.fee ?? 0n,
1887
1916
  change_commitment: changeCommitment,
1888
1917
  change_encrypted_memo: changeEncryptedMemo,
1889
- relayer: void 0
1918
+ relayer: void 0,
1890
1919
  // Option<H160> — None for direct Substrate submissions
1920
+ circuit_version: params.circuitVersion
1891
1921
  });
1892
1922
  if (signer) {
1893
1923
  return toTxResult(
@@ -1900,7 +1930,7 @@ var ShieldedPoolModule = class {
1900
1930
  * Performs a private (shielded) transfer between two notes.
1901
1931
  * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
1902
1932
  * Pass a `signer` to fall back to signed submission (e.g. for testing).
1903
- * Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee)
1933
+ * Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee, relayer, circuitVersion)
1904
1934
  */
1905
1935
  async privateTransfer(params, signer, txOptions) {
1906
1936
  const nullifiers = params.inputs.map((inp) => inp.nullifier);
@@ -1921,8 +1951,9 @@ var ShieldedPoolModule = class {
1921
1951
  encrypted_memos: memos,
1922
1952
  asset_id: params.assetId,
1923
1953
  fee: params.fee ?? 0n,
1924
- relayer: void 0
1954
+ relayer: void 0,
1925
1955
  // Option<H160> — None for direct Substrate submissions
1956
+ circuit_version: params.circuitVersion
1926
1957
  });
1927
1958
  if (signer) {
1928
1959
  return toTxResult(
@@ -1956,7 +1987,7 @@ var ShieldedPoolModule = class {
1956
1987
  * This is a SIGNED transaction — the relayer must sign it with their wallet.
1957
1988
  * Before calling this, generate a ZK value proof with generateFeeClaimProof() (not yet implemented).
1958
1989
  *
1959
- * Extrinsic: shieldedPool.claim_shielded_fees(commitment, amount, asset_id, memo, proof, public_signals)
1990
+ * Extrinsic: shieldedPool.claim_shielded_fees(commitment, amount, asset_id, memo, proof, public_signals, circuit_version)
1960
1991
  */
1961
1992
  async claimShieldedFees(params, signer, txOptions) {
1962
1993
  EncryptedMemo.validate(params.encryptedMemo, "claimShieldedFees.encryptedMemo");
@@ -1967,7 +1998,8 @@ var ShieldedPoolModule = class {
1967
1998
  asset_id: params.assetId,
1968
1999
  encrypted_memo: params.encryptedMemo,
1969
2000
  proof: params.proof,
1970
- public_signals: params.publicSignals
2001
+ public_signals: params.publicSignals,
2002
+ circuit_version: params.circuitVersion
1971
2003
  });
1972
2004
  return toTxResult(
1973
2005
  await (txOptions !== void 0 ? tx.signAndSubmit(signer, txOptions) : tx.signAndSubmit(signer))
@@ -2485,6 +2517,77 @@ var ZkVerifierModule = class {
2485
2517
  }
2486
2518
  };
2487
2519
 
2520
+ // src/shielded-pool/CircuitVersionResolver.ts
2521
+ import {
2522
+ WebArtifactProvider,
2523
+ circuitTypeToId
2524
+ } from "@orbinum/proof-generator";
2525
+ var CircuitVersionResolver = class {
2526
+ constructor(zkVerifier, baseUrl, providerFactory) {
2527
+ this.zkVerifier = zkVerifier;
2528
+ this.makeProvider = providerFactory ?? ((circuit, noteVersion) => new WebArtifactProvider({
2529
+ ...baseUrl ? { baseUrl } : {},
2530
+ circuitVersions: { [circuit]: noteVersion }
2531
+ }));
2532
+ }
2533
+ zkVerifier;
2534
+ makeProvider;
2535
+ /**
2536
+ * Resolves the prover + on-chain version for spending a note of `circuit`
2537
+ * created under `noteVersion`. Fail-closed: throws on unsupported version or
2538
+ * VK-hash mismatch (CDN vs chain), before generating any proof.
2539
+ */
2540
+ async resolve(circuit, noteVersion) {
2541
+ if (!Number.isInteger(noteVersion) || noteVersion <= 0) {
2542
+ throw new Error(
2543
+ `CircuitVersionResolver: invalid note circuitVersion ${noteVersion} for ${circuit}`
2544
+ );
2545
+ }
2546
+ const provider = this.makeProvider(circuit, noteVersion);
2547
+ const resolved = await provider.getResolvedVersion(circuit);
2548
+ if (resolved.version !== noteVersion) {
2549
+ throw new Error(
2550
+ `CircuitVersionResolver: prover resolved ${circuit} to v${resolved.version}, expected v${noteVersion}`
2551
+ );
2552
+ }
2553
+ const circuitId = circuitTypeToId(circuit);
2554
+ const info = await this.zkVerifier.getCircuitVersionInfo(circuitId);
2555
+ if (!info) {
2556
+ throw new Error(
2557
+ `CircuitVersionResolver: chain reports no version info for ${circuit} (id ${circuitId})`
2558
+ );
2559
+ }
2560
+ if (!info.supportedVersions.includes(noteVersion)) {
2561
+ throw new Error(
2562
+ `CircuitVersionResolver: chain does not support ${circuit} v${noteVersion} (supported: ${info.supportedVersions.join(", ")})`
2563
+ );
2564
+ }
2565
+ const chainVk = info.vkHashes.find((h) => h.version === noteVersion);
2566
+ if (!chainVk) {
2567
+ throw new Error(
2568
+ `CircuitVersionResolver: chain has no VK hash for ${circuit} v${noteVersion}`
2569
+ );
2570
+ }
2571
+ if (!vkHashEquals(chainVk.vkHash, resolved.vkHash)) {
2572
+ throw new Error(
2573
+ `CircuitVersionResolver: VK hash mismatch for ${circuit} v${noteVersion} \u2014 prover ${resolved.vkHash}, chain ${chainVk.vkHash}`
2574
+ );
2575
+ }
2576
+ return { provider, version: noteVersion };
2577
+ }
2578
+ };
2579
+ function vkHashEquals(a, b) {
2580
+ const na = normalizeHash(a);
2581
+ const nb = normalizeHash(b);
2582
+ if (na === null || nb === null) return false;
2583
+ return na === nb;
2584
+ }
2585
+ function normalizeHash(h) {
2586
+ if (typeof h !== "string") return null;
2587
+ const s = h.toLowerCase().startsWith("0x") ? h.toLowerCase().slice(2) : h.toLowerCase();
2588
+ return /^[0-9a-f]{64}$/.test(s) ? s : null;
2589
+ }
2590
+
2488
2591
  // src/relayer/RelayerStatusModule.ts
2489
2592
  var RelayerStatusModule = class {
2490
2593
  constructor(substrate) {
@@ -2730,12 +2833,12 @@ var AM_SEL = {
2730
2833
  var SP_SEL = {
2731
2834
  // shield(uint32,bytes32,bytes) → 0x9feb22ea (payable, amount = msg.value)
2732
2835
  SHIELD: new Uint8Array([159, 235, 34, 234]),
2733
- // privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256) 0x8c0f5d24
2734
- PRIVATE_TRANSFER: new Uint8Array([140, 15, 93, 36]),
2735
- // unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32) 0xd21d9a79
2736
- UNSHIELD: new Uint8Array([210, 29, 154, 121]),
2737
- // claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes) 0x42e1e74c
2738
- CLAIM_SHIELDED_FEES: new Uint8Array([66, 225, 231, 76])
2836
+ // privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256,uint32) 0x66ed2cd4
2837
+ PRIVATE_TRANSFER: new Uint8Array([102, 237, 44, 212]),
2838
+ // unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes,uint32) 0x4e505348
2839
+ UNSHIELD: new Uint8Array([78, 80, 83, 72]),
2840
+ // claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes,uint32) 0x88d9deba
2841
+ CLAIM_SHIELDED_FEES: new Uint8Array([136, 217, 222, 186])
2739
2842
  };
2740
2843
  var KNOWN_PRECOMPILES = {
2741
2844
  // ── Ethereum standard (EIP) ─────────────────────────────────────────────
@@ -2779,9 +2882,9 @@ var KNOWN_PRECOMPILES = {
2779
2882
  name: "ShieldedPool",
2780
2883
  functions: {
2781
2884
  "9feb22ea": "shield(uint32,bytes32,bytes)",
2782
- "8c0f5d24": "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256)",
2783
- d21d9a79: "unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)",
2784
- "42e1e74c": "claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes)"
2885
+ "66ed2cd4": "privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256,uint32)",
2886
+ "4e505348": "unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes,uint32)",
2887
+ "88d9deba": "claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes,uint32)"
2785
2888
  }
2786
2889
  }
2787
2890
  };
@@ -2835,7 +2938,8 @@ var ShieldedPoolPrecompile = class {
2835
2938
  // ─── privateTransfer ───────────────────────────────────────────────────────
2836
2939
  /**
2837
2940
  * Returns the ABI-encoded calldata for
2838
- * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[], uint32, uint256)`.
2941
+ * `privateTransfer(bytes, bytes32, bytes32[], bytes32[], bytes[], uint32, uint256, uint32)`.
2942
+ * The trailing `uint32` is the circuit version the input notes were created under.
2839
2943
  */
2840
2944
  buildPrivateTransferCalldata(params) {
2841
2945
  const nullifiers = params.inputs.map((i) => fromHex(i.nullifier));
@@ -2856,7 +2960,8 @@ var ShieldedPoolPrecompile = class {
2856
2960
  { type: "bytes32[]", value: commitments },
2857
2961
  { type: "bytes[]", value: memos },
2858
2962
  { type: "uint", value: BigInt(params.assetId) },
2859
- { type: "uint", value: params.fee ?? 0n }
2963
+ { type: "uint", value: params.fee ?? 0n },
2964
+ { type: "uint", value: BigInt(params.circuitVersion) }
2860
2965
  );
2861
2966
  }
2862
2967
  /**
@@ -2897,7 +3002,8 @@ var ShieldedPoolPrecompile = class {
2897
3002
  { type: "bytes32", value: recipientBytes },
2898
3003
  { type: "uint", value: params.fee ?? 0n },
2899
3004
  { type: "bytes32", value: changeCommitment },
2900
- { type: "bytes", value: changeEncryptedMemo }
3005
+ { type: "bytes", value: changeEncryptedMemo },
3006
+ { type: "uint", value: BigInt(params.circuitVersion) }
2901
3007
  );
2902
3008
  }
2903
3009
  /**
@@ -2947,7 +3053,7 @@ var ShieldedPoolPrecompile = class {
2947
3053
  // ─── claimShieldedFees ───────────────────────────────────────────────────────────────────
2948
3054
  /**
2949
3055
  * Returns the ABI-encoded calldata for
2950
- * `claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes)`.
3056
+ * `claimShieldedFees(bytes32,uint256,uint32,bytes,bytes,bytes,uint32)`.
2951
3057
  *
2952
3058
  * ABI layout (params after selector):
2953
3059
  * - `commitment` — bytes32 (fixed)
@@ -2956,6 +3062,7 @@ var ShieldedPoolPrecompile = class {
2956
3062
  * - `memo` — bytes (dynamic)
2957
3063
  * - `proof` — bytes (dynamic, 128 bytes Groth16)
2958
3064
  * - `publicSignals` — bytes (dynamic, 76 bytes)
3065
+ * - `circuitVersion` — uint32 (fixed, right-aligned)
2959
3066
  *
2960
3067
  * The validator identity is derived from `msg.sender` in the precompile —
2961
3068
  * do NOT include it in the calldata.
@@ -2981,7 +3088,8 @@ var ShieldedPoolPrecompile = class {
2981
3088
  { type: "uint", value: BigInt(params.assetId) },
2982
3089
  { type: "bytes", value: params.encryptedMemo },
2983
3090
  { type: "bytes", value: params.proof },
2984
- { type: "bytes", value: params.publicSignals }
3091
+ { type: "bytes", value: params.publicSignals },
3092
+ { type: "uint", value: BigInt(params.circuitVersion) }
2985
3093
  );
2986
3094
  }
2987
3095
  /**
@@ -3419,6 +3527,11 @@ var OrbinumClient = class _OrbinumClient {
3419
3527
  chain;
3420
3528
  /** Typed access to `zkVerifier_*` custom RPC endpoints. */
3421
3529
  zkVerifier;
3530
+ /**
3531
+ * Resolves a note's circuit version to a pinned prover + on-chain version
3532
+ * before spending it (fail-closed: throws on unsupported version / VK mismatch).
3533
+ */
3534
+ circuitVersionResolver;
3422
3535
  /** Typed access to `relayer_*` RPC endpoints (registry lookup and pending fee queries). */
3423
3536
  relayerStatus;
3424
3537
  /**
@@ -3427,7 +3540,7 @@ var OrbinumClient = class _OrbinumClient {
3427
3540
  */
3428
3541
  precompiles;
3429
3542
  /** @internal Use `OrbinumClient.connect()` to obtain an instance. */
3430
- constructor(substrate, evm, indexer) {
3543
+ constructor(substrate, evm, indexer, circuitsBaseUrl) {
3431
3544
  this.substrate = substrate;
3432
3545
  this.evm = evm;
3433
3546
  this.evmExplorer = evm ? new EvmExplorer(evm) : null;
@@ -3437,6 +3550,7 @@ var OrbinumClient = class _OrbinumClient {
3437
3550
  this.privacy = new PrivacyModule(substrate);
3438
3551
  this.chain = new ChainModule(substrate);
3439
3552
  this.zkVerifier = new ZkVerifierModule(substrate);
3553
+ this.circuitVersionResolver = new CircuitVersionResolver(this.zkVerifier, circuitsBaseUrl);
3440
3554
  this.relayerStatus = new RelayerStatusModule(substrate);
3441
3555
  this.precompiles = evm ? {
3442
3556
  shieldedPool: new ShieldedPoolPrecompile(evm),
@@ -3457,7 +3571,7 @@ var OrbinumClient = class _OrbinumClient {
3457
3571
  );
3458
3572
  const evm = config.evmRpc ? new EvmClient(config.evmRpc) : null;
3459
3573
  const indexer = config.indexerUrl ? new IndexerClient({ baseUrl: config.indexerUrl }) : null;
3460
- return new _OrbinumClient(substrate, evm, indexer);
3574
+ return new _OrbinumClient(substrate, evm, indexer, config.circuitsBaseUrl);
3461
3575
  }
3462
3576
  /** Closes the underlying Substrate WebSocket connection and releases all resources. */
3463
3577
  destroy() {
@@ -3579,6 +3693,8 @@ var OrbinumClientProvider = class {
3579
3693
  };
3580
3694
  if (this.config.evmRpc) connectConfig.evmRpc = this.config.evmRpc;
3581
3695
  if (this.config.indexerUrl) connectConfig.indexerUrl = this.config.indexerUrl;
3696
+ if (this.config.circuitsBaseUrl)
3697
+ connectConfig.circuitsBaseUrl = this.config.circuitsBaseUrl;
3582
3698
  const clientPromise = OrbinumClient.connect(connectConfig);
3583
3699
  const client = await Promise.race([clientPromise, timeoutPromise]);
3584
3700
  orphanClient = client;
@@ -3765,6 +3881,9 @@ var OrbinumClientProvider = class {
3765
3881
  }
3766
3882
  };
3767
3883
 
3884
+ // src/shielded-pool/protocol/types.ts
3885
+ var CURRENT_CIRCUIT_VERSION = 1;
3886
+
3768
3887
  // src/utils/stealth.ts
3769
3888
  import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
3770
3889
  import { hkdf } from "@noble/hashes/hkdf.js";
@@ -3873,6 +3992,7 @@ var NoteBuilder = class {
3873
3992
  const blinding = input.blinding ?? BigInt(Date.now());
3874
3993
  const spendingKey = input.spendingKey ?? 0n;
3875
3994
  const counterpartyPk = input.counterpartyPk ?? 0n;
3995
+ const circuitVersion = input.circuitVersion ?? CURRENT_CIRCUIT_VERSION;
3876
3996
  const useStealth = input.viewingPublicKey !== void 0 && input.recipientOwnerPk !== void 0;
3877
3997
  let memo;
3878
3998
  if (useStealth) {
@@ -3907,6 +4027,7 @@ var NoteBuilder = class {
3907
4027
  stealthCommitmentBytes,
3908
4028
  recipientIvkPacked,
3909
4029
  bigintTo32Le(counterpartyPk),
4030
+ circuitVersion,
3910
4031
  ephSk
3911
4032
  )
3912
4033
  );
@@ -3924,6 +4045,7 @@ var NoteBuilder = class {
3924
4045
  ownerPk: effectiveOwnerPk,
3925
4046
  blinding,
3926
4047
  spendingKey,
4048
+ circuitVersion,
3927
4049
  spent: false,
3928
4050
  spentAt: null,
3929
4051
  commitment: commitment2,
@@ -3946,7 +4068,8 @@ var NoteBuilder = class {
3946
4068
  Number(assetId),
3947
4069
  commitmentBytes,
3948
4070
  input.viewingPublicKey,
3949
- bigintTo32Le(counterpartyPk)
4071
+ bigintTo32Le(counterpartyPk),
4072
+ circuitVersion
3950
4073
  )
3951
4074
  ) : Array.from(EncryptedMemo.dummy());
3952
4075
  if (memo.length !== ENCRYPTED_MEMO_SIZE)
@@ -3959,6 +4082,7 @@ var NoteBuilder = class {
3959
4082
  ownerPk,
3960
4083
  blinding,
3961
4084
  spendingKey,
4085
+ circuitVersion,
3962
4086
  spent: false,
3963
4087
  spentAt: null,
3964
4088
  commitment,
@@ -3970,7 +4094,7 @@ var NoteBuilder = class {
3970
4094
  };
3971
4095
  }
3972
4096
  /**
3973
- * Build the 168-byte ECDH-encrypted memo for a note.
4097
+ * Build the 180-byte ECDH-encrypted memo for a note.
3974
4098
  *
3975
4099
  * Pure TypeScript implementation — no WASM dependency.
3976
4100
  * Uses ChaCha20-Poly1305 with ECDH key agreement (BabyJubJub ephemeral keypair).
@@ -3989,7 +4113,8 @@ var NoteBuilder = class {
3989
4113
  Number(note.assetId),
3990
4114
  bigintTo32Le(note.commitment),
3991
4115
  recipientIvkPacked ?? new Uint8Array(32),
3992
- counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n)
4116
+ counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n),
4117
+ note.circuitVersion
3993
4118
  );
3994
4119
  }
3995
4120
  };
@@ -4051,6 +4176,7 @@ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwn
4051
4176
  ownerPk: effectiveOwnerPk,
4052
4177
  blinding: plaintext.blinding,
4053
4178
  spendingKey: effectiveSpendingKey,
4179
+ circuitVersion: plaintext.circuitVersion,
4054
4180
  spent: false,
4055
4181
  spentAt: null,
4056
4182
  commitment: recomputed,
@@ -4124,7 +4250,7 @@ function selectNotes(notes, needed) {
4124
4250
  for (let j = i + 1; j < sorted.length; j++) {
4125
4251
  const a = sorted[i];
4126
4252
  const b = sorted[j];
4127
- if (a !== void 0 && b !== void 0 && a.value + b.value >= needed) {
4253
+ if (a !== void 0 && b !== void 0 && a.circuitVersion === b.circuitVersion && a.value + b.value >= needed) {
4128
4254
  return [a, b];
4129
4255
  }
4130
4256
  }
@@ -4481,6 +4607,11 @@ async function encryptNote(key, blindKey, note) {
4481
4607
  }
4482
4608
  async function decryptNoteRecord(key, rec) {
4483
4609
  const note = await decryptJson(key, rec.iv, rec.ciphertext);
4610
+ if (typeof note.circuitVersion !== "number") {
4611
+ throw new Error(
4612
+ "decryptNoteRecord: note is missing circuitVersion (invalid or corrupt record)"
4613
+ );
4614
+ }
4484
4615
  return applyNoteStatus(note, {
4485
4616
  ...rec.spent !== void 0 && { spent: rec.spent },
4486
4617
  spentAt: rec.spentAt ?? null
@@ -4492,9 +4623,9 @@ async function noteBlindTag(blindKey, hex) {
4492
4623
 
4493
4624
  // src/proof-generator/unshield.ts
4494
4625
  import {
4495
- CircuitType,
4626
+ CircuitType as CircuitType2,
4496
4627
  generateProof,
4497
- WebArtifactProvider
4628
+ WebArtifactProvider as WebArtifactProvider2
4498
4629
  } from "@orbinum/proof-generator";
4499
4630
  import { randomBytes as randomBytes3 } from "@noble/ciphers/utils.js";
4500
4631
  import { mulPointEscalar as mulPointEscalar6, Base8 as Base84 } from "@zk-kit/baby-jubjub";
@@ -4541,18 +4672,18 @@ async function generateUnshieldProof(inputs, options = {}) {
4541
4672
  change_blinding: changeBlinding.toString(),
4542
4673
  change_owner_pubkey: changeOwnerPubkey.toString()
4543
4674
  };
4544
- const provider = options.provider ?? new WebArtifactProvider();
4675
+ const provider = options.provider ?? new WebArtifactProvider2();
4545
4676
  const opts = { provider };
4546
4677
  if (options.verbose !== void 0) opts.verbose = options.verbose;
4547
- const proofResult = await generateProof(CircuitType.Unshield, circuitInputs, opts);
4678
+ const proofResult = await generateProof(CircuitType2.Unshield, circuitInputs, opts);
4548
4679
  return { ...proofResult, changeCommitment, changeValue, changeBlinding, changeOwnerPubkey };
4549
4680
  }
4550
4681
 
4551
4682
  // src/proof-generator/transfer.ts
4552
4683
  import {
4553
- CircuitType as CircuitType2,
4684
+ CircuitType as CircuitType3,
4554
4685
  generateProof as generateProof2,
4555
- WebArtifactProvider as WebArtifactProvider2
4686
+ WebArtifactProvider as WebArtifactProvider3
4556
4687
  } from "@orbinum/proof-generator";
4557
4688
  async function generateTransferProof(params, options = {}) {
4558
4689
  const root = leHexToBigint(params.merkleRoot).toString();
@@ -4578,17 +4709,17 @@ async function generateTransferProof(params, options = {}) {
4578
4709
  output_owner_pubkeys: [o0.ownerPk.toString(), o1.ownerPk.toString()],
4579
4710
  output_blindings: [o0.blinding.toString(), o1.blinding.toString()]
4580
4711
  };
4581
- const provider = options.provider ?? new WebArtifactProvider2();
4712
+ const provider = options.provider ?? new WebArtifactProvider3();
4582
4713
  const opts = { provider };
4583
4714
  if (options.verbose !== void 0) opts.verbose = options.verbose;
4584
- return generateProof2(CircuitType2.Transfer, circuitInputs, opts);
4715
+ return generateProof2(CircuitType3.Transfer, circuitInputs, opts);
4585
4716
  }
4586
4717
 
4587
4718
  // src/proof-generator/fee-claim.ts
4588
4719
  import {
4589
- CircuitType as CircuitType3,
4720
+ CircuitType as CircuitType4,
4590
4721
  generateProof as generateProof3,
4591
- WebArtifactProvider as WebArtifactProvider3
4722
+ WebArtifactProvider as WebArtifactProvider4
4592
4723
  } from "@orbinum/proof-generator";
4593
4724
  async function generateFeeClaimProof(inputs, options = {}) {
4594
4725
  if (inputs.amount <= 0n) {
@@ -4601,10 +4732,10 @@ async function generateFeeClaimProof(inputs, options = {}) {
4601
4732
  owner_pubkey: inputs.ownerPubkey.toString(),
4602
4733
  blinding: inputs.blinding.toString()
4603
4734
  };
4604
- const provider = options.provider ?? new WebArtifactProvider3();
4735
+ const provider = options.provider ?? new WebArtifactProvider4();
4605
4736
  const opts = { provider };
4606
4737
  if (options.verbose !== void 0) opts.verbose = options.verbose;
4607
- const proofResult = await generateProof3(CircuitType3.ValueProof, circuitInputs, opts);
4738
+ const proofResult = await generateProof3(CircuitType4.ValueProof, circuitInputs, opts);
4608
4739
  const [sigCommitment, sigValue, sigAssetId, sigOwnerHash] = proofResult.publicSignals.map(BigInt);
4609
4740
  const buf = new Uint8Array(76);
4610
4741
  buf.set(bigintTo32Le(sigCommitment), 0);
@@ -4660,9 +4791,19 @@ function decodePrecompileCalldata(address, input) {
4660
4791
  const recipient = toHex(data.slice(160, 192));
4661
4792
  const fee = decodeUint(data, 192);
4662
4793
  const changeCommitment = toHex(data.slice(224, 256));
4794
+ const circuitVersion = decodeUint(data, 288);
4663
4795
  return {
4664
4796
  fnSig,
4665
- args: { root, nullifier, assetId, amount, recipient, fee, changeCommitment }
4797
+ args: {
4798
+ root,
4799
+ nullifier,
4800
+ assetId,
4801
+ amount,
4802
+ recipient,
4803
+ fee,
4804
+ changeCommitment,
4805
+ circuitVersion
4806
+ }
4666
4807
  };
4667
4808
  } catch {
4668
4809
  return { fnSig, args: {} };
@@ -4678,7 +4819,8 @@ function decodePrecompileCalldata(address, input) {
4678
4819
  const commitments = Number(decodeUint(data, commOffset));
4679
4820
  const assetId = decodeUint(data, 160);
4680
4821
  const fee = decodeUint(data, 192);
4681
- return { fnSig, args: { root, nullifiers, commitments, assetId, fee } };
4822
+ const circuitVersion = decodeUint(data, 224);
4823
+ return { fnSig, args: { root, nullifiers, commitments, assetId, fee, circuitVersion } };
4682
4824
  } catch {
4683
4825
  return { fnSig, args: {} };
4684
4826
  }
@@ -4689,7 +4831,8 @@ function decodePrecompileCalldata(address, input) {
4689
4831
  const commitment = toHex(data.slice(0, 32));
4690
4832
  const amount = decodeUint(data, 32);
4691
4833
  const assetId = decodeUint(data, 64);
4692
- return { fnSig, args: { commitment, amount, assetId } };
4834
+ const circuitVersion = decodeUint(data, 192);
4835
+ return { fnSig, args: { commitment, amount, assetId, circuitVersion } };
4693
4836
  } catch {
4694
4837
  return { fnSig, args: {} };
4695
4838
  }
@@ -4701,8 +4844,8 @@ function decodePrecompileCalldata(address, input) {
4701
4844
  var CircuitId = {
4702
4845
  Transfer: 1,
4703
4846
  Unshield: 2,
4704
- ValueProof: 4,
4705
- PrivateLink: 5
4847
+ PrivateLink: 5,
4848
+ ValueProof: 6
4706
4849
  };
4707
4850
 
4708
4851
  // src/utils/string.ts
@@ -5384,8 +5527,10 @@ export {
5384
5527
  BABYJUB_SUBORDER,
5385
5528
  BN254_R,
5386
5529
  Blake2256,
5530
+ CURRENT_CIRCUIT_VERSION,
5387
5531
  CircuitId,
5388
- CircuitType,
5532
+ CircuitType2 as CircuitType,
5533
+ CircuitVersionResolver,
5389
5534
  CryptoPrecompiles,
5390
5535
  ENCRYPTED_MEMO_SIZE,
5391
5536
  EncryptedMemo,
@@ -5408,7 +5553,7 @@ export {
5408
5553
  Storage,
5409
5554
  SubstrateClient,
5410
5555
  VaultLockedError,
5411
- WebArtifactProvider,
5556
+ WebArtifactProvider2 as WebArtifactProvider,
5412
5557
  ZkVerifierModule,
5413
5558
  accountIdHexToSs58,
5414
5559
  addressToAccountIdHex,