@ocash/sdk 0.1.4-rc.1 → 0.1.4-rc.3

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/browser.js CHANGED
@@ -3222,8 +3222,8 @@ var MemoryStore = class {
3222
3222
  this.utxos = /* @__PURE__ */ new Map();
3223
3223
  this.operations = [];
3224
3224
  this.merkleLeavesByChain = /* @__PURE__ */ new Map();
3225
- this.merkleTreesByChain = /* @__PURE__ */ new Map();
3226
- this.merkleNodesByChain = /* @__PURE__ */ new Map();
3225
+ this.chairmanMerkleVersionsByChain = /* @__PURE__ */ new Map();
3226
+ this.chairmanMerkleNodesByChain = /* @__PURE__ */ new Map();
3227
3227
  this.entryMemosByChain = /* @__PURE__ */ new Map();
3228
3228
  this.entryNullifiersByChain = /* @__PURE__ */ new Map();
3229
3229
  const max = options?.maxOperations;
@@ -3239,8 +3239,8 @@ var MemoryStore = class {
3239
3239
  this.utxos.clear();
3240
3240
  this.operations = [];
3241
3241
  this.merkleLeavesByChain.clear();
3242
- this.merkleTreesByChain.clear();
3243
- this.merkleNodesByChain.clear();
3242
+ this.chairmanMerkleVersionsByChain.clear();
3243
+ this.chairmanMerkleNodesByChain.clear();
3244
3244
  this.entryMemosByChain.clear();
3245
3245
  this.entryNullifiersByChain.clear();
3246
3246
  }
@@ -3356,49 +3356,64 @@ var MemoryStore = class {
3356
3356
  return { chainId, cid: row.cid, commitment: row.commitment };
3357
3357
  }
3358
3358
  /**
3359
- * Get a merkle node by id.
3359
+ * Get a chairmanMerkle tree node by id.
3360
3360
  */
3361
- async getMerkleNode(chainId, id) {
3362
- return this.merkleNodesByChain.get(chainId)?.get(id);
3361
+ async getChairmanMerkleNode(chainId, id) {
3362
+ return this.chairmanMerkleNodesByChain.get(chainId)?.get(id);
3363
3363
  }
3364
3364
  /**
3365
- * Upsert merkle nodes for a chain.
3365
+ * Put chairmanMerkle tree nodes for a chain.
3366
3366
  */
3367
- async upsertMerkleNodes(chainId, nodes) {
3367
+ async putChairmanMerkleNodes(chainId, nodes) {
3368
3368
  if (!nodes.length) return;
3369
- let map = this.merkleNodesByChain.get(chainId);
3369
+ let map = this.chairmanMerkleNodesByChain.get(chainId);
3370
3370
  if (!map) {
3371
3371
  map = /* @__PURE__ */ new Map();
3372
- this.merkleNodesByChain.set(chainId, map);
3372
+ this.chairmanMerkleNodesByChain.set(chainId, map);
3373
3373
  }
3374
3374
  for (const node of nodes) {
3375
3375
  map.set(node.id, { ...node, chainId });
3376
3376
  }
3377
3377
  }
3378
3378
  /**
3379
- * Clear merkle nodes for a chain.
3379
+ * Get a chairmanMerkle version record by chain and version number.
3380
3380
  */
3381
- async clearMerkleNodes(chainId) {
3382
- this.merkleNodesByChain.delete(chainId);
3381
+ async getChairmanMerkleVersion(chainId, version) {
3382
+ const byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3383
+ const record = byVersion?.get(version);
3384
+ return record ? { ...record } : void 0;
3383
3385
  }
3384
3386
  /**
3385
- * Get persisted merkle tree state.
3387
+ * Get the latest chairmanMerkle version record (highest version number) for a chain.
3386
3388
  */
3387
- async getMerkleTree(chainId) {
3388
- const tree = this.merkleTreesByChain.get(chainId);
3389
- return tree ? { ...tree } : void 0;
3389
+ async getLatestChairmanMerkleVersion(chainId) {
3390
+ const byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3391
+ if (!byVersion || byVersion.size === 0) return void 0;
3392
+ let latest;
3393
+ for (const record of byVersion.values()) {
3394
+ if (!latest || record.version > latest.version) {
3395
+ latest = record;
3396
+ }
3397
+ }
3398
+ return latest ? { ...latest } : void 0;
3390
3399
  }
3391
3400
  /**
3392
- * Persist merkle tree state.
3401
+ * Persist a chairmanMerkle version record.
3393
3402
  */
3394
- async setMerkleTree(chainId, tree) {
3395
- this.merkleTreesByChain.set(chainId, { ...tree, chainId });
3403
+ async putChairmanMerkleVersion(chainId, record) {
3404
+ let byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3405
+ if (!byVersion) {
3406
+ byVersion = /* @__PURE__ */ new Map();
3407
+ this.chairmanMerkleVersionsByChain.set(chainId, byVersion);
3408
+ }
3409
+ byVersion.set(record.version, { ...record, chainId });
3396
3410
  }
3397
3411
  /**
3398
- * Clear merkle tree state.
3412
+ * Clear all chairmanMerkle tree state (both nodes and versions) for a chain.
3399
3413
  */
3400
- async clearMerkleTree(chainId) {
3401
- this.merkleTreesByChain.delete(chainId);
3414
+ async clearChairmanMerkleTree(chainId) {
3415
+ this.chairmanMerkleNodesByChain.delete(chainId);
3416
+ this.chairmanMerkleVersionsByChain.delete(chainId);
3402
3417
  }
3403
3418
  /**
3404
3419
  * Upsert entry memos (raw EntryService cache).
@@ -3922,13 +3937,14 @@ var KeyValueStore = class {
3922
3937
  this.utxoCache = /* @__PURE__ */ new Map();
3923
3938
  this.operationCache = /* @__PURE__ */ new Map();
3924
3939
  this.merkleLeafCids = {};
3925
- this.merkleTrees = {};
3926
- this.merkleNodeIds = {};
3940
+ this.chairmanMerkleLatestVersions = {};
3941
+ this.chairmanMerkleNodeIds = {};
3942
+ this.chairmanMerkleVersionNums = {};
3927
3943
  this.entryMemoCids = {};
3928
3944
  this.entryNullifierNids = {};
3929
3945
  this.loadedMerkleLeaves = /* @__PURE__ */ new Set();
3930
- this.loadedMerkleTrees = /* @__PURE__ */ new Set();
3931
- this.loadedMerkleNodes = /* @__PURE__ */ new Set();
3946
+ this.loadedChairmanMerkleVersions = /* @__PURE__ */ new Set();
3947
+ this.loadedChairmanMerkleNodes = /* @__PURE__ */ new Set();
3932
3948
  this.loadedEntryMemos = /* @__PURE__ */ new Set();
3933
3949
  this.loadedEntryNullifiers = /* @__PURE__ */ new Set();
3934
3950
  this.saveChain = Promise.resolve();
@@ -3962,9 +3978,6 @@ var KeyValueStore = class {
3962
3978
  walletOperationKey(id) {
3963
3979
  return `${this.walletBaseKey()}:operation:${id}`;
3964
3980
  }
3965
- sharedChainKey(part, chainId) {
3966
- return `${this.keyPrefix()}:shared:${part}:${chainId}`;
3967
- }
3968
3981
  sharedChainMetaKey(part, chainId) {
3969
3982
  return `${this.keyPrefix()}:shared:${part}:${chainId}:meta`;
3970
3983
  }
@@ -4017,13 +4030,14 @@ var KeyValueStore = class {
4017
4030
  this.operationCache.clear();
4018
4031
  this.walletMetaLoaded = false;
4019
4032
  this.merkleLeafCids = {};
4020
- this.merkleTrees = {};
4021
- this.merkleNodeIds = {};
4033
+ this.chairmanMerkleLatestVersions = {};
4034
+ this.chairmanMerkleNodeIds = {};
4035
+ this.chairmanMerkleVersionNums = {};
4022
4036
  this.entryMemoCids = {};
4023
4037
  this.entryNullifierNids = {};
4024
4038
  this.loadedMerkleLeaves.clear();
4025
- this.loadedMerkleTrees.clear();
4026
- this.loadedMerkleNodes.clear();
4039
+ this.loadedChairmanMerkleVersions.clear();
4040
+ this.loadedChairmanMerkleNodes.clear();
4027
4041
  this.loadedEntryMemos.clear();
4028
4042
  this.loadedEntryNullifiers.clear();
4029
4043
  }
@@ -4118,20 +4132,19 @@ var KeyValueStore = class {
4118
4132
  this.merkleLeafCids[key] = new Set(this.parseNumberIndex(cidsRaw));
4119
4133
  this.loadedMerkleLeaves.add(chainId);
4120
4134
  }
4121
- async ensureMerkleTreeLoaded(chainId) {
4122
- if (this.loadedMerkleTrees.has(chainId)) return;
4135
+ async ensureChairmanMerkleVersionsLoaded(chainId) {
4136
+ if (this.loadedChairmanMerkleVersions.has(chainId)) return;
4123
4137
  const key = String(chainId);
4124
- const raw = await this.options.client.get(this.sharedChainKey("merkleTrees", chainId));
4125
- const row = this.parseJson(raw, null);
4126
- if (row && typeof row === "object") this.merkleTrees[key] = row;
4127
- this.loadedMerkleTrees.add(chainId);
4138
+ const numsRaw = await this.options.client.get(this.sharedChainMetaKey("chairmanMerkleVersions", chainId));
4139
+ this.chairmanMerkleVersionNums[key] = new Set(this.parseNumberIndex(numsRaw));
4140
+ this.loadedChairmanMerkleVersions.add(chainId);
4128
4141
  }
4129
- async ensureMerkleNodesLoaded(chainId) {
4130
- if (this.loadedMerkleNodes.has(chainId)) return;
4142
+ async ensureChairmanMerkleNodesLoaded(chainId) {
4143
+ if (this.loadedChairmanMerkleNodes.has(chainId)) return;
4131
4144
  const key = String(chainId);
4132
- const idsRaw = await this.options.client.get(this.sharedChainMetaKey("merkleNodes", chainId));
4133
- this.merkleNodeIds[key] = new Set(this.parseStringIndex(idsRaw));
4134
- this.loadedMerkleNodes.add(chainId);
4145
+ const idsRaw = await this.options.client.get(this.sharedChainMetaKey("chairmanMerkleNodes", chainId));
4146
+ this.chairmanMerkleNodeIds[key] = new Set(this.parseStringIndex(idsRaw));
4147
+ this.loadedChairmanMerkleNodes.add(chainId);
4135
4148
  }
4136
4149
  async ensureEntryMemosLoaded(chainId) {
4137
4150
  if (this.loadedEntryMemos.has(chainId)) return;
@@ -4147,64 +4160,88 @@ var KeyValueStore = class {
4147
4160
  this.entryNullifierNids[key] = new Set(this.parseNumberIndex(nidsRaw));
4148
4161
  this.loadedEntryNullifiers.add(chainId);
4149
4162
  }
4150
- async getMerkleNode(chainId, id) {
4151
- await this.ensureMerkleNodesLoaded(chainId);
4152
- if (!this.merkleNodeIds[String(chainId)]?.has(id)) return void 0;
4153
- const raw = await this.options.client.get(this.sharedRecordKey("merkleNodes", chainId, id));
4163
+ async getChairmanMerkleNode(chainId, id) {
4164
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4165
+ if (!this.chairmanMerkleNodeIds[String(chainId)]?.has(id)) return void 0;
4166
+ const raw = await this.options.client.get(this.sharedRecordKey("chairmanMerkleNodes", chainId, id));
4154
4167
  const node = this.parseJson(raw, null);
4155
4168
  if (!node) return void 0;
4156
4169
  const hash = node.hash;
4157
4170
  if (typeof hash !== "string" || !hash.startsWith("0x")) return void 0;
4158
4171
  return { ...node, chainId };
4159
4172
  }
4160
- async upsertMerkleNodes(chainId, nodes) {
4173
+ async putChairmanMerkleNodes(chainId, nodes) {
4161
4174
  if (!nodes.length) return;
4162
- await this.ensureMerkleNodesLoaded(chainId);
4175
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4163
4176
  const key = String(chainId);
4164
- const ids = this.merkleNodeIds[key] ?? /* @__PURE__ */ new Set();
4177
+ const ids = this.chairmanMerkleNodeIds[key] ?? /* @__PURE__ */ new Set();
4165
4178
  const beforeSize = ids.size;
4166
4179
  for (const node of nodes) {
4167
4180
  ids.add(node.id);
4168
4181
  }
4169
- this.merkleNodeIds[key] = ids;
4182
+ this.chairmanMerkleNodeIds[key] = ids;
4170
4183
  await this.enqueueWrite(async () => {
4171
- await Promise.all(nodes.map((node) => this.writeJson(this.sharedRecordKey("merkleNodes", chainId, node.id), { ...node, chainId })));
4184
+ await Promise.all(nodes.map((node) => this.writeJson(this.sharedRecordKey("chairmanMerkleNodes", chainId, node.id), { ...node, chainId })));
4172
4185
  if (ids.size !== beforeSize) {
4173
- await this.writeJson(this.sharedChainMetaKey("merkleNodes", chainId), Array.from(ids));
4186
+ await this.writeJson(this.sharedChainMetaKey("chairmanMerkleNodes", chainId), Array.from(ids));
4174
4187
  }
4175
4188
  });
4176
4189
  }
4177
- async clearMerkleNodes(chainId) {
4178
- await this.ensureMerkleNodesLoaded(chainId);
4179
- const ids = Array.from(this.merkleNodeIds[String(chainId)] ?? []);
4180
- delete this.merkleNodeIds[String(chainId)];
4190
+ async getChairmanMerkleVersion(chainId, version) {
4191
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4192
+ if (!this.chairmanMerkleVersionNums[String(chainId)]?.has(version)) return void 0;
4193
+ const raw = await this.options.client.get(this.sharedRecordKey("chairmanMerkleVersions", chainId, version));
4194
+ const record = this.parseJson(raw, null);
4195
+ if (!record) return void 0;
4196
+ if (typeof record.rootHash !== "string" || !record.rootHash.startsWith("0x")) return void 0;
4197
+ if (typeof record.rootId !== "string") return void 0;
4198
+ const v = Number(record.version);
4199
+ if (!Number.isFinite(v) || v < 0) return void 0;
4200
+ return { chainId, version: Math.floor(v), rootId: record.rootId, rootHash: record.rootHash };
4201
+ }
4202
+ async getLatestChairmanMerkleVersion(chainId) {
4203
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4204
+ const nums = this.chairmanMerkleVersionNums[String(chainId)];
4205
+ if (!nums || nums.size === 0) return void 0;
4206
+ const maxVersion = Math.max(...nums);
4207
+ return this.getChairmanMerkleVersion(chainId, maxVersion);
4208
+ }
4209
+ async putChairmanMerkleVersion(chainId, record) {
4210
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4211
+ const key = String(chainId);
4212
+ const nums = this.chairmanMerkleVersionNums[key] ?? /* @__PURE__ */ new Set();
4213
+ const beforeSize = nums.size;
4214
+ nums.add(record.version);
4215
+ this.chairmanMerkleVersionNums[key] = nums;
4216
+ const current = this.chairmanMerkleLatestVersions[key];
4217
+ if (!current || record.version >= current.version) {
4218
+ this.chairmanMerkleLatestVersions[key] = { ...record, chainId };
4219
+ }
4220
+ const row = { ...record, chainId };
4181
4221
  await this.enqueueWrite(async () => {
4182
- await Promise.all(ids.map((id) => this.deleteOrReset(this.sharedRecordKey("merkleNodes", chainId, id), null)));
4183
- await this.deleteOrReset(this.sharedChainMetaKey("merkleNodes", chainId), []);
4222
+ await this.writeJson(this.sharedRecordKey("chairmanMerkleVersions", chainId, record.version), row);
4223
+ if (nums.size !== beforeSize) {
4224
+ await this.writeJson(this.sharedChainMetaKey("chairmanMerkleVersions", chainId), Array.from(nums).sort((a, b) => a - b));
4225
+ }
4184
4226
  });
4185
4227
  }
4186
- async getMerkleTree(chainId) {
4187
- await this.ensureMerkleTreeLoaded(chainId);
4188
- const row = this.merkleTrees[String(chainId)];
4189
- if (!row) return void 0;
4190
- const totalElements = Number(row.totalElements);
4191
- const lastUpdated = Number(row.lastUpdated);
4192
- const root = row.root;
4193
- if (typeof root !== "string" || !root.startsWith("0x")) return void 0;
4194
- if (!Number.isFinite(totalElements) || totalElements < 0) return void 0;
4195
- return { chainId, root, totalElements: Math.floor(totalElements), lastUpdated: Number.isFinite(lastUpdated) ? Math.floor(lastUpdated) : 0 };
4196
- }
4197
- async setMerkleTree(chainId, tree) {
4198
- await this.ensureMerkleTreeLoaded(chainId);
4199
- const row = { ...tree, chainId };
4200
- this.merkleTrees[String(chainId)] = row;
4201
- await this.enqueueWrite(() => this.writeJson(this.sharedChainKey("merkleTrees", chainId), row));
4202
- }
4203
- async clearMerkleTree(chainId) {
4204
- await this.ensureMerkleTreeLoaded(chainId);
4205
- delete this.merkleTrees[String(chainId)];
4228
+ async clearChairmanMerkleTree(chainId) {
4229
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4230
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4231
+ const nodeIds = Array.from(this.chairmanMerkleNodeIds[String(chainId)] ?? []);
4232
+ const versionNums = Array.from(this.chairmanMerkleVersionNums[String(chainId)] ?? []);
4233
+ delete this.chairmanMerkleNodeIds[String(chainId)];
4234
+ delete this.chairmanMerkleVersionNums[String(chainId)];
4235
+ delete this.chairmanMerkleLatestVersions[String(chainId)];
4206
4236
  await this.enqueueWrite(async () => {
4207
- await this.deleteOrReset(this.sharedChainKey("merkleTrees", chainId), null);
4237
+ await Promise.all([
4238
+ ...nodeIds.map((id) => this.deleteOrReset(this.sharedRecordKey("chairmanMerkleNodes", chainId, id), null)),
4239
+ ...versionNums.map((v) => this.deleteOrReset(this.sharedRecordKey("chairmanMerkleVersions", chainId, v), null))
4240
+ ]);
4241
+ await Promise.all([
4242
+ this.deleteOrReset(this.sharedChainMetaKey("chairmanMerkleNodes", chainId), []),
4243
+ this.deleteOrReset(this.sharedChainMetaKey("chairmanMerkleVersions", chainId), [])
4244
+ ]);
4208
4245
  });
4209
4246
  }
4210
4247
  async upsertEntryMemos(memos) {
@@ -7048,6 +7085,7 @@ var MerkleEngine = class _MerkleEngine {
7048
7085
  this.hydrateInFlight = /* @__PURE__ */ new Map();
7049
7086
  this.mode = options?.mode ?? "hybrid";
7050
7087
  this.treeDepth = Math.max(1, Math.floor(options?.treeDepth ?? TREE_DEPTH_DEFAULT));
7088
+ this.readContractRoot = options?.readContractRoot;
7051
7089
  }
7052
7090
  /**
7053
7091
  * Compute the current merkle root index from total elements.
@@ -7056,9 +7094,6 @@ var MerkleEngine = class _MerkleEngine {
7056
7094
  if (totalElements <= tempArraySize) return 0;
7057
7095
  return Math.floor((totalElements - 1) / tempArraySize);
7058
7096
  }
7059
- /**
7060
- * Get or initialize the pending leaf buffer for a chain.
7061
- */
7062
7097
  ensurePendingLeaves(chainId) {
7063
7098
  let pending = this.pendingLeavesByChain.get(chainId);
7064
7099
  if (!pending) {
@@ -7067,9 +7102,6 @@ var MerkleEngine = class _MerkleEngine {
7067
7102
  }
7068
7103
  return pending;
7069
7104
  }
7070
- /**
7071
- * Get or initialize chain-level merkle state.
7072
- */
7073
7105
  ensureChainState(chainId) {
7074
7106
  let state = this.chainStateByChain.get(chainId);
7075
7107
  if (!state) {
@@ -7078,15 +7110,32 @@ var MerkleEngine = class _MerkleEngine {
7078
7110
  }
7079
7111
  return state;
7080
7112
  }
7081
- /**
7082
- * Poseidon2 merkle hash for a left/right pair.
7083
- */
7113
+ // ── Hashing ──
7084
7114
  static hashPair(left, right) {
7085
7115
  return Poseidon2.hashToHex(BigInt(left), BigInt(right), Poseidon2Domain.Merkle);
7086
7116
  }
7117
+ static normalizeHex32(value, name) {
7118
+ try {
7119
+ const bi = BigInt(value);
7120
+ if (bi < 0n) throw new Error("negative");
7121
+ const hex = bi.toString(16).padStart(64, "0");
7122
+ if (hex.length > 64) throw new Error("too_large");
7123
+ return `0x${hex}`;
7124
+ } catch (error) {
7125
+ throw new SdkError("MERKLE", `Invalid ${name}`, { value }, error);
7126
+ }
7127
+ }
7128
+ // ── Static helpers ──
7129
+ static totalElementsInTree(totalElements, tempArraySize = TEMP_ARRAY_SIZE_DEFAULT) {
7130
+ if (tempArraySize <= 0) throw new SdkError("MERKLE", "tempArraySize must be greater than zero", { tempArraySize });
7131
+ if (totalElements <= 0n) return 0;
7132
+ const size = BigInt(tempArraySize);
7133
+ return Number((totalElements - 1n) / size * size);
7134
+ }
7135
+ // ── Subtree (levels 0-5, 32 leaves → 1 root) ──
7087
7136
  /**
7088
7137
  * Build a fixed-depth subtree from 32 contiguous leaves.
7089
- * Returns the subtree root and all intermediate nodes for storage.
7138
+ * Returns the subtree root hash and all intermediate nodes for storage.
7090
7139
  */
7091
7140
  static buildSubtree(leafCommitments, baseIndex) {
7092
7141
  if (leafCommitments.length !== SUBTREE_SIZE) {
@@ -7107,71 +7156,71 @@ var MerkleEngine = class _MerkleEngine {
7107
7156
  const position = basePos + i;
7108
7157
  nodesToStore.push({
7109
7158
  chainId: 0,
7110
- id: `${level}-${position}`,
7111
- level,
7112
- position,
7113
- hash: next[i]
7159
+ id: `st-${level}-${position}`,
7160
+ hash: next[i],
7161
+ leftId: null,
7162
+ rightId: null
7114
7163
  });
7115
7164
  }
7116
7165
  currentLevel = next;
7117
7166
  }
7118
7167
  return { subtreeRoot: currentLevel[0], nodesToStore };
7119
7168
  }
7169
+ // ── ChairmanMerkle tree (persistent segment tree, levels 5-32) ──
7120
7170
  /**
7121
- * Fetch a node hash from storage if available.
7122
- */
7123
- async getNodeHash(chainId, id) {
7124
- const node = await this.storage?.getMerkleNode?.(chainId, id);
7125
- return node?.hash;
7126
- }
7127
- /**
7128
- * Merge a completed subtree root into the main tree, updating frontier nodes.
7129
- */
7130
- async mergeSubtreeToMainTree(input) {
7131
- let currentValue = input.subtreeRoot;
7132
- let frontierUpdated = false;
7133
- const nodesToStore = [];
7134
- for (let level = SUBTREE_DEPTH; level < this.treeDepth; level++) {
7135
- const nodeIndex = input.newTotalElements - 1 >> level;
7136
- if ((nodeIndex & 1) === 0) {
7137
- if (!frontierUpdated) {
7138
- nodesToStore.push({
7139
- chainId: input.chainId,
7140
- id: `frontier-${level}`,
7141
- level,
7142
- position: nodeIndex,
7143
- hash: currentValue
7144
- });
7145
- frontierUpdated = true;
7171
+ * Insert a subtree root into the persistent main tree.
7172
+ *
7173
+ * Top-down recursive: descends from root (level treeDepth) to the target
7174
+ * leaf position (level SUBTREE_DEPTH). At each level only the node on the
7175
+ * update path is newly created; the sibling is shared from the previous
7176
+ * version's tree.
7177
+ *
7178
+ * @returns new root node ID/hash and all newly created nodes.
7179
+ */
7180
+ async insertSubtreeRoot(chainId, prevRootId, subtreeRootHash, batchIndex, version) {
7181
+ const MAIN_DEPTH = this.treeDepth - SUBTREE_DEPTH;
7182
+ const nodes = [];
7183
+ const descend = async (nodeId, depth) => {
7184
+ const originalLevel = this.treeDepth - depth;
7185
+ if (depth === MAIN_DEPTH) {
7186
+ const newId2 = `cm-${version}-${originalLevel}`;
7187
+ nodes.push({ chainId, id: newId2, hash: subtreeRootHash, leftId: null, rightId: null });
7188
+ return { id: newId2, hash: subtreeRootHash };
7189
+ }
7190
+ let prevLeftId = null;
7191
+ let prevRightId = null;
7192
+ if (nodeId) {
7193
+ const prevNode = await this.storage?.getChairmanMerkleNode?.(chainId, nodeId);
7194
+ if (prevNode) {
7195
+ prevLeftId = prevNode.leftId;
7196
+ prevRightId = prevNode.rightId;
7146
7197
  }
7147
- currentValue = _MerkleEngine.hashPair(currentValue, getZeroHash(level));
7148
- } else {
7149
- const leftHash = await this.getNodeHash(input.chainId, `frontier-${level}`) ?? getZeroHash(level);
7150
- currentValue = _MerkleEngine.hashPair(leftHash, currentValue);
7151
7198
  }
7152
- const nextLevel = level + 1;
7153
- nodesToStore.push({
7154
- chainId: input.chainId,
7155
- id: `${nextLevel}-${nodeIndex >> 1}`,
7156
- level: nextLevel,
7157
- position: nodeIndex >> 1,
7158
- hash: currentValue
7159
- });
7160
- }
7161
- return { finalRoot: currentValue, nodesToStore };
7162
- }
7163
- /**
7164
- * Convert on-chain totalElements to the count of fully merged elements.
7165
- */
7166
- static totalElementsInTree(totalElements, tempArraySize = TEMP_ARRAY_SIZE_DEFAULT) {
7167
- if (tempArraySize <= 0) throw new SdkError("MERKLE", "tempArraySize must be greater than zero", { tempArraySize });
7168
- if (totalElements <= 0n) return 0;
7169
- const size = BigInt(tempArraySize);
7170
- return Number((totalElements - 1n) / size * size);
7199
+ const childLevel = originalLevel - 1;
7200
+ const remainingDepth = MAIN_DEPTH - depth - 1;
7201
+ const goRight = (batchIndex >> remainingDepth & 1) === 1;
7202
+ let leftResult;
7203
+ let rightResult;
7204
+ if (goRight) {
7205
+ const leftHash = prevLeftId ? (await this.storage?.getChairmanMerkleNode?.(chainId, prevLeftId))?.hash ?? getZeroHash(childLevel) : getZeroHash(childLevel);
7206
+ leftResult = { id: prevLeftId, hash: leftHash };
7207
+ const right = await descend(prevRightId, depth + 1);
7208
+ rightResult = { id: right.id, hash: right.hash };
7209
+ } else {
7210
+ const left = await descend(prevLeftId, depth + 1);
7211
+ leftResult = { id: left.id, hash: left.hash };
7212
+ const rightHash = prevRightId ? (await this.storage?.getChairmanMerkleNode?.(chainId, prevRightId))?.hash ?? getZeroHash(childLevel) : getZeroHash(childLevel);
7213
+ rightResult = { id: prevRightId, hash: rightHash };
7214
+ }
7215
+ const hash = _MerkleEngine.hashPair(leftResult.hash, rightResult.hash);
7216
+ const newId = `cm-${version}-${originalLevel}`;
7217
+ nodes.push({ chainId, id: newId, hash, leftId: leftResult.id, rightId: rightResult.id });
7218
+ return { id: newId, hash };
7219
+ };
7220
+ const root = await descend(prevRootId, 0);
7221
+ return { rootId: root.id, rootHash: root.hash, nodes };
7171
7222
  }
7172
- /**
7173
- * Hydrate local merkle state from storage on first use.
7174
- */
7223
+ // ── Hydration ──
7175
7224
  async hydrateFromStorage(chainId) {
7176
7225
  if (this.mode === "remote") return;
7177
7226
  if (this.hydratedChains.has(chainId)) return;
@@ -7180,31 +7229,17 @@ var MerkleEngine = class _MerkleEngine {
7180
7229
  const task = (async () => {
7181
7230
  try {
7182
7231
  const state = this.ensureChainState(chainId);
7183
- const leaves = await this.storage?.getMerkleLeaves?.(chainId);
7184
- if (!leaves || leaves.length === 0) return;
7185
- const sorted = [...leaves].map((l) => ({
7186
- cid: l.cid,
7187
- commitment: _MerkleEngine.normalizeHex32(l.commitment, "memo.commitment")
7188
- })).sort((a, b) => a.cid - b.cid);
7189
- for (let i = 0; i < sorted.length; i++) {
7190
- if (sorted[i].cid !== i) throw new Error(`Non-contiguous persisted merkle leaves: expected cid=${i}, got cid=${sorted[i].cid}`);
7191
- }
7192
- const storedTree = await this.storage?.getMerkleTree?.(chainId);
7193
- const leafCount = sorted.length;
7194
- const mergedFromLeaves = _MerkleEngine.totalElementsInTree(BigInt(leafCount));
7195
- const mergedFromTree = typeof storedTree?.totalElements === "number" && storedTree.totalElements > 0 ? storedTree.totalElements : 0;
7196
- const mergedElements = Math.max(mergedFromLeaves, mergedFromTree);
7197
7232
  const pending = this.ensurePendingLeaves(chainId);
7198
- pending.length = 0;
7199
- state.mergedElements = mergedElements;
7200
- if (leafCount > mergedElements) {
7201
- pending.push(...sorted.slice(mergedElements).map((l) => l.commitment));
7233
+ const latest = await this.storage?.getLatestChairmanMerkleVersion?.(chainId);
7234
+ if (latest) {
7235
+ state.mergedElements = latest.version;
7236
+ state.root = _MerkleEngine.normalizeHex32(latest.rootHash, "chairmanMerkleVersion.rootHash");
7202
7237
  }
7203
- if (storedTree?.root) {
7204
- state.root = _MerkleEngine.normalizeHex32(storedTree.root, "merkleTree.root");
7205
- } else {
7206
- const rootNode = await this.storage?.getMerkleNode?.(chainId, `${this.treeDepth}-0`);
7207
- state.root = rootNode?.hash ?? getZeroHash(this.treeDepth);
7238
+ const leaves = await this.storage?.getMerkleLeaves?.(chainId);
7239
+ if (leaves && leaves.length > state.mergedElements) {
7240
+ const sorted = [...leaves].sort((a, b) => a.cid - b.cid).slice(state.mergedElements);
7241
+ pending.length = 0;
7242
+ pending.push(...sorted.map((l) => _MerkleEngine.normalizeHex32(l.commitment, "leaf.commitment")));
7208
7243
  }
7209
7244
  } catch (error) {
7210
7245
  if (this.mode === "hybrid") return;
@@ -7217,26 +7252,7 @@ var MerkleEngine = class _MerkleEngine {
7217
7252
  this.hydrateInFlight.set(chainId, task);
7218
7253
  return task;
7219
7254
  }
7220
- /**
7221
- * Normalize unknown values to a 32-byte hex string.
7222
- */
7223
- static normalizeHex32(value, name) {
7224
- try {
7225
- const bi = BigInt(value);
7226
- if (bi < 0n) throw new Error("negative");
7227
- const hex = bi.toString(16).padStart(64, "0");
7228
- if (hex.length > 64) throw new Error("too_large");
7229
- return `0x${hex}`;
7230
- } catch (error) {
7231
- throw new SdkError("MERKLE", `Invalid ${name}`, { value }, error);
7232
- }
7233
- }
7234
- /**
7235
- * Feed contiguous (cid-ordered) memo leaves into the local merkle tree.
7236
- *
7237
- * This mirrors the client/app behavior: only after we have a full consecutive batch of 32 leaves
7238
- * do we merge them into the main tree. Leaves that are still in the buffer do not get local proofs.
7239
- */
7255
+ // ── Ingestion ──
7240
7256
  async ingestEntryMemos(chainId, memos) {
7241
7257
  if (this.mode === "remote") return;
7242
7258
  await this.hydrateFromStorage(chainId);
@@ -7264,26 +7280,42 @@ var MerkleEngine = class _MerkleEngine {
7264
7280
  expected++;
7265
7281
  while (pending.length >= SUBTREE_SIZE) {
7266
7282
  const batch = pending.splice(0, SUBTREE_SIZE);
7267
- const baseIndex = state.mergedElements;
7268
- const subtree = _MerkleEngine.buildSubtree(batch, baseIndex);
7269
- const merged = await this.mergeSubtreeToMainTree({ chainId, subtreeRoot: subtree.subtreeRoot, newTotalElements: baseIndex + SUBTREE_SIZE });
7270
- state.mergedElements += SUBTREE_SIZE;
7271
- state.root = merged.finalRoot;
7272
- const checkpointNode = {
7273
- chainId,
7274
- id: `checkpoint-${state.mergedElements}`,
7275
- level: -1,
7276
- position: 0,
7277
- hash: state.root
7278
- };
7279
- const nodes = [...subtree.nodesToStore, ...merged.nodesToStore, checkpointNode].map((n) => ({ ...n, chainId }));
7280
- await this.storage?.upsertMerkleNodes?.(chainId, nodes);
7281
- await this.storage?.setMerkleTree?.(chainId, {
7283
+ const batchIndex = state.mergedElements / SUBTREE_SIZE;
7284
+ const subtree = _MerkleEngine.buildSubtree(batch, state.mergedElements);
7285
+ const subtreeNodes = subtree.nodesToStore.map((n) => ({ ...n, chainId }));
7286
+ const prevVersion = await this.storage?.getLatestChairmanMerkleVersion?.(chainId);
7287
+ const prevRootId = prevVersion?.rootId ?? null;
7288
+ const newVersion = state.mergedElements + SUBTREE_SIZE;
7289
+ const result = await this.insertSubtreeRoot(chainId, prevRootId, subtree.subtreeRoot, batchIndex, newVersion);
7290
+ if (this.readContractRoot) {
7291
+ const rootIndex = newVersion / SUBTREE_SIZE;
7292
+ const onChainRoot = await this.readContractRoot(chainId, rootIndex).catch(() => null);
7293
+ if (onChainRoot !== null) {
7294
+ const onChainNorm = _MerkleEngine.normalizeHex32(onChainRoot, "onChainRoot");
7295
+ const isZero = BigInt(onChainNorm) === 0n;
7296
+ if (!isZero && onChainNorm !== result.rootHash) {
7297
+ const target = state.mergedElements;
7298
+ await this._rollback(chainId, target);
7299
+ throw new SdkError("MERKLE", "Local merkle root mismatch with on-chain root \u2014 rolled back", {
7300
+ chainId,
7301
+ rootIndex,
7302
+ localRoot: result.rootHash,
7303
+ onChainRoot: onChainNorm,
7304
+ version: newVersion,
7305
+ rollbackTarget: target
7306
+ });
7307
+ }
7308
+ }
7309
+ }
7310
+ await this.storage?.putChairmanMerkleNodes?.(chainId, [...subtreeNodes, ...result.nodes]);
7311
+ await this.storage?.putChairmanMerkleVersion?.(chainId, {
7282
7312
  chainId,
7283
- root: state.root,
7284
- totalElements: state.mergedElements,
7285
- lastUpdated: Date.now()
7313
+ version: newVersion,
7314
+ rootId: result.rootId,
7315
+ rootHash: result.rootHash
7286
7316
  });
7317
+ state.mergedElements = newVersion;
7318
+ state.root = result.rootHash;
7287
7319
  }
7288
7320
  }
7289
7321
  this.hydratedChains.add(chainId);
@@ -7294,20 +7326,33 @@ var MerkleEngine = class _MerkleEngine {
7294
7326
  throw new SdkError("MERKLE", "Failed to ingest local merkle leaves", { chainId, leafCount: leaves.length }, error);
7295
7327
  }
7296
7328
  }
7329
+ // ── Rollback (tree O(1) + sync cursor reset) ──
7297
7330
  /**
7298
- * Roll back the local Merkle tree to a previous checkpoint.
7331
+ * Public rollback: step back one batch (32 elements) from the current position.
7332
+ * Upper-layer code calls this on any error to reset and retry.
7299
7333
  *
7300
- * Reconstructs correct frontier state by replaying merges from stored subtree
7301
- * roots (level-5 nodes). Cost: O(batches × depth) where batches = target / 32.
7334
+ * What gets rolled back:
7335
+ * - ChairmanMerkle tree version pointer (O(1) old nodes still in storage)
7336
+ * - Pending leaves buffer (cleared)
7337
+ * - Sync cursor: memo + merkle fields (nullifier left unchanged — independent)
7302
7338
  *
7303
- * @param targetMergedElements Must be a non-negative multiple of 32 (SUBTREE_SIZE).
7339
+ * @returns true if rollback succeeded, false if already at 0 or target version doesn't exist.
7340
+ */
7341
+ async rollback(chainId) {
7342
+ const state = this.ensureChainState(chainId);
7343
+ const target = Math.max(0, state.mergedElements - SUBTREE_SIZE);
7344
+ return this._rollback(chainId, target);
7345
+ }
7346
+ /**
7347
+ * Internal rollback to an exact batch boundary.
7348
+ *
7349
+ * @param targetMergedElements Must be a non-negative multiple of 32.
7304
7350
  * Pass 0 to reset to the empty tree.
7305
- * @returns true if rollback succeeded, false if required data (checkpoint or
7306
- * subtree roots) is missing in storage.
7351
+ * @returns true if rollback succeeded, false if the target version doesn't exist.
7307
7352
  */
7308
- async rollbackTree(chainId, targetMergedElements) {
7353
+ async _rollback(chainId, targetMergedElements) {
7309
7354
  if (targetMergedElements < 0 || targetMergedElements % SUBTREE_SIZE !== 0) {
7310
- throw new SdkError("MERKLE", "rollbackTree target must be a non-negative multiple of 32", { targetMergedElements });
7355
+ throw new SdkError("MERKLE", "_rollback target must be a non-negative multiple of 32", { targetMergedElements });
7311
7356
  }
7312
7357
  const state = this.ensureChainState(chainId);
7313
7358
  const pending = this.ensurePendingLeaves(chainId);
@@ -7315,78 +7360,35 @@ var MerkleEngine = class _MerkleEngine {
7315
7360
  state.mergedElements = 0;
7316
7361
  state.root = getZeroHash(this.treeDepth);
7317
7362
  pending.length = 0;
7318
- await this.storage?.setMerkleTree?.(chainId, {
7319
- chainId,
7320
- root: state.root,
7321
- totalElements: 0,
7322
- lastUpdated: Date.now()
7323
- });
7363
+ await this.resetSyncCursor(chainId, 0);
7324
7364
  return true;
7325
7365
  }
7326
- const checkpoint = await this.storage?.getMerkleNode?.(chainId, `checkpoint-${targetMergedElements}`);
7327
- if (!checkpoint) return false;
7328
- const numBatches = targetMergedElements / SUBTREE_SIZE;
7329
- const subtreeRoots = [];
7330
- for (let batch = 0; batch < numBatches; batch++) {
7331
- const node = await this.storage?.getMerkleNode?.(chainId, `${SUBTREE_DEPTH}-${batch}`);
7332
- if (!node) return false;
7333
- subtreeRoots.push(node.hash);
7334
- }
7335
- const resetNodes = [];
7336
- for (let level = SUBTREE_DEPTH; level < this.treeDepth; level++) {
7337
- resetNodes.push({
7338
- chainId,
7339
- id: `frontier-${level}`,
7340
- level,
7341
- position: 0,
7342
- hash: getZeroHash(level)
7343
- });
7344
- }
7345
- await this.storage?.upsertMerkleNodes?.(chainId, resetNodes);
7346
- let replayRoot = getZeroHash(this.treeDepth);
7347
- for (let batch = 0; batch < numBatches; batch++) {
7348
- const merged = await this.mergeSubtreeToMainTree({
7349
- chainId,
7350
- subtreeRoot: subtreeRoots[batch],
7351
- newTotalElements: (batch + 1) * SUBTREE_SIZE
7352
- });
7353
- replayRoot = merged.finalRoot;
7354
- await this.storage?.upsertMerkleNodes?.(chainId, merged.nodesToStore.map((n) => ({ ...n, chainId })));
7355
- }
7356
- const replayNorm = _MerkleEngine.normalizeHex32(replayRoot, "replay.root");
7357
- const checkpointNorm = _MerkleEngine.normalizeHex32(checkpoint.hash, "checkpoint.root");
7358
- if (replayNorm !== checkpointNorm) {
7359
- return false;
7360
- }
7366
+ const version = await this.storage?.getChairmanMerkleVersion?.(chainId, targetMergedElements);
7367
+ if (!version) return false;
7361
7368
  state.mergedElements = targetMergedElements;
7362
- state.root = checkpointNorm;
7369
+ state.root = _MerkleEngine.normalizeHex32(version.rootHash, "version.rootHash");
7363
7370
  pending.length = 0;
7364
7371
  this.hydratedChains.add(chainId);
7365
- await this.storage?.setMerkleTree?.(chainId, {
7366
- chainId,
7367
- root: state.root,
7368
- totalElements: targetMergedElements,
7369
- lastUpdated: Date.now()
7370
- });
7371
- const updatedCheckpoint = {
7372
- chainId,
7373
- id: `checkpoint-${targetMergedElements}`,
7374
- level: -1,
7375
- position: 0,
7376
- hash: checkpointNorm
7377
- };
7378
- await this.storage?.upsertMerkleNodes?.(chainId, [updatedCheckpoint]);
7372
+ await this.resetSyncCursor(chainId, targetMergedElements);
7379
7373
  return true;
7380
7374
  }
7381
7375
  /**
7382
- * Convenience wrapper to request a single proof.
7376
+ * Reset the sync cursor's memo field to `targetMemo` (and derive merkle cursor),
7377
+ * but only if the current cursor is ahead of the target.
7378
+ * Nullifier cursor is left unchanged — nullifiers are independent of tree state.
7383
7379
  */
7380
+ async resetSyncCursor(chainId, targetMemo) {
7381
+ if (!this.storage?.getSyncCursor || !this.storage?.setSyncCursor) return;
7382
+ const cursor = await this.storage.getSyncCursor(chainId);
7383
+ if (!cursor || cursor.memo <= targetMemo) return;
7384
+ cursor.memo = targetMemo;
7385
+ cursor.merkle = this.currentMerkleRootIndex(targetMemo);
7386
+ await this.storage.setSyncCursor(chainId, cursor);
7387
+ }
7388
+ // ── Proof generation ──
7384
7389
  async getProofByCid(input) {
7385
7390
  return this.getProofByCids({ chainId: input.chainId, cids: [input.cid], totalElements: input.totalElements });
7386
7391
  }
7387
- /**
7388
- * Get merkle proofs for a set of cids using local/hybrid/remote logic.
7389
- */
7390
7392
  async getProofByCids(input) {
7391
7393
  const cids = [...input.cids];
7392
7394
  if (cids.length === 0) throw new SdkError("MERKLE", "No cids provided", { chainId: input.chainId });
@@ -7401,15 +7403,16 @@ var MerkleEngine = class _MerkleEngine {
7401
7403
  await this.hydrateFromStorage(input.chainId);
7402
7404
  const canUseLocal = this.mode !== "remote";
7403
7405
  if (canUseLocal) {
7404
- const tree = await this.storage?.getMerkleTree?.(input.chainId);
7405
- const hasDb = typeof this.storage?.getMerkleLeaf === "function" && typeof this.storage?.getMerkleNode === "function" && typeof tree?.totalElements === "number" && typeof tree?.root === "string";
7406
- if (hasDb && tree) {
7407
- if (tree.totalElements < contractTreeElements) {
7406
+ const version = contractTreeElements > 0 ? await this.storage?.getChairmanMerkleVersion?.(input.chainId, contractTreeElements) : void 0;
7407
+ const hasDb = typeof this.storage?.getMerkleLeaf === "function" && typeof this.storage?.getChairmanMerkleNode === "function" && (contractTreeElements === 0 || !!version);
7408
+ if (hasDb) {
7409
+ const state = this.ensureChainState(input.chainId);
7410
+ if (contractTreeElements > 0 && state.mergedElements < contractTreeElements) {
7408
7411
  if (this.mode === "local") {
7409
7412
  throw new SdkError("MERKLE", "Local merkle db is behind contract", {
7410
7413
  chainId: input.chainId,
7411
7414
  cids,
7412
- localTotalElements: tree.totalElements,
7415
+ localMergedElements: state.mergedElements,
7413
7416
  contractTreeElements
7414
7417
  });
7415
7418
  }
@@ -7421,30 +7424,13 @@ var MerkleEngine = class _MerkleEngine {
7421
7424
  proof.push({ leaf_index: cid, path: new Array(this.treeDepth + 1).fill("0") });
7422
7425
  continue;
7423
7426
  }
7424
- const leaf = await this.storage.getMerkleLeaf(input.chainId, cid);
7425
- if (!leaf) throw new Error(`missing_leaf:${cid}`);
7426
- const path = [leaf.commitment];
7427
- for (let level = 1; level <= this.treeDepth; level++) {
7428
- const siblingIndex = cid >> level - 1 ^ 1;
7429
- if (level === 1) {
7430
- const siblingLeaf = await this.storage.getMerkleLeaf(input.chainId, siblingIndex);
7431
- path.push(siblingLeaf?.commitment ?? getZeroHash(0));
7432
- continue;
7433
- }
7434
- const targetLevel = level - 1;
7435
- const siblingNode = await this.storage.getMerkleNode(input.chainId, `${targetLevel}-${siblingIndex}`);
7436
- path.push(siblingNode?.hash ?? getZeroHash(targetLevel));
7437
- }
7427
+ const path = await this.buildLocalProofPath(input.chainId, cid, version);
7438
7428
  proof.push({ leaf_index: cid, path });
7439
7429
  }
7440
- let effectiveRoot = tree.root;
7441
- if (tree.totalElements > contractTreeElements && contractTreeElements > 0) {
7442
- const checkpoint = await this.storage.getMerkleNode(input.chainId, `checkpoint-${contractTreeElements}`);
7443
- if (checkpoint) effectiveRoot = checkpoint.hash;
7444
- }
7430
+ const effectiveRoot = contractTreeElements > 0 ? _MerkleEngine.normalizeHex32(version.rootHash, "version.rootHash") : getZeroHash(this.treeDepth);
7445
7431
  return {
7446
7432
  proof,
7447
- merkle_root: _MerkleEngine.normalizeHex32(effectiveRoot, "merkleTree.root"),
7433
+ merkle_root: effectiveRoot,
7448
7434
  latest_cid: totalElements > 0n ? Number(totalElements - 1n) : -1
7449
7435
  };
7450
7436
  } catch (error) {
@@ -7454,7 +7440,7 @@ var MerkleEngine = class _MerkleEngine {
7454
7440
  }
7455
7441
  }
7456
7442
  } else if (this.mode === "local" && needsTreeProof.length) {
7457
- throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter_merkle_db" });
7443
+ throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter_or_version" });
7458
7444
  }
7459
7445
  }
7460
7446
  if (needsTreeProof.length === 0) {
@@ -7479,15 +7465,64 @@ var MerkleEngine = class _MerkleEngine {
7479
7465
  };
7480
7466
  }
7481
7467
  /**
7482
- * Fetch a remote merkle root (used when no proofs are needed).
7468
+ * Build a local proof path by traversing the chairmanMerkle tree.
7469
+ *
7470
+ * Levels 0-4: sibling hashes from subtree internal nodes (st-{level}-{pos}).
7471
+ * Levels 5-31: sibling hashes from chairmanMerkle tree traversal (top-down from version root).
7483
7472
  */
7473
+ async buildLocalProofPath(chainId, cid, version) {
7474
+ const leaf = await this.storage.getMerkleLeaf(chainId, cid);
7475
+ if (!leaf) throw new Error(`missing_leaf:${cid}`);
7476
+ const path = [leaf.commitment];
7477
+ for (let level = 1; level <= SUBTREE_DEPTH; level++) {
7478
+ const siblingPos = cid >> level - 1 ^ 1;
7479
+ if (level === 1) {
7480
+ const siblingLeaf = await this.storage.getMerkleLeaf(chainId, siblingPos);
7481
+ path.push(siblingLeaf?.commitment ?? getZeroHash(0));
7482
+ } else {
7483
+ const targetLevel = level - 1;
7484
+ const node = await this.storage.getChairmanMerkleNode(chainId, `st-${targetLevel}-${siblingPos}`);
7485
+ path.push(node?.hash ?? getZeroHash(targetLevel));
7486
+ }
7487
+ }
7488
+ const batchIndex = cid >> SUBTREE_DEPTH;
7489
+ const MAIN_DEPTH = this.treeDepth - SUBTREE_DEPTH;
7490
+ const mainSiblings = [];
7491
+ let nodeId = version.rootId;
7492
+ for (let depth = 0; depth < MAIN_DEPTH; depth++) {
7493
+ const childLevel = this.treeDepth - depth - 1;
7494
+ if (!nodeId) {
7495
+ mainSiblings.push(getZeroHash(childLevel));
7496
+ continue;
7497
+ }
7498
+ const node = await this.storage.getChairmanMerkleNode(chainId, nodeId);
7499
+ if (!node) {
7500
+ mainSiblings.push(getZeroHash(childLevel));
7501
+ nodeId = null;
7502
+ continue;
7503
+ }
7504
+ const remainingDepth = MAIN_DEPTH - depth - 1;
7505
+ const goRight = (batchIndex >> remainingDepth & 1) === 1;
7506
+ if (goRight) {
7507
+ const leftNode = node.leftId ? await this.storage.getChairmanMerkleNode(chainId, node.leftId) : null;
7508
+ mainSiblings.push(leftNode?.hash ?? getZeroHash(childLevel));
7509
+ nodeId = node.rightId;
7510
+ } else {
7511
+ const rightNode = node.rightId ? await this.storage.getChairmanMerkleNode(chainId, node.rightId) : null;
7512
+ mainSiblings.push(rightNode?.hash ?? getZeroHash(childLevel));
7513
+ nodeId = node.leftId;
7514
+ }
7515
+ }
7516
+ for (let i = mainSiblings.length - 1; i >= 0; i--) {
7517
+ path.push(mainSiblings[i]);
7518
+ }
7519
+ return path;
7520
+ }
7521
+ // ── Remote helpers ──
7484
7522
  async fetchRemoteRootOnly(chainId) {
7485
7523
  const remote = await this.fetchRemoteProofFromService({ chainId, cids: [0] });
7486
7524
  return _MerkleEngine.normalizeHex32(remote.merkle_root, "remote.merkle_root");
7487
7525
  }
7488
- /**
7489
- * Fetch proofs from the remote merkle service.
7490
- */
7491
7526
  async fetchRemoteProofFromService(input) {
7492
7527
  const chain = this.getChain(input.chainId);
7493
7528
  if (!chain.merkleProofUrl) {
@@ -7496,9 +7531,7 @@ var MerkleEngine = class _MerkleEngine {
7496
7531
  const client = new MerkleClient(chain.merkleProofUrl);
7497
7532
  return client.getProofByCids(input.cids);
7498
7533
  }
7499
- /**
7500
- * Build membership witnesses for provided UTXOs from a remote proof response.
7501
- */
7534
+ // ── Witness builders (unchanged) ──
7502
7535
  buildAccMemberWitnesses(input) {
7503
7536
  return input.utxos.map((utxo, idx) => {
7504
7537
  const remoteProof = input.remote.proof[idx];
@@ -7512,9 +7545,6 @@ var MerkleEngine = class _MerkleEngine {
7512
7545
  };
7513
7546
  });
7514
7547
  }
7515
- /**
7516
- * Convert UTXOs into circuit input secrets, decrypting memos and padding if needed.
7517
- */
7518
7548
  async buildInputSecretsFromUtxos(input) {
7519
7549
  if (!Array.isArray(input.utxos) || input.utxos.length === 0) {
7520
7550
  throw new SdkError("MERKLE", "No utxos provided", { count: 0 });
@@ -8658,7 +8688,7 @@ var _IndexedDbStore = class _IndexedDbStore {
8658
8688
  this.options = options;
8659
8689
  this.cursors = /* @__PURE__ */ new Map();
8660
8690
  this.operations = [];
8661
- this.merkleTrees = {};
8691
+ this.chairmanMerkleLatestVersions = {};
8662
8692
  this.db = null;
8663
8693
  const max = options.maxOperations;
8664
8694
  this.maxOperations = max == null ? Number.POSITIVE_INFINITY : Math.max(0, Math.floor(max));
@@ -8702,8 +8732,8 @@ var _IndexedDbStore = class _IndexedDbStore {
8702
8732
  { name: `${base}:entryMemos`, keyPath: ["chainId", "cid"], indexes: [{ name: "chainId", keyPath: "chainId" }] },
8703
8733
  { name: `${base}:entryNullifiers`, keyPath: ["chainId", "nid"], indexes: [{ name: "chainId", keyPath: "chainId" }] },
8704
8734
  { name: `${base}:merkleLeaves`, keyPath: ["chainId", "cid"], indexes: [{ name: "chainId", keyPath: "chainId" }] },
8705
- { name: `${base}:merkleTrees`, keyPath: "chainId" },
8706
- { name: `${base}:merkleNodes`, keyPath: ["chainId", "id"], indexes: [{ name: "chainId", keyPath: "chainId" }] }
8735
+ { name: `${base}:chairmanMerkleNodes`, keyPath: ["chainId", "id"], indexes: [{ name: "chainId", keyPath: "chainId" }] },
8736
+ { name: `${base}:chairmanMerkleVersions`, keyPath: ["chainId", "version"], indexes: [{ name: "chainId", keyPath: "chainId" }] }
8707
8737
  ];
8708
8738
  }
8709
8739
  async openDb() {
@@ -8717,6 +8747,12 @@ var _IndexedDbStore = class _IndexedDbStore {
8717
8747
  req.onerror = () => reject(req.error ?? new Error("indexedDB open failed"));
8718
8748
  req.onupgradeneeded = () => {
8719
8749
  const db2 = req.result;
8750
+ const defNames = new Set(defs.map((d) => d.name));
8751
+ for (const storeName of Array.from(db2.objectStoreNames)) {
8752
+ if (!defNames.has(storeName)) {
8753
+ db2.deleteObjectStore(storeName);
8754
+ }
8755
+ }
8720
8756
  for (const def of defs) {
8721
8757
  let store = null;
8722
8758
  if (!db2.objectStoreNames.contains(def.name)) {
@@ -8748,8 +8784,8 @@ var _IndexedDbStore = class _IndexedDbStore {
8748
8784
  entryMemos: `${base}:entryMemos`,
8749
8785
  entryNullifiers: `${base}:entryNullifiers`,
8750
8786
  merkleLeaves: `${base}:merkleLeaves`,
8751
- merkleTrees: `${base}:merkleTrees`,
8752
- merkleNodes: `${base}:merkleNodes`
8787
+ chairmanMerkleNodes: `${base}:chairmanMerkleNodes`,
8788
+ chairmanMerkleVersions: `${base}:chairmanMerkleVersions`
8753
8789
  };
8754
8790
  }
8755
8791
  async getAll(storeName) {
@@ -8886,7 +8922,7 @@ var _IndexedDbStore = class _IndexedDbStore {
8886
8922
  const walletKey = this.walletKey();
8887
8923
  this.cursors.clear();
8888
8924
  this.operations = [];
8889
- this.merkleTrees = {};
8925
+ this.chairmanMerkleLatestVersions = {};
8890
8926
  const cursorRows = await this.getAllByIndex(stores.cursors, "walletId", walletKey);
8891
8927
  for (const row of cursorRows) {
8892
8928
  this.cursors.set(row.chainId, { memo: row.memo, nullifier: row.nullifier, merkle: row.merkle });
@@ -8896,62 +8932,79 @@ var _IndexedDbStore = class _IndexedDbStore {
8896
8932
  const { walletId: _walletId, ...operation } = row;
8897
8933
  return operation;
8898
8934
  }).sort((a, b) => b.createdAt - a.createdAt);
8899
- const treeRows = await this.getAll(stores.merkleTrees);
8900
- for (const row of treeRows) {
8901
- this.merkleTrees[String(row.chainId)] = { ...row };
8935
+ const allVersionRows = await this.getAll(stores.chairmanMerkleVersions);
8936
+ for (const row of allVersionRows) {
8937
+ const key = String(row.chainId);
8938
+ const existing = this.chairmanMerkleLatestVersions[key];
8939
+ if (!existing || row.version > existing.version) {
8940
+ this.chairmanMerkleLatestVersions[key] = { ...row };
8941
+ }
8902
8942
  }
8903
8943
  this.pruneOperations();
8904
8944
  }
8905
8945
  /**
8906
- * Get a merkle node by id.
8946
+ * Get a chairmanMerkle node by id.
8907
8947
  */
8908
- async getMerkleNode(chainId, id) {
8909
- const node = await this.getByKey(this.storeNames().merkleNodes, [chainId, id]);
8948
+ async getChairmanMerkleNode(chainId, id) {
8949
+ const node = await this.getByKey(this.storeNames().chairmanMerkleNodes, [chainId, id]);
8910
8950
  if (!node) return void 0;
8911
8951
  const hash = node.hash;
8912
8952
  if (typeof hash !== "string" || !hash.startsWith("0x")) return void 0;
8953
+ if (typeof node.leftId !== "string" && node.leftId !== null) return void 0;
8954
+ if (typeof node.rightId !== "string" && node.rightId !== null) return void 0;
8913
8955
  return { ...node, chainId };
8914
8956
  }
8915
8957
  /**
8916
- * Upsert merkle nodes and persist.
8958
+ * Put chairmanMerkle nodes and persist.
8917
8959
  */
8918
- async upsertMerkleNodes(chainId, nodes) {
8960
+ async putChairmanMerkleNodes(chainId, nodes) {
8919
8961
  if (!nodes.length) return;
8920
8962
  const rows = nodes.map((node) => ({ ...node, chainId }));
8921
- await this.putMany(this.storeNames().merkleNodes, rows);
8963
+ await this.putMany(this.storeNames().chairmanMerkleNodes, rows);
8922
8964
  }
8923
8965
  /**
8924
- * Clear merkle nodes for a chain.
8966
+ * Get a chairmanMerkle version record by chainId and version.
8925
8967
  */
8926
- async clearMerkleNodes(chainId) {
8927
- await this.deleteAllByIndex(this.storeNames().merkleNodes, "chainId", chainId);
8968
+ async getChairmanMerkleVersion(chainId, version) {
8969
+ return this.getByKey(this.storeNames().chairmanMerkleVersions, [chainId, version]);
8928
8970
  }
8929
8971
  /**
8930
- * Get persisted merkle tree metadata for a chain.
8972
+ * Get the latest chairmanMerkle version for a chain from in-memory cache,
8973
+ * falling back to loading from the store if not cached.
8931
8974
  */
8932
- async getMerkleTree(chainId) {
8933
- const row = this.merkleTrees[String(chainId)];
8934
- if (!row) return void 0;
8935
- const totalElements = Number(row.totalElements);
8936
- const lastUpdated = Number(row.lastUpdated);
8937
- const root = row.root;
8938
- if (typeof root !== "string" || !root.startsWith("0x")) return void 0;
8939
- if (!Number.isFinite(totalElements) || totalElements < 0) return void 0;
8940
- return { chainId, root, totalElements: Math.floor(totalElements), lastUpdated: Number.isFinite(lastUpdated) ? Math.floor(lastUpdated) : 0 };
8975
+ async getLatestChairmanMerkleVersion(chainId) {
8976
+ const cached = this.chairmanMerkleLatestVersions[String(chainId)];
8977
+ if (cached) return { ...cached };
8978
+ const rows = await this.getAllByIndex(this.storeNames().chairmanMerkleVersions, "chainId", chainId);
8979
+ if (!rows.length) return void 0;
8980
+ let best = rows[0];
8981
+ for (const row of rows) {
8982
+ if (row.version > best.version) best = row;
8983
+ }
8984
+ this.chairmanMerkleLatestVersions[String(chainId)] = { ...best };
8985
+ return { ...best };
8941
8986
  }
8942
8987
  /**
8943
- * Persist merkle tree metadata for a chain.
8988
+ * Persist a chairmanMerkle version record and update the in-memory cache
8989
+ * if this is the latest version for the chain.
8944
8990
  */
8945
- async setMerkleTree(chainId, tree) {
8946
- this.merkleTrees[String(chainId)] = { ...tree, chainId };
8947
- await this.putMany(this.storeNames().merkleTrees, [{ ...tree, chainId }]);
8991
+ async putChairmanMerkleVersion(chainId, record) {
8992
+ await this.putMany(this.storeNames().chairmanMerkleVersions, [{ ...record, chainId }]);
8993
+ const key = String(chainId);
8994
+ const existing = this.chairmanMerkleLatestVersions[key];
8995
+ if (!existing || record.version >= existing.version) {
8996
+ this.chairmanMerkleLatestVersions[key] = { ...record, chainId };
8997
+ }
8948
8998
  }
8949
8999
  /**
8950
- * Clear merkle tree metadata for a chain.
9000
+ * Clear both chairmanMerkle nodes and versions for a chain and reset the cache.
8951
9001
  */
8952
- async clearMerkleTree(chainId) {
8953
- delete this.merkleTrees[String(chainId)];
8954
- await this.deleteByKeys(this.storeNames().merkleTrees, [chainId]);
9002
+ async clearChairmanMerkleTree(chainId) {
9003
+ delete this.chairmanMerkleLatestVersions[String(chainId)];
9004
+ await Promise.all([
9005
+ this.deleteAllByIndex(this.storeNames().chairmanMerkleNodes, "chainId", chainId),
9006
+ this.deleteAllByIndex(this.storeNames().chairmanMerkleVersions, "chainId", chainId)
9007
+ ]);
8955
9008
  }
8956
9009
  /**
8957
9010
  * Upsert entry memos and persist.
@@ -9196,7 +9249,7 @@ var _IndexedDbStore = class _IndexedDbStore {
9196
9249
  return applyOperationsQuery(this.operations, input);
9197
9250
  }
9198
9251
  };
9199
- _IndexedDbStore.DB_VERSION = 2;
9252
+ _IndexedDbStore.DB_VERSION = 3;
9200
9253
  var IndexedDbStore = _IndexedDbStore;
9201
9254
  export {
9202
9255
  App_ABI,