@ocash/sdk 0.1.4-rc.0 → 0.1.4-rc.2

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.cjs CHANGED
@@ -3290,8 +3290,8 @@ var MemoryStore = class {
3290
3290
  this.utxos = /* @__PURE__ */ new Map();
3291
3291
  this.operations = [];
3292
3292
  this.merkleLeavesByChain = /* @__PURE__ */ new Map();
3293
- this.merkleTreesByChain = /* @__PURE__ */ new Map();
3294
- this.merkleNodesByChain = /* @__PURE__ */ new Map();
3293
+ this.chairmanMerkleVersionsByChain = /* @__PURE__ */ new Map();
3294
+ this.chairmanMerkleNodesByChain = /* @__PURE__ */ new Map();
3295
3295
  this.entryMemosByChain = /* @__PURE__ */ new Map();
3296
3296
  this.entryNullifiersByChain = /* @__PURE__ */ new Map();
3297
3297
  const max = options?.maxOperations;
@@ -3307,8 +3307,8 @@ var MemoryStore = class {
3307
3307
  this.utxos.clear();
3308
3308
  this.operations = [];
3309
3309
  this.merkleLeavesByChain.clear();
3310
- this.merkleTreesByChain.clear();
3311
- this.merkleNodesByChain.clear();
3310
+ this.chairmanMerkleVersionsByChain.clear();
3311
+ this.chairmanMerkleNodesByChain.clear();
3312
3312
  this.entryMemosByChain.clear();
3313
3313
  this.entryNullifiersByChain.clear();
3314
3314
  }
@@ -3424,49 +3424,64 @@ var MemoryStore = class {
3424
3424
  return { chainId, cid: row.cid, commitment: row.commitment };
3425
3425
  }
3426
3426
  /**
3427
- * Get a merkle node by id.
3427
+ * Get a chairmanMerkle tree node by id.
3428
3428
  */
3429
- async getMerkleNode(chainId, id) {
3430
- return this.merkleNodesByChain.get(chainId)?.get(id);
3429
+ async getChairmanMerkleNode(chainId, id) {
3430
+ return this.chairmanMerkleNodesByChain.get(chainId)?.get(id);
3431
3431
  }
3432
3432
  /**
3433
- * Upsert merkle nodes for a chain.
3433
+ * Put chairmanMerkle tree nodes for a chain.
3434
3434
  */
3435
- async upsertMerkleNodes(chainId, nodes) {
3435
+ async putChairmanMerkleNodes(chainId, nodes) {
3436
3436
  if (!nodes.length) return;
3437
- let map = this.merkleNodesByChain.get(chainId);
3437
+ let map = this.chairmanMerkleNodesByChain.get(chainId);
3438
3438
  if (!map) {
3439
3439
  map = /* @__PURE__ */ new Map();
3440
- this.merkleNodesByChain.set(chainId, map);
3440
+ this.chairmanMerkleNodesByChain.set(chainId, map);
3441
3441
  }
3442
3442
  for (const node of nodes) {
3443
3443
  map.set(node.id, { ...node, chainId });
3444
3444
  }
3445
3445
  }
3446
3446
  /**
3447
- * Clear merkle nodes for a chain.
3447
+ * Get a chairmanMerkle version record by chain and version number.
3448
3448
  */
3449
- async clearMerkleNodes(chainId) {
3450
- this.merkleNodesByChain.delete(chainId);
3449
+ async getChairmanMerkleVersion(chainId, version) {
3450
+ const byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3451
+ const record = byVersion?.get(version);
3452
+ return record ? { ...record } : void 0;
3451
3453
  }
3452
3454
  /**
3453
- * Get persisted merkle tree state.
3455
+ * Get the latest chairmanMerkle version record (highest version number) for a chain.
3454
3456
  */
3455
- async getMerkleTree(chainId) {
3456
- const tree = this.merkleTreesByChain.get(chainId);
3457
- return tree ? { ...tree } : void 0;
3457
+ async getLatestChairmanMerkleVersion(chainId) {
3458
+ const byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3459
+ if (!byVersion || byVersion.size === 0) return void 0;
3460
+ let latest;
3461
+ for (const record of byVersion.values()) {
3462
+ if (!latest || record.version > latest.version) {
3463
+ latest = record;
3464
+ }
3465
+ }
3466
+ return latest ? { ...latest } : void 0;
3458
3467
  }
3459
3468
  /**
3460
- * Persist merkle tree state.
3469
+ * Persist a chairmanMerkle version record.
3461
3470
  */
3462
- async setMerkleTree(chainId, tree) {
3463
- this.merkleTreesByChain.set(chainId, { ...tree, chainId });
3471
+ async putChairmanMerkleVersion(chainId, record) {
3472
+ let byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3473
+ if (!byVersion) {
3474
+ byVersion = /* @__PURE__ */ new Map();
3475
+ this.chairmanMerkleVersionsByChain.set(chainId, byVersion);
3476
+ }
3477
+ byVersion.set(record.version, { ...record, chainId });
3464
3478
  }
3465
3479
  /**
3466
- * Clear merkle tree state.
3480
+ * Clear all chairmanMerkle tree state (both nodes and versions) for a chain.
3467
3481
  */
3468
- async clearMerkleTree(chainId) {
3469
- this.merkleTreesByChain.delete(chainId);
3482
+ async clearChairmanMerkleTree(chainId) {
3483
+ this.chairmanMerkleNodesByChain.delete(chainId);
3484
+ this.chairmanMerkleVersionsByChain.delete(chainId);
3470
3485
  }
3471
3486
  /**
3472
3487
  * Upsert entry memos (raw EntryService cache).
@@ -3990,13 +4005,14 @@ var KeyValueStore = class {
3990
4005
  this.utxoCache = /* @__PURE__ */ new Map();
3991
4006
  this.operationCache = /* @__PURE__ */ new Map();
3992
4007
  this.merkleLeafCids = {};
3993
- this.merkleTrees = {};
3994
- this.merkleNodeIds = {};
4008
+ this.chairmanMerkleLatestVersions = {};
4009
+ this.chairmanMerkleNodeIds = {};
4010
+ this.chairmanMerkleVersionNums = {};
3995
4011
  this.entryMemoCids = {};
3996
4012
  this.entryNullifierNids = {};
3997
4013
  this.loadedMerkleLeaves = /* @__PURE__ */ new Set();
3998
- this.loadedMerkleTrees = /* @__PURE__ */ new Set();
3999
- this.loadedMerkleNodes = /* @__PURE__ */ new Set();
4014
+ this.loadedChairmanMerkleVersions = /* @__PURE__ */ new Set();
4015
+ this.loadedChairmanMerkleNodes = /* @__PURE__ */ new Set();
4000
4016
  this.loadedEntryMemos = /* @__PURE__ */ new Set();
4001
4017
  this.loadedEntryNullifiers = /* @__PURE__ */ new Set();
4002
4018
  this.saveChain = Promise.resolve();
@@ -4030,9 +4046,6 @@ var KeyValueStore = class {
4030
4046
  walletOperationKey(id) {
4031
4047
  return `${this.walletBaseKey()}:operation:${id}`;
4032
4048
  }
4033
- sharedChainKey(part, chainId) {
4034
- return `${this.keyPrefix()}:shared:${part}:${chainId}`;
4035
- }
4036
4049
  sharedChainMetaKey(part, chainId) {
4037
4050
  return `${this.keyPrefix()}:shared:${part}:${chainId}:meta`;
4038
4051
  }
@@ -4085,13 +4098,14 @@ var KeyValueStore = class {
4085
4098
  this.operationCache.clear();
4086
4099
  this.walletMetaLoaded = false;
4087
4100
  this.merkleLeafCids = {};
4088
- this.merkleTrees = {};
4089
- this.merkleNodeIds = {};
4101
+ this.chairmanMerkleLatestVersions = {};
4102
+ this.chairmanMerkleNodeIds = {};
4103
+ this.chairmanMerkleVersionNums = {};
4090
4104
  this.entryMemoCids = {};
4091
4105
  this.entryNullifierNids = {};
4092
4106
  this.loadedMerkleLeaves.clear();
4093
- this.loadedMerkleTrees.clear();
4094
- this.loadedMerkleNodes.clear();
4107
+ this.loadedChairmanMerkleVersions.clear();
4108
+ this.loadedChairmanMerkleNodes.clear();
4095
4109
  this.loadedEntryMemos.clear();
4096
4110
  this.loadedEntryNullifiers.clear();
4097
4111
  }
@@ -4186,20 +4200,19 @@ var KeyValueStore = class {
4186
4200
  this.merkleLeafCids[key] = new Set(this.parseNumberIndex(cidsRaw));
4187
4201
  this.loadedMerkleLeaves.add(chainId);
4188
4202
  }
4189
- async ensureMerkleTreeLoaded(chainId) {
4190
- if (this.loadedMerkleTrees.has(chainId)) return;
4203
+ async ensureChairmanMerkleVersionsLoaded(chainId) {
4204
+ if (this.loadedChairmanMerkleVersions.has(chainId)) return;
4191
4205
  const key = String(chainId);
4192
- const raw = await this.options.client.get(this.sharedChainKey("merkleTrees", chainId));
4193
- const row = this.parseJson(raw, null);
4194
- if (row && typeof row === "object") this.merkleTrees[key] = row;
4195
- this.loadedMerkleTrees.add(chainId);
4206
+ const numsRaw = await this.options.client.get(this.sharedChainMetaKey("chairmanMerkleVersions", chainId));
4207
+ this.chairmanMerkleVersionNums[key] = new Set(this.parseNumberIndex(numsRaw));
4208
+ this.loadedChairmanMerkleVersions.add(chainId);
4196
4209
  }
4197
- async ensureMerkleNodesLoaded(chainId) {
4198
- if (this.loadedMerkleNodes.has(chainId)) return;
4210
+ async ensureChairmanMerkleNodesLoaded(chainId) {
4211
+ if (this.loadedChairmanMerkleNodes.has(chainId)) return;
4199
4212
  const key = String(chainId);
4200
- const idsRaw = await this.options.client.get(this.sharedChainMetaKey("merkleNodes", chainId));
4201
- this.merkleNodeIds[key] = new Set(this.parseStringIndex(idsRaw));
4202
- this.loadedMerkleNodes.add(chainId);
4213
+ const idsRaw = await this.options.client.get(this.sharedChainMetaKey("chairmanMerkleNodes", chainId));
4214
+ this.chairmanMerkleNodeIds[key] = new Set(this.parseStringIndex(idsRaw));
4215
+ this.loadedChairmanMerkleNodes.add(chainId);
4203
4216
  }
4204
4217
  async ensureEntryMemosLoaded(chainId) {
4205
4218
  if (this.loadedEntryMemos.has(chainId)) return;
@@ -4215,64 +4228,88 @@ var KeyValueStore = class {
4215
4228
  this.entryNullifierNids[key] = new Set(this.parseNumberIndex(nidsRaw));
4216
4229
  this.loadedEntryNullifiers.add(chainId);
4217
4230
  }
4218
- async getMerkleNode(chainId, id) {
4219
- await this.ensureMerkleNodesLoaded(chainId);
4220
- if (!this.merkleNodeIds[String(chainId)]?.has(id)) return void 0;
4221
- const raw = await this.options.client.get(this.sharedRecordKey("merkleNodes", chainId, id));
4231
+ async getChairmanMerkleNode(chainId, id) {
4232
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4233
+ if (!this.chairmanMerkleNodeIds[String(chainId)]?.has(id)) return void 0;
4234
+ const raw = await this.options.client.get(this.sharedRecordKey("chairmanMerkleNodes", chainId, id));
4222
4235
  const node = this.parseJson(raw, null);
4223
4236
  if (!node) return void 0;
4224
4237
  const hash = node.hash;
4225
4238
  if (typeof hash !== "string" || !hash.startsWith("0x")) return void 0;
4226
4239
  return { ...node, chainId };
4227
4240
  }
4228
- async upsertMerkleNodes(chainId, nodes) {
4241
+ async putChairmanMerkleNodes(chainId, nodes) {
4229
4242
  if (!nodes.length) return;
4230
- await this.ensureMerkleNodesLoaded(chainId);
4243
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4231
4244
  const key = String(chainId);
4232
- const ids = this.merkleNodeIds[key] ?? /* @__PURE__ */ new Set();
4245
+ const ids = this.chairmanMerkleNodeIds[key] ?? /* @__PURE__ */ new Set();
4233
4246
  const beforeSize = ids.size;
4234
4247
  for (const node of nodes) {
4235
4248
  ids.add(node.id);
4236
4249
  }
4237
- this.merkleNodeIds[key] = ids;
4250
+ this.chairmanMerkleNodeIds[key] = ids;
4238
4251
  await this.enqueueWrite(async () => {
4239
- await Promise.all(nodes.map((node) => this.writeJson(this.sharedRecordKey("merkleNodes", chainId, node.id), { ...node, chainId })));
4252
+ await Promise.all(nodes.map((node) => this.writeJson(this.sharedRecordKey("chairmanMerkleNodes", chainId, node.id), { ...node, chainId })));
4240
4253
  if (ids.size !== beforeSize) {
4241
- await this.writeJson(this.sharedChainMetaKey("merkleNodes", chainId), Array.from(ids));
4254
+ await this.writeJson(this.sharedChainMetaKey("chairmanMerkleNodes", chainId), Array.from(ids));
4242
4255
  }
4243
4256
  });
4244
4257
  }
4245
- async clearMerkleNodes(chainId) {
4246
- await this.ensureMerkleNodesLoaded(chainId);
4247
- const ids = Array.from(this.merkleNodeIds[String(chainId)] ?? []);
4248
- delete this.merkleNodeIds[String(chainId)];
4258
+ async getChairmanMerkleVersion(chainId, version) {
4259
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4260
+ if (!this.chairmanMerkleVersionNums[String(chainId)]?.has(version)) return void 0;
4261
+ const raw = await this.options.client.get(this.sharedRecordKey("chairmanMerkleVersions", chainId, version));
4262
+ const record = this.parseJson(raw, null);
4263
+ if (!record) return void 0;
4264
+ if (typeof record.rootHash !== "string" || !record.rootHash.startsWith("0x")) return void 0;
4265
+ if (typeof record.rootId !== "string") return void 0;
4266
+ const v = Number(record.version);
4267
+ if (!Number.isFinite(v) || v < 0) return void 0;
4268
+ return { chainId, version: Math.floor(v), rootId: record.rootId, rootHash: record.rootHash };
4269
+ }
4270
+ async getLatestChairmanMerkleVersion(chainId) {
4271
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4272
+ const nums = this.chairmanMerkleVersionNums[String(chainId)];
4273
+ if (!nums || nums.size === 0) return void 0;
4274
+ const maxVersion = Math.max(...nums);
4275
+ return this.getChairmanMerkleVersion(chainId, maxVersion);
4276
+ }
4277
+ async putChairmanMerkleVersion(chainId, record) {
4278
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4279
+ const key = String(chainId);
4280
+ const nums = this.chairmanMerkleVersionNums[key] ?? /* @__PURE__ */ new Set();
4281
+ const beforeSize = nums.size;
4282
+ nums.add(record.version);
4283
+ this.chairmanMerkleVersionNums[key] = nums;
4284
+ const current = this.chairmanMerkleLatestVersions[key];
4285
+ if (!current || record.version >= current.version) {
4286
+ this.chairmanMerkleLatestVersions[key] = { ...record, chainId };
4287
+ }
4288
+ const row = { ...record, chainId };
4249
4289
  await this.enqueueWrite(async () => {
4250
- await Promise.all(ids.map((id) => this.deleteOrReset(this.sharedRecordKey("merkleNodes", chainId, id), null)));
4251
- await this.deleteOrReset(this.sharedChainMetaKey("merkleNodes", chainId), []);
4290
+ await this.writeJson(this.sharedRecordKey("chairmanMerkleVersions", chainId, record.version), row);
4291
+ if (nums.size !== beforeSize) {
4292
+ await this.writeJson(this.sharedChainMetaKey("chairmanMerkleVersions", chainId), Array.from(nums).sort((a, b) => a - b));
4293
+ }
4252
4294
  });
4253
4295
  }
4254
- async getMerkleTree(chainId) {
4255
- await this.ensureMerkleTreeLoaded(chainId);
4256
- const row = this.merkleTrees[String(chainId)];
4257
- if (!row) return void 0;
4258
- const totalElements = Number(row.totalElements);
4259
- const lastUpdated = Number(row.lastUpdated);
4260
- const root = row.root;
4261
- if (typeof root !== "string" || !root.startsWith("0x")) return void 0;
4262
- if (!Number.isFinite(totalElements) || totalElements < 0) return void 0;
4263
- return { chainId, root, totalElements: Math.floor(totalElements), lastUpdated: Number.isFinite(lastUpdated) ? Math.floor(lastUpdated) : 0 };
4264
- }
4265
- async setMerkleTree(chainId, tree) {
4266
- await this.ensureMerkleTreeLoaded(chainId);
4267
- const row = { ...tree, chainId };
4268
- this.merkleTrees[String(chainId)] = row;
4269
- await this.enqueueWrite(() => this.writeJson(this.sharedChainKey("merkleTrees", chainId), row));
4270
- }
4271
- async clearMerkleTree(chainId) {
4272
- await this.ensureMerkleTreeLoaded(chainId);
4273
- delete this.merkleTrees[String(chainId)];
4296
+ async clearChairmanMerkleTree(chainId) {
4297
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4298
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4299
+ const nodeIds = Array.from(this.chairmanMerkleNodeIds[String(chainId)] ?? []);
4300
+ const versionNums = Array.from(this.chairmanMerkleVersionNums[String(chainId)] ?? []);
4301
+ delete this.chairmanMerkleNodeIds[String(chainId)];
4302
+ delete this.chairmanMerkleVersionNums[String(chainId)];
4303
+ delete this.chairmanMerkleLatestVersions[String(chainId)];
4274
4304
  await this.enqueueWrite(async () => {
4275
- await this.deleteOrReset(this.sharedChainKey("merkleTrees", chainId), null);
4305
+ await Promise.all([
4306
+ ...nodeIds.map((id) => this.deleteOrReset(this.sharedRecordKey("chairmanMerkleNodes", chainId, id), null)),
4307
+ ...versionNums.map((v) => this.deleteOrReset(this.sharedRecordKey("chairmanMerkleVersions", chainId, v), null))
4308
+ ]);
4309
+ await Promise.all([
4310
+ this.deleteOrReset(this.sharedChainMetaKey("chairmanMerkleNodes", chainId), []),
4311
+ this.deleteOrReset(this.sharedChainMetaKey("chairmanMerkleVersions", chainId), [])
4312
+ ]);
4276
4313
  });
4277
4314
  }
4278
4315
  async upsertEntryMemos(memos) {
@@ -7116,6 +7153,7 @@ var MerkleEngine = class _MerkleEngine {
7116
7153
  this.hydrateInFlight = /* @__PURE__ */ new Map();
7117
7154
  this.mode = options?.mode ?? "hybrid";
7118
7155
  this.treeDepth = Math.max(1, Math.floor(options?.treeDepth ?? TREE_DEPTH_DEFAULT));
7156
+ this.readContractRoot = options?.readContractRoot;
7119
7157
  }
7120
7158
  /**
7121
7159
  * Compute the current merkle root index from total elements.
@@ -7124,9 +7162,6 @@ var MerkleEngine = class _MerkleEngine {
7124
7162
  if (totalElements <= tempArraySize) return 0;
7125
7163
  return Math.floor((totalElements - 1) / tempArraySize);
7126
7164
  }
7127
- /**
7128
- * Get or initialize the pending leaf buffer for a chain.
7129
- */
7130
7165
  ensurePendingLeaves(chainId) {
7131
7166
  let pending = this.pendingLeavesByChain.get(chainId);
7132
7167
  if (!pending) {
@@ -7135,9 +7170,6 @@ var MerkleEngine = class _MerkleEngine {
7135
7170
  }
7136
7171
  return pending;
7137
7172
  }
7138
- /**
7139
- * Get or initialize chain-level merkle state.
7140
- */
7141
7173
  ensureChainState(chainId) {
7142
7174
  let state = this.chainStateByChain.get(chainId);
7143
7175
  if (!state) {
@@ -7146,15 +7178,32 @@ var MerkleEngine = class _MerkleEngine {
7146
7178
  }
7147
7179
  return state;
7148
7180
  }
7149
- /**
7150
- * Poseidon2 merkle hash for a left/right pair.
7151
- */
7181
+ // ── Hashing ──
7152
7182
  static hashPair(left, right) {
7153
7183
  return Poseidon2.hashToHex(BigInt(left), BigInt(right), Poseidon2Domain.Merkle);
7154
7184
  }
7185
+ static normalizeHex32(value, name) {
7186
+ try {
7187
+ const bi = BigInt(value);
7188
+ if (bi < 0n) throw new Error("negative");
7189
+ const hex = bi.toString(16).padStart(64, "0");
7190
+ if (hex.length > 64) throw new Error("too_large");
7191
+ return `0x${hex}`;
7192
+ } catch (error) {
7193
+ throw new SdkError("MERKLE", `Invalid ${name}`, { value }, error);
7194
+ }
7195
+ }
7196
+ // ── Static helpers ──
7197
+ static totalElementsInTree(totalElements, tempArraySize = TEMP_ARRAY_SIZE_DEFAULT) {
7198
+ if (tempArraySize <= 0) throw new SdkError("MERKLE", "tempArraySize must be greater than zero", { tempArraySize });
7199
+ if (totalElements <= 0n) return 0;
7200
+ const size = BigInt(tempArraySize);
7201
+ return Number((totalElements - 1n) / size * size);
7202
+ }
7203
+ // ── Subtree (levels 0-5, 32 leaves → 1 root) ──
7155
7204
  /**
7156
7205
  * Build a fixed-depth subtree from 32 contiguous leaves.
7157
- * Returns the subtree root and all intermediate nodes for storage.
7206
+ * Returns the subtree root hash and all intermediate nodes for storage.
7158
7207
  */
7159
7208
  static buildSubtree(leafCommitments, baseIndex) {
7160
7209
  if (leafCommitments.length !== SUBTREE_SIZE) {
@@ -7175,71 +7224,71 @@ var MerkleEngine = class _MerkleEngine {
7175
7224
  const position = basePos + i;
7176
7225
  nodesToStore.push({
7177
7226
  chainId: 0,
7178
- id: `${level}-${position}`,
7179
- level,
7180
- position,
7181
- hash: next[i]
7227
+ id: `st-${level}-${position}`,
7228
+ hash: next[i],
7229
+ leftId: null,
7230
+ rightId: null
7182
7231
  });
7183
7232
  }
7184
7233
  currentLevel = next;
7185
7234
  }
7186
7235
  return { subtreeRoot: currentLevel[0], nodesToStore };
7187
7236
  }
7237
+ // ── ChairmanMerkle tree (persistent segment tree, levels 5-32) ──
7188
7238
  /**
7189
- * Fetch a node hash from storage if available.
7190
- */
7191
- async getNodeHash(chainId, id) {
7192
- const node = await this.storage?.getMerkleNode?.(chainId, id);
7193
- return node?.hash;
7194
- }
7195
- /**
7196
- * Merge a completed subtree root into the main tree, updating frontier nodes.
7197
- */
7198
- async mergeSubtreeToMainTree(input) {
7199
- let currentValue = input.subtreeRoot;
7200
- let frontierUpdated = false;
7201
- const nodesToStore = [];
7202
- for (let level = SUBTREE_DEPTH; level < this.treeDepth; level++) {
7203
- const nodeIndex = input.newTotalElements - 1 >> level;
7204
- if ((nodeIndex & 1) === 0) {
7205
- if (!frontierUpdated) {
7206
- nodesToStore.push({
7207
- chainId: input.chainId,
7208
- id: `frontier-${level}`,
7209
- level,
7210
- position: nodeIndex,
7211
- hash: currentValue
7212
- });
7213
- frontierUpdated = true;
7239
+ * Insert a subtree root into the persistent main tree.
7240
+ *
7241
+ * Top-down recursive: descends from root (level treeDepth) to the target
7242
+ * leaf position (level SUBTREE_DEPTH). At each level only the node on the
7243
+ * update path is newly created; the sibling is shared from the previous
7244
+ * version's tree.
7245
+ *
7246
+ * @returns new root node ID/hash and all newly created nodes.
7247
+ */
7248
+ async insertSubtreeRoot(chainId, prevRootId, subtreeRootHash, batchIndex, version) {
7249
+ const MAIN_DEPTH = this.treeDepth - SUBTREE_DEPTH;
7250
+ const nodes = [];
7251
+ const descend = async (nodeId, depth) => {
7252
+ const originalLevel = this.treeDepth - depth;
7253
+ if (depth === MAIN_DEPTH) {
7254
+ const newId2 = `cm-${version}-${originalLevel}`;
7255
+ nodes.push({ chainId, id: newId2, hash: subtreeRootHash, leftId: null, rightId: null });
7256
+ return { id: newId2, hash: subtreeRootHash };
7257
+ }
7258
+ let prevLeftId = null;
7259
+ let prevRightId = null;
7260
+ if (nodeId) {
7261
+ const prevNode = await this.storage?.getChairmanMerkleNode?.(chainId, nodeId);
7262
+ if (prevNode) {
7263
+ prevLeftId = prevNode.leftId;
7264
+ prevRightId = prevNode.rightId;
7214
7265
  }
7215
- currentValue = _MerkleEngine.hashPair(currentValue, getZeroHash(level));
7216
- } else {
7217
- const leftHash = await this.getNodeHash(input.chainId, `frontier-${level}`) ?? getZeroHash(level);
7218
- currentValue = _MerkleEngine.hashPair(leftHash, currentValue);
7219
7266
  }
7220
- const nextLevel = level + 1;
7221
- nodesToStore.push({
7222
- chainId: input.chainId,
7223
- id: `${nextLevel}-${nodeIndex >> 1}`,
7224
- level: nextLevel,
7225
- position: nodeIndex >> 1,
7226
- hash: currentValue
7227
- });
7228
- }
7229
- return { finalRoot: currentValue, nodesToStore };
7230
- }
7231
- /**
7232
- * Convert on-chain totalElements to the count of fully merged elements.
7233
- */
7234
- static totalElementsInTree(totalElements, tempArraySize = TEMP_ARRAY_SIZE_DEFAULT) {
7235
- if (tempArraySize <= 0) throw new SdkError("MERKLE", "tempArraySize must be greater than zero", { tempArraySize });
7236
- if (totalElements <= 0n) return 0;
7237
- const size = BigInt(tempArraySize);
7238
- return Number((totalElements - 1n) / size * size);
7267
+ const childLevel = originalLevel - 1;
7268
+ const remainingDepth = MAIN_DEPTH - depth - 1;
7269
+ const goRight = (batchIndex >> remainingDepth & 1) === 1;
7270
+ let leftResult;
7271
+ let rightResult;
7272
+ if (goRight) {
7273
+ const leftHash = prevLeftId ? (await this.storage?.getChairmanMerkleNode?.(chainId, prevLeftId))?.hash ?? getZeroHash(childLevel) : getZeroHash(childLevel);
7274
+ leftResult = { id: prevLeftId, hash: leftHash };
7275
+ const right = await descend(prevRightId, depth + 1);
7276
+ rightResult = { id: right.id, hash: right.hash };
7277
+ } else {
7278
+ const left = await descend(prevLeftId, depth + 1);
7279
+ leftResult = { id: left.id, hash: left.hash };
7280
+ const rightHash = prevRightId ? (await this.storage?.getChairmanMerkleNode?.(chainId, prevRightId))?.hash ?? getZeroHash(childLevel) : getZeroHash(childLevel);
7281
+ rightResult = { id: prevRightId, hash: rightHash };
7282
+ }
7283
+ const hash = _MerkleEngine.hashPair(leftResult.hash, rightResult.hash);
7284
+ const newId = `cm-${version}-${originalLevel}`;
7285
+ nodes.push({ chainId, id: newId, hash, leftId: leftResult.id, rightId: rightResult.id });
7286
+ return { id: newId, hash };
7287
+ };
7288
+ const root = await descend(prevRootId, 0);
7289
+ return { rootId: root.id, rootHash: root.hash, nodes };
7239
7290
  }
7240
- /**
7241
- * Hydrate local merkle state from storage on first use.
7242
- */
7291
+ // ── Hydration ──
7243
7292
  async hydrateFromStorage(chainId) {
7244
7293
  if (this.mode === "remote") return;
7245
7294
  if (this.hydratedChains.has(chainId)) return;
@@ -7248,31 +7297,17 @@ var MerkleEngine = class _MerkleEngine {
7248
7297
  const task = (async () => {
7249
7298
  try {
7250
7299
  const state = this.ensureChainState(chainId);
7251
- const leaves = await this.storage?.getMerkleLeaves?.(chainId);
7252
- if (!leaves || leaves.length === 0) return;
7253
- const sorted = [...leaves].map((l) => ({
7254
- cid: l.cid,
7255
- commitment: _MerkleEngine.normalizeHex32(l.commitment, "memo.commitment")
7256
- })).sort((a, b) => a.cid - b.cid);
7257
- for (let i = 0; i < sorted.length; i++) {
7258
- if (sorted[i].cid !== i) throw new Error(`Non-contiguous persisted merkle leaves: expected cid=${i}, got cid=${sorted[i].cid}`);
7259
- }
7260
- const storedTree = await this.storage?.getMerkleTree?.(chainId);
7261
- const leafCount = sorted.length;
7262
- const mergedFromLeaves = _MerkleEngine.totalElementsInTree(BigInt(leafCount));
7263
- const mergedFromTree = typeof storedTree?.totalElements === "number" && storedTree.totalElements > 0 ? storedTree.totalElements : 0;
7264
- const mergedElements = Math.max(mergedFromLeaves, mergedFromTree);
7265
7300
  const pending = this.ensurePendingLeaves(chainId);
7266
- pending.length = 0;
7267
- state.mergedElements = mergedElements;
7268
- if (leafCount > mergedElements) {
7269
- pending.push(...sorted.slice(mergedElements).map((l) => l.commitment));
7301
+ const latest = await this.storage?.getLatestChairmanMerkleVersion?.(chainId);
7302
+ if (latest) {
7303
+ state.mergedElements = latest.version;
7304
+ state.root = _MerkleEngine.normalizeHex32(latest.rootHash, "chairmanMerkleVersion.rootHash");
7270
7305
  }
7271
- if (storedTree?.root) {
7272
- state.root = _MerkleEngine.normalizeHex32(storedTree.root, "merkleTree.root");
7273
- } else {
7274
- const rootNode = await this.storage?.getMerkleNode?.(chainId, `${this.treeDepth}-0`);
7275
- state.root = rootNode?.hash ?? getZeroHash(this.treeDepth);
7306
+ const leaves = await this.storage?.getMerkleLeaves?.(chainId);
7307
+ if (leaves && leaves.length > state.mergedElements) {
7308
+ const sorted = [...leaves].sort((a, b) => a.cid - b.cid).slice(state.mergedElements);
7309
+ pending.length = 0;
7310
+ pending.push(...sorted.map((l) => _MerkleEngine.normalizeHex32(l.commitment, "leaf.commitment")));
7276
7311
  }
7277
7312
  } catch (error) {
7278
7313
  if (this.mode === "hybrid") return;
@@ -7285,26 +7320,7 @@ var MerkleEngine = class _MerkleEngine {
7285
7320
  this.hydrateInFlight.set(chainId, task);
7286
7321
  return task;
7287
7322
  }
7288
- /**
7289
- * Normalize unknown values to a 32-byte hex string.
7290
- */
7291
- static normalizeHex32(value, name) {
7292
- try {
7293
- const bi = BigInt(value);
7294
- if (bi < 0n) throw new Error("negative");
7295
- const hex = bi.toString(16).padStart(64, "0");
7296
- if (hex.length > 64) throw new Error("too_large");
7297
- return `0x${hex}`;
7298
- } catch (error) {
7299
- throw new SdkError("MERKLE", `Invalid ${name}`, { value }, error);
7300
- }
7301
- }
7302
- /**
7303
- * Feed contiguous (cid-ordered) memo leaves into the local merkle tree.
7304
- *
7305
- * This mirrors the client/app behavior: only after we have a full consecutive batch of 32 leaves
7306
- * do we merge them into the main tree. Leaves that are still in the buffer do not get local proofs.
7307
- */
7323
+ // ── Ingestion ──
7308
7324
  async ingestEntryMemos(chainId, memos) {
7309
7325
  if (this.mode === "remote") return;
7310
7326
  await this.hydrateFromStorage(chainId);
@@ -7332,26 +7348,42 @@ var MerkleEngine = class _MerkleEngine {
7332
7348
  expected++;
7333
7349
  while (pending.length >= SUBTREE_SIZE) {
7334
7350
  const batch = pending.splice(0, SUBTREE_SIZE);
7335
- const baseIndex = state.mergedElements;
7336
- const subtree = _MerkleEngine.buildSubtree(batch, baseIndex);
7337
- const merged = await this.mergeSubtreeToMainTree({ chainId, subtreeRoot: subtree.subtreeRoot, newTotalElements: baseIndex + SUBTREE_SIZE });
7338
- state.mergedElements += SUBTREE_SIZE;
7339
- state.root = merged.finalRoot;
7340
- const checkpointNode = {
7341
- chainId,
7342
- id: `checkpoint-${state.mergedElements}`,
7343
- level: -1,
7344
- position: 0,
7345
- hash: state.root
7346
- };
7347
- const nodes = [...subtree.nodesToStore, ...merged.nodesToStore, checkpointNode].map((n) => ({ ...n, chainId }));
7348
- await this.storage?.upsertMerkleNodes?.(chainId, nodes);
7349
- await this.storage?.setMerkleTree?.(chainId, {
7351
+ const batchIndex = state.mergedElements / SUBTREE_SIZE;
7352
+ const subtree = _MerkleEngine.buildSubtree(batch, state.mergedElements);
7353
+ const subtreeNodes = subtree.nodesToStore.map((n) => ({ ...n, chainId }));
7354
+ const prevVersion = await this.storage?.getLatestChairmanMerkleVersion?.(chainId);
7355
+ const prevRootId = prevVersion?.rootId ?? null;
7356
+ const newVersion = state.mergedElements + SUBTREE_SIZE;
7357
+ const result = await this.insertSubtreeRoot(chainId, prevRootId, subtree.subtreeRoot, batchIndex, newVersion);
7358
+ if (this.readContractRoot) {
7359
+ const rootIndex = newVersion / SUBTREE_SIZE;
7360
+ const onChainRoot = await this.readContractRoot(chainId, rootIndex).catch(() => null);
7361
+ if (onChainRoot !== null) {
7362
+ const onChainNorm = _MerkleEngine.normalizeHex32(onChainRoot, "onChainRoot");
7363
+ const isZero = BigInt(onChainNorm) === 0n;
7364
+ if (!isZero && onChainNorm !== result.rootHash) {
7365
+ const target = state.mergedElements;
7366
+ await this.rollback(chainId, target);
7367
+ throw new SdkError("MERKLE", "Local merkle root mismatch with on-chain root \u2014 rolled back", {
7368
+ chainId,
7369
+ rootIndex,
7370
+ localRoot: result.rootHash,
7371
+ onChainRoot: onChainNorm,
7372
+ version: newVersion,
7373
+ rollbackTarget: target
7374
+ });
7375
+ }
7376
+ }
7377
+ }
7378
+ await this.storage?.putChairmanMerkleNodes?.(chainId, [...subtreeNodes, ...result.nodes]);
7379
+ await this.storage?.putChairmanMerkleVersion?.(chainId, {
7350
7380
  chainId,
7351
- root: state.root,
7352
- totalElements: state.mergedElements,
7353
- lastUpdated: Date.now()
7381
+ version: newVersion,
7382
+ rootId: result.rootId,
7383
+ rootHash: result.rootHash
7354
7384
  });
7385
+ state.mergedElements = newVersion;
7386
+ state.root = result.rootHash;
7355
7387
  }
7356
7388
  }
7357
7389
  this.hydratedChains.add(chainId);
@@ -7362,15 +7394,59 @@ var MerkleEngine = class _MerkleEngine {
7362
7394
  throw new SdkError("MERKLE", "Failed to ingest local merkle leaves", { chainId, leafCount: leaves.length }, error);
7363
7395
  }
7364
7396
  }
7397
+ // ── Rollback (tree O(1) + sync cursor reset) ──
7365
7398
  /**
7366
- * Convenience wrapper to request a single proof.
7399
+ * Unified rollback: rewind tree to a previous batch boundary AND reset the
7400
+ * sync cursor so memo sync restarts from the same point.
7401
+ *
7402
+ * What gets rolled back:
7403
+ * - ChairmanMerkle tree version pointer (O(1) — old nodes still in storage)
7404
+ * - Pending leaves buffer (cleared)
7405
+ * - Sync cursor: memo + merkle fields (nullifier left unchanged — independent)
7406
+ *
7407
+ * @param targetMergedElements Must be a non-negative multiple of 32.
7408
+ * Pass 0 to reset to the empty tree.
7409
+ * @returns true if rollback succeeded, false if the target version doesn't exist.
7367
7410
  */
7368
- async getProofByCid(input) {
7369
- return this.getProofByCids({ chainId: input.chainId, cids: [input.cid], totalElements: input.totalElements });
7411
+ async rollback(chainId, targetMergedElements) {
7412
+ if (targetMergedElements < 0 || targetMergedElements % SUBTREE_SIZE !== 0) {
7413
+ throw new SdkError("MERKLE", "rollback target must be a non-negative multiple of 32", { targetMergedElements });
7414
+ }
7415
+ const state = this.ensureChainState(chainId);
7416
+ const pending = this.ensurePendingLeaves(chainId);
7417
+ if (targetMergedElements === 0) {
7418
+ state.mergedElements = 0;
7419
+ state.root = getZeroHash(this.treeDepth);
7420
+ pending.length = 0;
7421
+ await this.resetSyncCursor(chainId, 0);
7422
+ return true;
7423
+ }
7424
+ const version = await this.storage?.getChairmanMerkleVersion?.(chainId, targetMergedElements);
7425
+ if (!version) return false;
7426
+ state.mergedElements = targetMergedElements;
7427
+ state.root = _MerkleEngine.normalizeHex32(version.rootHash, "version.rootHash");
7428
+ pending.length = 0;
7429
+ this.hydratedChains.add(chainId);
7430
+ await this.resetSyncCursor(chainId, targetMergedElements);
7431
+ return true;
7370
7432
  }
7371
7433
  /**
7372
- * Get merkle proofs for a set of cids using local/hybrid/remote logic.
7434
+ * Reset the sync cursor's memo field to `targetMemo` (and derive merkle cursor),
7435
+ * but only if the current cursor is ahead of the target.
7436
+ * Nullifier cursor is left unchanged — nullifiers are independent of tree state.
7373
7437
  */
7438
+ async resetSyncCursor(chainId, targetMemo) {
7439
+ if (!this.storage?.getSyncCursor || !this.storage?.setSyncCursor) return;
7440
+ const cursor = await this.storage.getSyncCursor(chainId);
7441
+ if (!cursor || cursor.memo <= targetMemo) return;
7442
+ cursor.memo = targetMemo;
7443
+ cursor.merkle = this.currentMerkleRootIndex(targetMemo);
7444
+ await this.storage.setSyncCursor(chainId, cursor);
7445
+ }
7446
+ // ── Proof generation ──
7447
+ async getProofByCid(input) {
7448
+ return this.getProofByCids({ chainId: input.chainId, cids: [input.cid], totalElements: input.totalElements });
7449
+ }
7374
7450
  async getProofByCids(input) {
7375
7451
  const cids = [...input.cids];
7376
7452
  if (cids.length === 0) throw new SdkError("MERKLE", "No cids provided", { chainId: input.chainId });
@@ -7385,15 +7461,16 @@ var MerkleEngine = class _MerkleEngine {
7385
7461
  await this.hydrateFromStorage(input.chainId);
7386
7462
  const canUseLocal = this.mode !== "remote";
7387
7463
  if (canUseLocal) {
7388
- const tree = await this.storage?.getMerkleTree?.(input.chainId);
7389
- const hasDb = typeof this.storage?.getMerkleLeaf === "function" && typeof this.storage?.getMerkleNode === "function" && typeof tree?.totalElements === "number" && typeof tree?.root === "string";
7390
- if (hasDb && tree) {
7391
- if (tree.totalElements < contractTreeElements) {
7464
+ const version = contractTreeElements > 0 ? await this.storage?.getChairmanMerkleVersion?.(input.chainId, contractTreeElements) : void 0;
7465
+ const hasDb = typeof this.storage?.getMerkleLeaf === "function" && typeof this.storage?.getChairmanMerkleNode === "function" && (contractTreeElements === 0 || !!version);
7466
+ if (hasDb) {
7467
+ const state = this.ensureChainState(input.chainId);
7468
+ if (contractTreeElements > 0 && state.mergedElements < contractTreeElements) {
7392
7469
  if (this.mode === "local") {
7393
7470
  throw new SdkError("MERKLE", "Local merkle db is behind contract", {
7394
7471
  chainId: input.chainId,
7395
7472
  cids,
7396
- localTotalElements: tree.totalElements,
7473
+ localMergedElements: state.mergedElements,
7397
7474
  contractTreeElements
7398
7475
  });
7399
7476
  }
@@ -7405,30 +7482,13 @@ var MerkleEngine = class _MerkleEngine {
7405
7482
  proof.push({ leaf_index: cid, path: new Array(this.treeDepth + 1).fill("0") });
7406
7483
  continue;
7407
7484
  }
7408
- const leaf = await this.storage.getMerkleLeaf(input.chainId, cid);
7409
- if (!leaf) throw new Error(`missing_leaf:${cid}`);
7410
- const path = [leaf.commitment];
7411
- for (let level = 1; level <= this.treeDepth; level++) {
7412
- const siblingIndex = cid >> level - 1 ^ 1;
7413
- if (level === 1) {
7414
- const siblingLeaf = await this.storage.getMerkleLeaf(input.chainId, siblingIndex);
7415
- path.push(siblingLeaf?.commitment ?? getZeroHash(0));
7416
- continue;
7417
- }
7418
- const targetLevel = level - 1;
7419
- const siblingNode = await this.storage.getMerkleNode(input.chainId, `${targetLevel}-${siblingIndex}`);
7420
- path.push(siblingNode?.hash ?? getZeroHash(targetLevel));
7421
- }
7485
+ const path = await this.buildLocalProofPath(input.chainId, cid, version);
7422
7486
  proof.push({ leaf_index: cid, path });
7423
7487
  }
7424
- let effectiveRoot = tree.root;
7425
- if (tree.totalElements > contractTreeElements && contractTreeElements > 0) {
7426
- const checkpoint = await this.storage.getMerkleNode(input.chainId, `checkpoint-${contractTreeElements}`);
7427
- if (checkpoint) effectiveRoot = checkpoint.hash;
7428
- }
7488
+ const effectiveRoot = contractTreeElements > 0 ? _MerkleEngine.normalizeHex32(version.rootHash, "version.rootHash") : getZeroHash(this.treeDepth);
7429
7489
  return {
7430
7490
  proof,
7431
- merkle_root: _MerkleEngine.normalizeHex32(effectiveRoot, "merkleTree.root"),
7491
+ merkle_root: effectiveRoot,
7432
7492
  latest_cid: totalElements > 0n ? Number(totalElements - 1n) : -1
7433
7493
  };
7434
7494
  } catch (error) {
@@ -7438,7 +7498,7 @@ var MerkleEngine = class _MerkleEngine {
7438
7498
  }
7439
7499
  }
7440
7500
  } else if (this.mode === "local" && needsTreeProof.length) {
7441
- throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter_merkle_db" });
7501
+ throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter_or_version" });
7442
7502
  }
7443
7503
  }
7444
7504
  if (needsTreeProof.length === 0) {
@@ -7463,15 +7523,64 @@ var MerkleEngine = class _MerkleEngine {
7463
7523
  };
7464
7524
  }
7465
7525
  /**
7466
- * Fetch a remote merkle root (used when no proofs are needed).
7526
+ * Build a local proof path by traversing the chairmanMerkle tree.
7527
+ *
7528
+ * Levels 0-4: sibling hashes from subtree internal nodes (st-{level}-{pos}).
7529
+ * Levels 5-31: sibling hashes from chairmanMerkle tree traversal (top-down from version root).
7467
7530
  */
7531
+ async buildLocalProofPath(chainId, cid, version) {
7532
+ const leaf = await this.storage.getMerkleLeaf(chainId, cid);
7533
+ if (!leaf) throw new Error(`missing_leaf:${cid}`);
7534
+ const path = [leaf.commitment];
7535
+ for (let level = 1; level <= SUBTREE_DEPTH; level++) {
7536
+ const siblingPos = cid >> level - 1 ^ 1;
7537
+ if (level === 1) {
7538
+ const siblingLeaf = await this.storage.getMerkleLeaf(chainId, siblingPos);
7539
+ path.push(siblingLeaf?.commitment ?? getZeroHash(0));
7540
+ } else {
7541
+ const targetLevel = level - 1;
7542
+ const node = await this.storage.getChairmanMerkleNode(chainId, `st-${targetLevel}-${siblingPos}`);
7543
+ path.push(node?.hash ?? getZeroHash(targetLevel));
7544
+ }
7545
+ }
7546
+ const batchIndex = cid >> SUBTREE_DEPTH;
7547
+ const MAIN_DEPTH = this.treeDepth - SUBTREE_DEPTH;
7548
+ const mainSiblings = [];
7549
+ let nodeId = version.rootId;
7550
+ for (let depth = 0; depth < MAIN_DEPTH; depth++) {
7551
+ const childLevel = this.treeDepth - depth - 1;
7552
+ if (!nodeId) {
7553
+ mainSiblings.push(getZeroHash(childLevel));
7554
+ continue;
7555
+ }
7556
+ const node = await this.storage.getChairmanMerkleNode(chainId, nodeId);
7557
+ if (!node) {
7558
+ mainSiblings.push(getZeroHash(childLevel));
7559
+ nodeId = null;
7560
+ continue;
7561
+ }
7562
+ const remainingDepth = MAIN_DEPTH - depth - 1;
7563
+ const goRight = (batchIndex >> remainingDepth & 1) === 1;
7564
+ if (goRight) {
7565
+ const leftNode = node.leftId ? await this.storage.getChairmanMerkleNode(chainId, node.leftId) : null;
7566
+ mainSiblings.push(leftNode?.hash ?? getZeroHash(childLevel));
7567
+ nodeId = node.rightId;
7568
+ } else {
7569
+ const rightNode = node.rightId ? await this.storage.getChairmanMerkleNode(chainId, node.rightId) : null;
7570
+ mainSiblings.push(rightNode?.hash ?? getZeroHash(childLevel));
7571
+ nodeId = node.leftId;
7572
+ }
7573
+ }
7574
+ for (let i = mainSiblings.length - 1; i >= 0; i--) {
7575
+ path.push(mainSiblings[i]);
7576
+ }
7577
+ return path;
7578
+ }
7579
+ // ── Remote helpers ──
7468
7580
  async fetchRemoteRootOnly(chainId) {
7469
7581
  const remote = await this.fetchRemoteProofFromService({ chainId, cids: [0] });
7470
7582
  return _MerkleEngine.normalizeHex32(remote.merkle_root, "remote.merkle_root");
7471
7583
  }
7472
- /**
7473
- * Fetch proofs from the remote merkle service.
7474
- */
7475
7584
  async fetchRemoteProofFromService(input) {
7476
7585
  const chain = this.getChain(input.chainId);
7477
7586
  if (!chain.merkleProofUrl) {
@@ -7480,9 +7589,7 @@ var MerkleEngine = class _MerkleEngine {
7480
7589
  const client = new MerkleClient(chain.merkleProofUrl);
7481
7590
  return client.getProofByCids(input.cids);
7482
7591
  }
7483
- /**
7484
- * Build membership witnesses for provided UTXOs from a remote proof response.
7485
- */
7592
+ // ── Witness builders (unchanged) ──
7486
7593
  buildAccMemberWitnesses(input) {
7487
7594
  return input.utxos.map((utxo, idx) => {
7488
7595
  const remoteProof = input.remote.proof[idx];
@@ -7496,9 +7603,6 @@ var MerkleEngine = class _MerkleEngine {
7496
7603
  };
7497
7604
  });
7498
7605
  }
7499
- /**
7500
- * Convert UTXOs into circuit input secrets, decrypting memos and padding if needed.
7501
- */
7502
7606
  async buildInputSecretsFromUtxos(input) {
7503
7607
  if (!Array.isArray(input.utxos) || input.utxos.length === 0) {
7504
7608
  throw new SdkError("MERKLE", "No utxos provided", { count: 0 });
@@ -8642,7 +8746,7 @@ var _IndexedDbStore = class _IndexedDbStore {
8642
8746
  this.options = options;
8643
8747
  this.cursors = /* @__PURE__ */ new Map();
8644
8748
  this.operations = [];
8645
- this.merkleTrees = {};
8749
+ this.chairmanMerkleLatestVersions = {};
8646
8750
  this.db = null;
8647
8751
  const max = options.maxOperations;
8648
8752
  this.maxOperations = max == null ? Number.POSITIVE_INFINITY : Math.max(0, Math.floor(max));
@@ -8686,8 +8790,8 @@ var _IndexedDbStore = class _IndexedDbStore {
8686
8790
  { name: `${base}:entryMemos`, keyPath: ["chainId", "cid"], indexes: [{ name: "chainId", keyPath: "chainId" }] },
8687
8791
  { name: `${base}:entryNullifiers`, keyPath: ["chainId", "nid"], indexes: [{ name: "chainId", keyPath: "chainId" }] },
8688
8792
  { name: `${base}:merkleLeaves`, keyPath: ["chainId", "cid"], indexes: [{ name: "chainId", keyPath: "chainId" }] },
8689
- { name: `${base}:merkleTrees`, keyPath: "chainId" },
8690
- { name: `${base}:merkleNodes`, keyPath: ["chainId", "id"], indexes: [{ name: "chainId", keyPath: "chainId" }] }
8793
+ { name: `${base}:chairmanMerkleNodes`, keyPath: ["chainId", "id"], indexes: [{ name: "chainId", keyPath: "chainId" }] },
8794
+ { name: `${base}:chairmanMerkleVersions`, keyPath: ["chainId", "version"], indexes: [{ name: "chainId", keyPath: "chainId" }] }
8691
8795
  ];
8692
8796
  }
8693
8797
  async openDb() {
@@ -8701,6 +8805,12 @@ var _IndexedDbStore = class _IndexedDbStore {
8701
8805
  req.onerror = () => reject(req.error ?? new Error("indexedDB open failed"));
8702
8806
  req.onupgradeneeded = () => {
8703
8807
  const db2 = req.result;
8808
+ const defNames = new Set(defs.map((d) => d.name));
8809
+ for (const storeName of Array.from(db2.objectStoreNames)) {
8810
+ if (!defNames.has(storeName)) {
8811
+ db2.deleteObjectStore(storeName);
8812
+ }
8813
+ }
8704
8814
  for (const def of defs) {
8705
8815
  let store = null;
8706
8816
  if (!db2.objectStoreNames.contains(def.name)) {
@@ -8732,8 +8842,8 @@ var _IndexedDbStore = class _IndexedDbStore {
8732
8842
  entryMemos: `${base}:entryMemos`,
8733
8843
  entryNullifiers: `${base}:entryNullifiers`,
8734
8844
  merkleLeaves: `${base}:merkleLeaves`,
8735
- merkleTrees: `${base}:merkleTrees`,
8736
- merkleNodes: `${base}:merkleNodes`
8845
+ chairmanMerkleNodes: `${base}:chairmanMerkleNodes`,
8846
+ chairmanMerkleVersions: `${base}:chairmanMerkleVersions`
8737
8847
  };
8738
8848
  }
8739
8849
  async getAll(storeName) {
@@ -8870,7 +8980,7 @@ var _IndexedDbStore = class _IndexedDbStore {
8870
8980
  const walletKey = this.walletKey();
8871
8981
  this.cursors.clear();
8872
8982
  this.operations = [];
8873
- this.merkleTrees = {};
8983
+ this.chairmanMerkleLatestVersions = {};
8874
8984
  const cursorRows = await this.getAllByIndex(stores.cursors, "walletId", walletKey);
8875
8985
  for (const row of cursorRows) {
8876
8986
  this.cursors.set(row.chainId, { memo: row.memo, nullifier: row.nullifier, merkle: row.merkle });
@@ -8880,62 +8990,79 @@ var _IndexedDbStore = class _IndexedDbStore {
8880
8990
  const { walletId: _walletId, ...operation } = row;
8881
8991
  return operation;
8882
8992
  }).sort((a, b) => b.createdAt - a.createdAt);
8883
- const treeRows = await this.getAll(stores.merkleTrees);
8884
- for (const row of treeRows) {
8885
- this.merkleTrees[String(row.chainId)] = { ...row };
8993
+ const allVersionRows = await this.getAll(stores.chairmanMerkleVersions);
8994
+ for (const row of allVersionRows) {
8995
+ const key = String(row.chainId);
8996
+ const existing = this.chairmanMerkleLatestVersions[key];
8997
+ if (!existing || row.version > existing.version) {
8998
+ this.chairmanMerkleLatestVersions[key] = { ...row };
8999
+ }
8886
9000
  }
8887
9001
  this.pruneOperations();
8888
9002
  }
8889
9003
  /**
8890
- * Get a merkle node by id.
9004
+ * Get a chairmanMerkle node by id.
8891
9005
  */
8892
- async getMerkleNode(chainId, id) {
8893
- const node = await this.getByKey(this.storeNames().merkleNodes, [chainId, id]);
9006
+ async getChairmanMerkleNode(chainId, id) {
9007
+ const node = await this.getByKey(this.storeNames().chairmanMerkleNodes, [chainId, id]);
8894
9008
  if (!node) return void 0;
8895
9009
  const hash = node.hash;
8896
9010
  if (typeof hash !== "string" || !hash.startsWith("0x")) return void 0;
9011
+ if (typeof node.leftId !== "string" && node.leftId !== null) return void 0;
9012
+ if (typeof node.rightId !== "string" && node.rightId !== null) return void 0;
8897
9013
  return { ...node, chainId };
8898
9014
  }
8899
9015
  /**
8900
- * Upsert merkle nodes and persist.
9016
+ * Put chairmanMerkle nodes and persist.
8901
9017
  */
8902
- async upsertMerkleNodes(chainId, nodes) {
9018
+ async putChairmanMerkleNodes(chainId, nodes) {
8903
9019
  if (!nodes.length) return;
8904
9020
  const rows = nodes.map((node) => ({ ...node, chainId }));
8905
- await this.putMany(this.storeNames().merkleNodes, rows);
9021
+ await this.putMany(this.storeNames().chairmanMerkleNodes, rows);
8906
9022
  }
8907
9023
  /**
8908
- * Clear merkle nodes for a chain.
9024
+ * Get a chairmanMerkle version record by chainId and version.
8909
9025
  */
8910
- async clearMerkleNodes(chainId) {
8911
- await this.deleteAllByIndex(this.storeNames().merkleNodes, "chainId", chainId);
9026
+ async getChairmanMerkleVersion(chainId, version) {
9027
+ return this.getByKey(this.storeNames().chairmanMerkleVersions, [chainId, version]);
8912
9028
  }
8913
9029
  /**
8914
- * Get persisted merkle tree metadata for a chain.
9030
+ * Get the latest chairmanMerkle version for a chain from in-memory cache,
9031
+ * falling back to loading from the store if not cached.
8915
9032
  */
8916
- async getMerkleTree(chainId) {
8917
- const row = this.merkleTrees[String(chainId)];
8918
- if (!row) return void 0;
8919
- const totalElements = Number(row.totalElements);
8920
- const lastUpdated = Number(row.lastUpdated);
8921
- const root = row.root;
8922
- if (typeof root !== "string" || !root.startsWith("0x")) return void 0;
8923
- if (!Number.isFinite(totalElements) || totalElements < 0) return void 0;
8924
- return { chainId, root, totalElements: Math.floor(totalElements), lastUpdated: Number.isFinite(lastUpdated) ? Math.floor(lastUpdated) : 0 };
9033
+ async getLatestChairmanMerkleVersion(chainId) {
9034
+ const cached = this.chairmanMerkleLatestVersions[String(chainId)];
9035
+ if (cached) return { ...cached };
9036
+ const rows = await this.getAllByIndex(this.storeNames().chairmanMerkleVersions, "chainId", chainId);
9037
+ if (!rows.length) return void 0;
9038
+ let best = rows[0];
9039
+ for (const row of rows) {
9040
+ if (row.version > best.version) best = row;
9041
+ }
9042
+ this.chairmanMerkleLatestVersions[String(chainId)] = { ...best };
9043
+ return { ...best };
8925
9044
  }
8926
9045
  /**
8927
- * Persist merkle tree metadata for a chain.
9046
+ * Persist a chairmanMerkle version record and update the in-memory cache
9047
+ * if this is the latest version for the chain.
8928
9048
  */
8929
- async setMerkleTree(chainId, tree) {
8930
- this.merkleTrees[String(chainId)] = { ...tree, chainId };
8931
- await this.putMany(this.storeNames().merkleTrees, [{ ...tree, chainId }]);
9049
+ async putChairmanMerkleVersion(chainId, record) {
9050
+ await this.putMany(this.storeNames().chairmanMerkleVersions, [{ ...record, chainId }]);
9051
+ const key = String(chainId);
9052
+ const existing = this.chairmanMerkleLatestVersions[key];
9053
+ if (!existing || record.version >= existing.version) {
9054
+ this.chairmanMerkleLatestVersions[key] = { ...record, chainId };
9055
+ }
8932
9056
  }
8933
9057
  /**
8934
- * Clear merkle tree metadata for a chain.
9058
+ * Clear both chairmanMerkle nodes and versions for a chain and reset the cache.
8935
9059
  */
8936
- async clearMerkleTree(chainId) {
8937
- delete this.merkleTrees[String(chainId)];
8938
- await this.deleteByKeys(this.storeNames().merkleTrees, [chainId]);
9060
+ async clearChairmanMerkleTree(chainId) {
9061
+ delete this.chairmanMerkleLatestVersions[String(chainId)];
9062
+ await Promise.all([
9063
+ this.deleteAllByIndex(this.storeNames().chairmanMerkleNodes, "chainId", chainId),
9064
+ this.deleteAllByIndex(this.storeNames().chairmanMerkleVersions, "chainId", chainId)
9065
+ ]);
8939
9066
  }
8940
9067
  /**
8941
9068
  * Upsert entry memos and persist.
@@ -9180,7 +9307,7 @@ var _IndexedDbStore = class _IndexedDbStore {
9180
9307
  return applyOperationsQuery(this.operations, input);
9181
9308
  }
9182
9309
  };
9183
- _IndexedDbStore.DB_VERSION = 2;
9310
+ _IndexedDbStore.DB_VERSION = 3;
9184
9311
  var IndexedDbStore = _IndexedDbStore;
9185
9312
  // Annotate the CommonJS export names for ESM import in node:
9186
9313
  0 && (module.exports = {