@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/index.cjs CHANGED
@@ -3289,8 +3289,8 @@ var MemoryStore = class {
3289
3289
  this.utxos = /* @__PURE__ */ new Map();
3290
3290
  this.operations = [];
3291
3291
  this.merkleLeavesByChain = /* @__PURE__ */ new Map();
3292
- this.merkleTreesByChain = /* @__PURE__ */ new Map();
3293
- this.merkleNodesByChain = /* @__PURE__ */ new Map();
3292
+ this.chairmanMerkleVersionsByChain = /* @__PURE__ */ new Map();
3293
+ this.chairmanMerkleNodesByChain = /* @__PURE__ */ new Map();
3294
3294
  this.entryMemosByChain = /* @__PURE__ */ new Map();
3295
3295
  this.entryNullifiersByChain = /* @__PURE__ */ new Map();
3296
3296
  const max = options?.maxOperations;
@@ -3306,8 +3306,8 @@ var MemoryStore = class {
3306
3306
  this.utxos.clear();
3307
3307
  this.operations = [];
3308
3308
  this.merkleLeavesByChain.clear();
3309
- this.merkleTreesByChain.clear();
3310
- this.merkleNodesByChain.clear();
3309
+ this.chairmanMerkleVersionsByChain.clear();
3310
+ this.chairmanMerkleNodesByChain.clear();
3311
3311
  this.entryMemosByChain.clear();
3312
3312
  this.entryNullifiersByChain.clear();
3313
3313
  }
@@ -3423,49 +3423,64 @@ var MemoryStore = class {
3423
3423
  return { chainId, cid: row.cid, commitment: row.commitment };
3424
3424
  }
3425
3425
  /**
3426
- * Get a merkle node by id.
3426
+ * Get a chairmanMerkle tree node by id.
3427
3427
  */
3428
- async getMerkleNode(chainId, id) {
3429
- return this.merkleNodesByChain.get(chainId)?.get(id);
3428
+ async getChairmanMerkleNode(chainId, id) {
3429
+ return this.chairmanMerkleNodesByChain.get(chainId)?.get(id);
3430
3430
  }
3431
3431
  /**
3432
- * Upsert merkle nodes for a chain.
3432
+ * Put chairmanMerkle tree nodes for a chain.
3433
3433
  */
3434
- async upsertMerkleNodes(chainId, nodes) {
3434
+ async putChairmanMerkleNodes(chainId, nodes) {
3435
3435
  if (!nodes.length) return;
3436
- let map = this.merkleNodesByChain.get(chainId);
3436
+ let map = this.chairmanMerkleNodesByChain.get(chainId);
3437
3437
  if (!map) {
3438
3438
  map = /* @__PURE__ */ new Map();
3439
- this.merkleNodesByChain.set(chainId, map);
3439
+ this.chairmanMerkleNodesByChain.set(chainId, map);
3440
3440
  }
3441
3441
  for (const node of nodes) {
3442
3442
  map.set(node.id, { ...node, chainId });
3443
3443
  }
3444
3444
  }
3445
3445
  /**
3446
- * Clear merkle nodes for a chain.
3446
+ * Get a chairmanMerkle version record by chain and version number.
3447
3447
  */
3448
- async clearMerkleNodes(chainId) {
3449
- this.merkleNodesByChain.delete(chainId);
3448
+ async getChairmanMerkleVersion(chainId, version) {
3449
+ const byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3450
+ const record = byVersion?.get(version);
3451
+ return record ? { ...record } : void 0;
3450
3452
  }
3451
3453
  /**
3452
- * Get persisted merkle tree state.
3454
+ * Get the latest chairmanMerkle version record (highest version number) for a chain.
3453
3455
  */
3454
- async getMerkleTree(chainId) {
3455
- const tree = this.merkleTreesByChain.get(chainId);
3456
- return tree ? { ...tree } : void 0;
3456
+ async getLatestChairmanMerkleVersion(chainId) {
3457
+ const byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3458
+ if (!byVersion || byVersion.size === 0) return void 0;
3459
+ let latest;
3460
+ for (const record of byVersion.values()) {
3461
+ if (!latest || record.version > latest.version) {
3462
+ latest = record;
3463
+ }
3464
+ }
3465
+ return latest ? { ...latest } : void 0;
3457
3466
  }
3458
3467
  /**
3459
- * Persist merkle tree state.
3468
+ * Persist a chairmanMerkle version record.
3460
3469
  */
3461
- async setMerkleTree(chainId, tree) {
3462
- this.merkleTreesByChain.set(chainId, { ...tree, chainId });
3470
+ async putChairmanMerkleVersion(chainId, record) {
3471
+ let byVersion = this.chairmanMerkleVersionsByChain.get(chainId);
3472
+ if (!byVersion) {
3473
+ byVersion = /* @__PURE__ */ new Map();
3474
+ this.chairmanMerkleVersionsByChain.set(chainId, byVersion);
3475
+ }
3476
+ byVersion.set(record.version, { ...record, chainId });
3463
3477
  }
3464
3478
  /**
3465
- * Clear merkle tree state.
3479
+ * Clear all chairmanMerkle tree state (both nodes and versions) for a chain.
3466
3480
  */
3467
- async clearMerkleTree(chainId) {
3468
- this.merkleTreesByChain.delete(chainId);
3481
+ async clearChairmanMerkleTree(chainId) {
3482
+ this.chairmanMerkleNodesByChain.delete(chainId);
3483
+ this.chairmanMerkleVersionsByChain.delete(chainId);
3469
3484
  }
3470
3485
  /**
3471
3486
  * Upsert entry memos (raw EntryService cache).
@@ -3989,13 +4004,14 @@ var KeyValueStore = class {
3989
4004
  this.utxoCache = /* @__PURE__ */ new Map();
3990
4005
  this.operationCache = /* @__PURE__ */ new Map();
3991
4006
  this.merkleLeafCids = {};
3992
- this.merkleTrees = {};
3993
- this.merkleNodeIds = {};
4007
+ this.chairmanMerkleLatestVersions = {};
4008
+ this.chairmanMerkleNodeIds = {};
4009
+ this.chairmanMerkleVersionNums = {};
3994
4010
  this.entryMemoCids = {};
3995
4011
  this.entryNullifierNids = {};
3996
4012
  this.loadedMerkleLeaves = /* @__PURE__ */ new Set();
3997
- this.loadedMerkleTrees = /* @__PURE__ */ new Set();
3998
- this.loadedMerkleNodes = /* @__PURE__ */ new Set();
4013
+ this.loadedChairmanMerkleVersions = /* @__PURE__ */ new Set();
4014
+ this.loadedChairmanMerkleNodes = /* @__PURE__ */ new Set();
3999
4015
  this.loadedEntryMemos = /* @__PURE__ */ new Set();
4000
4016
  this.loadedEntryNullifiers = /* @__PURE__ */ new Set();
4001
4017
  this.saveChain = Promise.resolve();
@@ -4029,9 +4045,6 @@ var KeyValueStore = class {
4029
4045
  walletOperationKey(id) {
4030
4046
  return `${this.walletBaseKey()}:operation:${id}`;
4031
4047
  }
4032
- sharedChainKey(part, chainId) {
4033
- return `${this.keyPrefix()}:shared:${part}:${chainId}`;
4034
- }
4035
4048
  sharedChainMetaKey(part, chainId) {
4036
4049
  return `${this.keyPrefix()}:shared:${part}:${chainId}:meta`;
4037
4050
  }
@@ -4084,13 +4097,14 @@ var KeyValueStore = class {
4084
4097
  this.operationCache.clear();
4085
4098
  this.walletMetaLoaded = false;
4086
4099
  this.merkleLeafCids = {};
4087
- this.merkleTrees = {};
4088
- this.merkleNodeIds = {};
4100
+ this.chairmanMerkleLatestVersions = {};
4101
+ this.chairmanMerkleNodeIds = {};
4102
+ this.chairmanMerkleVersionNums = {};
4089
4103
  this.entryMemoCids = {};
4090
4104
  this.entryNullifierNids = {};
4091
4105
  this.loadedMerkleLeaves.clear();
4092
- this.loadedMerkleTrees.clear();
4093
- this.loadedMerkleNodes.clear();
4106
+ this.loadedChairmanMerkleVersions.clear();
4107
+ this.loadedChairmanMerkleNodes.clear();
4094
4108
  this.loadedEntryMemos.clear();
4095
4109
  this.loadedEntryNullifiers.clear();
4096
4110
  }
@@ -4185,20 +4199,19 @@ var KeyValueStore = class {
4185
4199
  this.merkleLeafCids[key] = new Set(this.parseNumberIndex(cidsRaw));
4186
4200
  this.loadedMerkleLeaves.add(chainId);
4187
4201
  }
4188
- async ensureMerkleTreeLoaded(chainId) {
4189
- if (this.loadedMerkleTrees.has(chainId)) return;
4202
+ async ensureChairmanMerkleVersionsLoaded(chainId) {
4203
+ if (this.loadedChairmanMerkleVersions.has(chainId)) return;
4190
4204
  const key = String(chainId);
4191
- const raw = await this.options.client.get(this.sharedChainKey("merkleTrees", chainId));
4192
- const row = this.parseJson(raw, null);
4193
- if (row && typeof row === "object") this.merkleTrees[key] = row;
4194
- this.loadedMerkleTrees.add(chainId);
4205
+ const numsRaw = await this.options.client.get(this.sharedChainMetaKey("chairmanMerkleVersions", chainId));
4206
+ this.chairmanMerkleVersionNums[key] = new Set(this.parseNumberIndex(numsRaw));
4207
+ this.loadedChairmanMerkleVersions.add(chainId);
4195
4208
  }
4196
- async ensureMerkleNodesLoaded(chainId) {
4197
- if (this.loadedMerkleNodes.has(chainId)) return;
4209
+ async ensureChairmanMerkleNodesLoaded(chainId) {
4210
+ if (this.loadedChairmanMerkleNodes.has(chainId)) return;
4198
4211
  const key = String(chainId);
4199
- const idsRaw = await this.options.client.get(this.sharedChainMetaKey("merkleNodes", chainId));
4200
- this.merkleNodeIds[key] = new Set(this.parseStringIndex(idsRaw));
4201
- this.loadedMerkleNodes.add(chainId);
4212
+ const idsRaw = await this.options.client.get(this.sharedChainMetaKey("chairmanMerkleNodes", chainId));
4213
+ this.chairmanMerkleNodeIds[key] = new Set(this.parseStringIndex(idsRaw));
4214
+ this.loadedChairmanMerkleNodes.add(chainId);
4202
4215
  }
4203
4216
  async ensureEntryMemosLoaded(chainId) {
4204
4217
  if (this.loadedEntryMemos.has(chainId)) return;
@@ -4214,64 +4227,88 @@ var KeyValueStore = class {
4214
4227
  this.entryNullifierNids[key] = new Set(this.parseNumberIndex(nidsRaw));
4215
4228
  this.loadedEntryNullifiers.add(chainId);
4216
4229
  }
4217
- async getMerkleNode(chainId, id) {
4218
- await this.ensureMerkleNodesLoaded(chainId);
4219
- if (!this.merkleNodeIds[String(chainId)]?.has(id)) return void 0;
4220
- const raw = await this.options.client.get(this.sharedRecordKey("merkleNodes", chainId, id));
4230
+ async getChairmanMerkleNode(chainId, id) {
4231
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4232
+ if (!this.chairmanMerkleNodeIds[String(chainId)]?.has(id)) return void 0;
4233
+ const raw = await this.options.client.get(this.sharedRecordKey("chairmanMerkleNodes", chainId, id));
4221
4234
  const node = this.parseJson(raw, null);
4222
4235
  if (!node) return void 0;
4223
4236
  const hash = node.hash;
4224
4237
  if (typeof hash !== "string" || !hash.startsWith("0x")) return void 0;
4225
4238
  return { ...node, chainId };
4226
4239
  }
4227
- async upsertMerkleNodes(chainId, nodes) {
4240
+ async putChairmanMerkleNodes(chainId, nodes) {
4228
4241
  if (!nodes.length) return;
4229
- await this.ensureMerkleNodesLoaded(chainId);
4242
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4230
4243
  const key = String(chainId);
4231
- const ids = this.merkleNodeIds[key] ?? /* @__PURE__ */ new Set();
4244
+ const ids = this.chairmanMerkleNodeIds[key] ?? /* @__PURE__ */ new Set();
4232
4245
  const beforeSize = ids.size;
4233
4246
  for (const node of nodes) {
4234
4247
  ids.add(node.id);
4235
4248
  }
4236
- this.merkleNodeIds[key] = ids;
4249
+ this.chairmanMerkleNodeIds[key] = ids;
4237
4250
  await this.enqueueWrite(async () => {
4238
- await Promise.all(nodes.map((node) => this.writeJson(this.sharedRecordKey("merkleNodes", chainId, node.id), { ...node, chainId })));
4251
+ await Promise.all(nodes.map((node) => this.writeJson(this.sharedRecordKey("chairmanMerkleNodes", chainId, node.id), { ...node, chainId })));
4239
4252
  if (ids.size !== beforeSize) {
4240
- await this.writeJson(this.sharedChainMetaKey("merkleNodes", chainId), Array.from(ids));
4253
+ await this.writeJson(this.sharedChainMetaKey("chairmanMerkleNodes", chainId), Array.from(ids));
4241
4254
  }
4242
4255
  });
4243
4256
  }
4244
- async clearMerkleNodes(chainId) {
4245
- await this.ensureMerkleNodesLoaded(chainId);
4246
- const ids = Array.from(this.merkleNodeIds[String(chainId)] ?? []);
4247
- delete this.merkleNodeIds[String(chainId)];
4257
+ async getChairmanMerkleVersion(chainId, version) {
4258
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4259
+ if (!this.chairmanMerkleVersionNums[String(chainId)]?.has(version)) return void 0;
4260
+ const raw = await this.options.client.get(this.sharedRecordKey("chairmanMerkleVersions", chainId, version));
4261
+ const record = this.parseJson(raw, null);
4262
+ if (!record) return void 0;
4263
+ if (typeof record.rootHash !== "string" || !record.rootHash.startsWith("0x")) return void 0;
4264
+ if (typeof record.rootId !== "string") return void 0;
4265
+ const v = Number(record.version);
4266
+ if (!Number.isFinite(v) || v < 0) return void 0;
4267
+ return { chainId, version: Math.floor(v), rootId: record.rootId, rootHash: record.rootHash };
4268
+ }
4269
+ async getLatestChairmanMerkleVersion(chainId) {
4270
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4271
+ const nums = this.chairmanMerkleVersionNums[String(chainId)];
4272
+ if (!nums || nums.size === 0) return void 0;
4273
+ const maxVersion = Math.max(...nums);
4274
+ return this.getChairmanMerkleVersion(chainId, maxVersion);
4275
+ }
4276
+ async putChairmanMerkleVersion(chainId, record) {
4277
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4278
+ const key = String(chainId);
4279
+ const nums = this.chairmanMerkleVersionNums[key] ?? /* @__PURE__ */ new Set();
4280
+ const beforeSize = nums.size;
4281
+ nums.add(record.version);
4282
+ this.chairmanMerkleVersionNums[key] = nums;
4283
+ const current = this.chairmanMerkleLatestVersions[key];
4284
+ if (!current || record.version >= current.version) {
4285
+ this.chairmanMerkleLatestVersions[key] = { ...record, chainId };
4286
+ }
4287
+ const row = { ...record, chainId };
4248
4288
  await this.enqueueWrite(async () => {
4249
- await Promise.all(ids.map((id) => this.deleteOrReset(this.sharedRecordKey("merkleNodes", chainId, id), null)));
4250
- await this.deleteOrReset(this.sharedChainMetaKey("merkleNodes", chainId), []);
4289
+ await this.writeJson(this.sharedRecordKey("chairmanMerkleVersions", chainId, record.version), row);
4290
+ if (nums.size !== beforeSize) {
4291
+ await this.writeJson(this.sharedChainMetaKey("chairmanMerkleVersions", chainId), Array.from(nums).sort((a, b) => a - b));
4292
+ }
4251
4293
  });
4252
4294
  }
4253
- async getMerkleTree(chainId) {
4254
- await this.ensureMerkleTreeLoaded(chainId);
4255
- const row = this.merkleTrees[String(chainId)];
4256
- if (!row) return void 0;
4257
- const totalElements = Number(row.totalElements);
4258
- const lastUpdated = Number(row.lastUpdated);
4259
- const root = row.root;
4260
- if (typeof root !== "string" || !root.startsWith("0x")) return void 0;
4261
- if (!Number.isFinite(totalElements) || totalElements < 0) return void 0;
4262
- return { chainId, root, totalElements: Math.floor(totalElements), lastUpdated: Number.isFinite(lastUpdated) ? Math.floor(lastUpdated) : 0 };
4263
- }
4264
- async setMerkleTree(chainId, tree) {
4265
- await this.ensureMerkleTreeLoaded(chainId);
4266
- const row = { ...tree, chainId };
4267
- this.merkleTrees[String(chainId)] = row;
4268
- await this.enqueueWrite(() => this.writeJson(this.sharedChainKey("merkleTrees", chainId), row));
4269
- }
4270
- async clearMerkleTree(chainId) {
4271
- await this.ensureMerkleTreeLoaded(chainId);
4272
- delete this.merkleTrees[String(chainId)];
4295
+ async clearChairmanMerkleTree(chainId) {
4296
+ await this.ensureChairmanMerkleNodesLoaded(chainId);
4297
+ await this.ensureChairmanMerkleVersionsLoaded(chainId);
4298
+ const nodeIds = Array.from(this.chairmanMerkleNodeIds[String(chainId)] ?? []);
4299
+ const versionNums = Array.from(this.chairmanMerkleVersionNums[String(chainId)] ?? []);
4300
+ delete this.chairmanMerkleNodeIds[String(chainId)];
4301
+ delete this.chairmanMerkleVersionNums[String(chainId)];
4302
+ delete this.chairmanMerkleLatestVersions[String(chainId)];
4273
4303
  await this.enqueueWrite(async () => {
4274
- await this.deleteOrReset(this.sharedChainKey("merkleTrees", chainId), null);
4304
+ await Promise.all([
4305
+ ...nodeIds.map((id) => this.deleteOrReset(this.sharedRecordKey("chairmanMerkleNodes", chainId, id), null)),
4306
+ ...versionNums.map((v) => this.deleteOrReset(this.sharedRecordKey("chairmanMerkleVersions", chainId, v), null))
4307
+ ]);
4308
+ await Promise.all([
4309
+ this.deleteOrReset(this.sharedChainMetaKey("chairmanMerkleNodes", chainId), []),
4310
+ this.deleteOrReset(this.sharedChainMetaKey("chairmanMerkleVersions", chainId), [])
4311
+ ]);
4275
4312
  });
4276
4313
  }
4277
4314
  async upsertEntryMemos(memos) {
@@ -7115,6 +7152,7 @@ var MerkleEngine = class _MerkleEngine {
7115
7152
  this.hydrateInFlight = /* @__PURE__ */ new Map();
7116
7153
  this.mode = options?.mode ?? "hybrid";
7117
7154
  this.treeDepth = Math.max(1, Math.floor(options?.treeDepth ?? TREE_DEPTH_DEFAULT));
7155
+ this.readContractRoot = options?.readContractRoot;
7118
7156
  }
7119
7157
  /**
7120
7158
  * Compute the current merkle root index from total elements.
@@ -7123,9 +7161,6 @@ var MerkleEngine = class _MerkleEngine {
7123
7161
  if (totalElements <= tempArraySize) return 0;
7124
7162
  return Math.floor((totalElements - 1) / tempArraySize);
7125
7163
  }
7126
- /**
7127
- * Get or initialize the pending leaf buffer for a chain.
7128
- */
7129
7164
  ensurePendingLeaves(chainId) {
7130
7165
  let pending = this.pendingLeavesByChain.get(chainId);
7131
7166
  if (!pending) {
@@ -7134,9 +7169,6 @@ var MerkleEngine = class _MerkleEngine {
7134
7169
  }
7135
7170
  return pending;
7136
7171
  }
7137
- /**
7138
- * Get or initialize chain-level merkle state.
7139
- */
7140
7172
  ensureChainState(chainId) {
7141
7173
  let state = this.chainStateByChain.get(chainId);
7142
7174
  if (!state) {
@@ -7145,15 +7177,32 @@ var MerkleEngine = class _MerkleEngine {
7145
7177
  }
7146
7178
  return state;
7147
7179
  }
7148
- /**
7149
- * Poseidon2 merkle hash for a left/right pair.
7150
- */
7180
+ // ── Hashing ──
7151
7181
  static hashPair(left, right) {
7152
7182
  return Poseidon2.hashToHex(BigInt(left), BigInt(right), Poseidon2Domain.Merkle);
7153
7183
  }
7184
+ static normalizeHex32(value, name) {
7185
+ try {
7186
+ const bi = BigInt(value);
7187
+ if (bi < 0n) throw new Error("negative");
7188
+ const hex = bi.toString(16).padStart(64, "0");
7189
+ if (hex.length > 64) throw new Error("too_large");
7190
+ return `0x${hex}`;
7191
+ } catch (error) {
7192
+ throw new SdkError("MERKLE", `Invalid ${name}`, { value }, error);
7193
+ }
7194
+ }
7195
+ // ── Static helpers ──
7196
+ static totalElementsInTree(totalElements, tempArraySize = TEMP_ARRAY_SIZE_DEFAULT) {
7197
+ if (tempArraySize <= 0) throw new SdkError("MERKLE", "tempArraySize must be greater than zero", { tempArraySize });
7198
+ if (totalElements <= 0n) return 0;
7199
+ const size = BigInt(tempArraySize);
7200
+ return Number((totalElements - 1n) / size * size);
7201
+ }
7202
+ // ── Subtree (levels 0-5, 32 leaves → 1 root) ──
7154
7203
  /**
7155
7204
  * Build a fixed-depth subtree from 32 contiguous leaves.
7156
- * Returns the subtree root and all intermediate nodes for storage.
7205
+ * Returns the subtree root hash and all intermediate nodes for storage.
7157
7206
  */
7158
7207
  static buildSubtree(leafCommitments, baseIndex) {
7159
7208
  if (leafCommitments.length !== SUBTREE_SIZE) {
@@ -7174,71 +7223,71 @@ var MerkleEngine = class _MerkleEngine {
7174
7223
  const position = basePos + i;
7175
7224
  nodesToStore.push({
7176
7225
  chainId: 0,
7177
- id: `${level}-${position}`,
7178
- level,
7179
- position,
7180
- hash: next[i]
7226
+ id: `st-${level}-${position}`,
7227
+ hash: next[i],
7228
+ leftId: null,
7229
+ rightId: null
7181
7230
  });
7182
7231
  }
7183
7232
  currentLevel = next;
7184
7233
  }
7185
7234
  return { subtreeRoot: currentLevel[0], nodesToStore };
7186
7235
  }
7236
+ // ── ChairmanMerkle tree (persistent segment tree, levels 5-32) ──
7187
7237
  /**
7188
- * Fetch a node hash from storage if available.
7189
- */
7190
- async getNodeHash(chainId, id) {
7191
- const node = await this.storage?.getMerkleNode?.(chainId, id);
7192
- return node?.hash;
7193
- }
7194
- /**
7195
- * Merge a completed subtree root into the main tree, updating frontier nodes.
7196
- */
7197
- async mergeSubtreeToMainTree(input) {
7198
- let currentValue = input.subtreeRoot;
7199
- let frontierUpdated = false;
7200
- const nodesToStore = [];
7201
- for (let level = SUBTREE_DEPTH; level < this.treeDepth; level++) {
7202
- const nodeIndex = input.newTotalElements - 1 >> level;
7203
- if ((nodeIndex & 1) === 0) {
7204
- if (!frontierUpdated) {
7205
- nodesToStore.push({
7206
- chainId: input.chainId,
7207
- id: `frontier-${level}`,
7208
- level,
7209
- position: nodeIndex,
7210
- hash: currentValue
7211
- });
7212
- frontierUpdated = true;
7238
+ * Insert a subtree root into the persistent main tree.
7239
+ *
7240
+ * Top-down recursive: descends from root (level treeDepth) to the target
7241
+ * leaf position (level SUBTREE_DEPTH). At each level only the node on the
7242
+ * update path is newly created; the sibling is shared from the previous
7243
+ * version's tree.
7244
+ *
7245
+ * @returns new root node ID/hash and all newly created nodes.
7246
+ */
7247
+ async insertSubtreeRoot(chainId, prevRootId, subtreeRootHash, batchIndex, version) {
7248
+ const MAIN_DEPTH = this.treeDepth - SUBTREE_DEPTH;
7249
+ const nodes = [];
7250
+ const descend = async (nodeId, depth) => {
7251
+ const originalLevel = this.treeDepth - depth;
7252
+ if (depth === MAIN_DEPTH) {
7253
+ const newId2 = `cm-${version}-${originalLevel}`;
7254
+ nodes.push({ chainId, id: newId2, hash: subtreeRootHash, leftId: null, rightId: null });
7255
+ return { id: newId2, hash: subtreeRootHash };
7256
+ }
7257
+ let prevLeftId = null;
7258
+ let prevRightId = null;
7259
+ if (nodeId) {
7260
+ const prevNode = await this.storage?.getChairmanMerkleNode?.(chainId, nodeId);
7261
+ if (prevNode) {
7262
+ prevLeftId = prevNode.leftId;
7263
+ prevRightId = prevNode.rightId;
7213
7264
  }
7214
- currentValue = _MerkleEngine.hashPair(currentValue, getZeroHash(level));
7215
- } else {
7216
- const leftHash = await this.getNodeHash(input.chainId, `frontier-${level}`) ?? getZeroHash(level);
7217
- currentValue = _MerkleEngine.hashPair(leftHash, currentValue);
7218
7265
  }
7219
- const nextLevel = level + 1;
7220
- nodesToStore.push({
7221
- chainId: input.chainId,
7222
- id: `${nextLevel}-${nodeIndex >> 1}`,
7223
- level: nextLevel,
7224
- position: nodeIndex >> 1,
7225
- hash: currentValue
7226
- });
7227
- }
7228
- return { finalRoot: currentValue, nodesToStore };
7229
- }
7230
- /**
7231
- * Convert on-chain totalElements to the count of fully merged elements.
7232
- */
7233
- static totalElementsInTree(totalElements, tempArraySize = TEMP_ARRAY_SIZE_DEFAULT) {
7234
- if (tempArraySize <= 0) throw new SdkError("MERKLE", "tempArraySize must be greater than zero", { tempArraySize });
7235
- if (totalElements <= 0n) return 0;
7236
- const size = BigInt(tempArraySize);
7237
- return Number((totalElements - 1n) / size * size);
7266
+ const childLevel = originalLevel - 1;
7267
+ const remainingDepth = MAIN_DEPTH - depth - 1;
7268
+ const goRight = (batchIndex >> remainingDepth & 1) === 1;
7269
+ let leftResult;
7270
+ let rightResult;
7271
+ if (goRight) {
7272
+ const leftHash = prevLeftId ? (await this.storage?.getChairmanMerkleNode?.(chainId, prevLeftId))?.hash ?? getZeroHash(childLevel) : getZeroHash(childLevel);
7273
+ leftResult = { id: prevLeftId, hash: leftHash };
7274
+ const right = await descend(prevRightId, depth + 1);
7275
+ rightResult = { id: right.id, hash: right.hash };
7276
+ } else {
7277
+ const left = await descend(prevLeftId, depth + 1);
7278
+ leftResult = { id: left.id, hash: left.hash };
7279
+ const rightHash = prevRightId ? (await this.storage?.getChairmanMerkleNode?.(chainId, prevRightId))?.hash ?? getZeroHash(childLevel) : getZeroHash(childLevel);
7280
+ rightResult = { id: prevRightId, hash: rightHash };
7281
+ }
7282
+ const hash = _MerkleEngine.hashPair(leftResult.hash, rightResult.hash);
7283
+ const newId = `cm-${version}-${originalLevel}`;
7284
+ nodes.push({ chainId, id: newId, hash, leftId: leftResult.id, rightId: rightResult.id });
7285
+ return { id: newId, hash };
7286
+ };
7287
+ const root = await descend(prevRootId, 0);
7288
+ return { rootId: root.id, rootHash: root.hash, nodes };
7238
7289
  }
7239
- /**
7240
- * Hydrate local merkle state from storage on first use.
7241
- */
7290
+ // ── Hydration ──
7242
7291
  async hydrateFromStorage(chainId) {
7243
7292
  if (this.mode === "remote") return;
7244
7293
  if (this.hydratedChains.has(chainId)) return;
@@ -7247,31 +7296,17 @@ var MerkleEngine = class _MerkleEngine {
7247
7296
  const task = (async () => {
7248
7297
  try {
7249
7298
  const state = this.ensureChainState(chainId);
7250
- const leaves = await this.storage?.getMerkleLeaves?.(chainId);
7251
- if (!leaves || leaves.length === 0) return;
7252
- const sorted = [...leaves].map((l) => ({
7253
- cid: l.cid,
7254
- commitment: _MerkleEngine.normalizeHex32(l.commitment, "memo.commitment")
7255
- })).sort((a, b) => a.cid - b.cid);
7256
- for (let i = 0; i < sorted.length; i++) {
7257
- if (sorted[i].cid !== i) throw new Error(`Non-contiguous persisted merkle leaves: expected cid=${i}, got cid=${sorted[i].cid}`);
7258
- }
7259
- const storedTree = await this.storage?.getMerkleTree?.(chainId);
7260
- const leafCount = sorted.length;
7261
- const mergedFromLeaves = _MerkleEngine.totalElementsInTree(BigInt(leafCount));
7262
- const mergedFromTree = typeof storedTree?.totalElements === "number" && storedTree.totalElements > 0 ? storedTree.totalElements : 0;
7263
- const mergedElements = Math.max(mergedFromLeaves, mergedFromTree);
7264
7299
  const pending = this.ensurePendingLeaves(chainId);
7265
- pending.length = 0;
7266
- state.mergedElements = mergedElements;
7267
- if (leafCount > mergedElements) {
7268
- pending.push(...sorted.slice(mergedElements).map((l) => l.commitment));
7300
+ const latest = await this.storage?.getLatestChairmanMerkleVersion?.(chainId);
7301
+ if (latest) {
7302
+ state.mergedElements = latest.version;
7303
+ state.root = _MerkleEngine.normalizeHex32(latest.rootHash, "chairmanMerkleVersion.rootHash");
7269
7304
  }
7270
- if (storedTree?.root) {
7271
- state.root = _MerkleEngine.normalizeHex32(storedTree.root, "merkleTree.root");
7272
- } else {
7273
- const rootNode = await this.storage?.getMerkleNode?.(chainId, `${this.treeDepth}-0`);
7274
- state.root = rootNode?.hash ?? getZeroHash(this.treeDepth);
7305
+ const leaves = await this.storage?.getMerkleLeaves?.(chainId);
7306
+ if (leaves && leaves.length > state.mergedElements) {
7307
+ const sorted = [...leaves].sort((a, b) => a.cid - b.cid).slice(state.mergedElements);
7308
+ pending.length = 0;
7309
+ pending.push(...sorted.map((l) => _MerkleEngine.normalizeHex32(l.commitment, "leaf.commitment")));
7275
7310
  }
7276
7311
  } catch (error) {
7277
7312
  if (this.mode === "hybrid") return;
@@ -7284,26 +7319,7 @@ var MerkleEngine = class _MerkleEngine {
7284
7319
  this.hydrateInFlight.set(chainId, task);
7285
7320
  return task;
7286
7321
  }
7287
- /**
7288
- * Normalize unknown values to a 32-byte hex string.
7289
- */
7290
- static normalizeHex32(value, name) {
7291
- try {
7292
- const bi = BigInt(value);
7293
- if (bi < 0n) throw new Error("negative");
7294
- const hex = bi.toString(16).padStart(64, "0");
7295
- if (hex.length > 64) throw new Error("too_large");
7296
- return `0x${hex}`;
7297
- } catch (error) {
7298
- throw new SdkError("MERKLE", `Invalid ${name}`, { value }, error);
7299
- }
7300
- }
7301
- /**
7302
- * Feed contiguous (cid-ordered) memo leaves into the local merkle tree.
7303
- *
7304
- * This mirrors the client/app behavior: only after we have a full consecutive batch of 32 leaves
7305
- * do we merge them into the main tree. Leaves that are still in the buffer do not get local proofs.
7306
- */
7322
+ // ── Ingestion ──
7307
7323
  async ingestEntryMemos(chainId, memos) {
7308
7324
  if (this.mode === "remote") return;
7309
7325
  await this.hydrateFromStorage(chainId);
@@ -7331,26 +7347,42 @@ var MerkleEngine = class _MerkleEngine {
7331
7347
  expected++;
7332
7348
  while (pending.length >= SUBTREE_SIZE) {
7333
7349
  const batch = pending.splice(0, SUBTREE_SIZE);
7334
- const baseIndex = state.mergedElements;
7335
- const subtree = _MerkleEngine.buildSubtree(batch, baseIndex);
7336
- const merged = await this.mergeSubtreeToMainTree({ chainId, subtreeRoot: subtree.subtreeRoot, newTotalElements: baseIndex + SUBTREE_SIZE });
7337
- state.mergedElements += SUBTREE_SIZE;
7338
- state.root = merged.finalRoot;
7339
- const checkpointNode = {
7340
- chainId,
7341
- id: `checkpoint-${state.mergedElements}`,
7342
- level: -1,
7343
- position: 0,
7344
- hash: state.root
7345
- };
7346
- const nodes = [...subtree.nodesToStore, ...merged.nodesToStore, checkpointNode].map((n) => ({ ...n, chainId }));
7347
- await this.storage?.upsertMerkleNodes?.(chainId, nodes);
7348
- await this.storage?.setMerkleTree?.(chainId, {
7350
+ const batchIndex = state.mergedElements / SUBTREE_SIZE;
7351
+ const subtree = _MerkleEngine.buildSubtree(batch, state.mergedElements);
7352
+ const subtreeNodes = subtree.nodesToStore.map((n) => ({ ...n, chainId }));
7353
+ const prevVersion = await this.storage?.getLatestChairmanMerkleVersion?.(chainId);
7354
+ const prevRootId = prevVersion?.rootId ?? null;
7355
+ const newVersion = state.mergedElements + SUBTREE_SIZE;
7356
+ const result = await this.insertSubtreeRoot(chainId, prevRootId, subtree.subtreeRoot, batchIndex, newVersion);
7357
+ if (this.readContractRoot) {
7358
+ const rootIndex = newVersion / SUBTREE_SIZE;
7359
+ const onChainRoot = await this.readContractRoot(chainId, rootIndex).catch(() => null);
7360
+ if (onChainRoot !== null) {
7361
+ const onChainNorm = _MerkleEngine.normalizeHex32(onChainRoot, "onChainRoot");
7362
+ const isZero = BigInt(onChainNorm) === 0n;
7363
+ if (!isZero && onChainNorm !== result.rootHash) {
7364
+ const target = state.mergedElements;
7365
+ await this.rollback(chainId, target);
7366
+ throw new SdkError("MERKLE", "Local merkle root mismatch with on-chain root \u2014 rolled back", {
7367
+ chainId,
7368
+ rootIndex,
7369
+ localRoot: result.rootHash,
7370
+ onChainRoot: onChainNorm,
7371
+ version: newVersion,
7372
+ rollbackTarget: target
7373
+ });
7374
+ }
7375
+ }
7376
+ }
7377
+ await this.storage?.putChairmanMerkleNodes?.(chainId, [...subtreeNodes, ...result.nodes]);
7378
+ await this.storage?.putChairmanMerkleVersion?.(chainId, {
7349
7379
  chainId,
7350
- root: state.root,
7351
- totalElements: state.mergedElements,
7352
- lastUpdated: Date.now()
7380
+ version: newVersion,
7381
+ rootId: result.rootId,
7382
+ rootHash: result.rootHash
7353
7383
  });
7384
+ state.mergedElements = newVersion;
7385
+ state.root = result.rootHash;
7354
7386
  }
7355
7387
  }
7356
7388
  this.hydratedChains.add(chainId);
@@ -7361,15 +7393,59 @@ var MerkleEngine = class _MerkleEngine {
7361
7393
  throw new SdkError("MERKLE", "Failed to ingest local merkle leaves", { chainId, leafCount: leaves.length }, error);
7362
7394
  }
7363
7395
  }
7396
+ // ── Rollback (tree O(1) + sync cursor reset) ──
7364
7397
  /**
7365
- * Convenience wrapper to request a single proof.
7398
+ * Unified rollback: rewind tree to a previous batch boundary AND reset the
7399
+ * sync cursor so memo sync restarts from the same point.
7400
+ *
7401
+ * What gets rolled back:
7402
+ * - ChairmanMerkle tree version pointer (O(1) — old nodes still in storage)
7403
+ * - Pending leaves buffer (cleared)
7404
+ * - Sync cursor: memo + merkle fields (nullifier left unchanged — independent)
7405
+ *
7406
+ * @param targetMergedElements Must be a non-negative multiple of 32.
7407
+ * Pass 0 to reset to the empty tree.
7408
+ * @returns true if rollback succeeded, false if the target version doesn't exist.
7366
7409
  */
7367
- async getProofByCid(input) {
7368
- return this.getProofByCids({ chainId: input.chainId, cids: [input.cid], totalElements: input.totalElements });
7410
+ async rollback(chainId, targetMergedElements) {
7411
+ if (targetMergedElements < 0 || targetMergedElements % SUBTREE_SIZE !== 0) {
7412
+ throw new SdkError("MERKLE", "rollback target must be a non-negative multiple of 32", { targetMergedElements });
7413
+ }
7414
+ const state = this.ensureChainState(chainId);
7415
+ const pending = this.ensurePendingLeaves(chainId);
7416
+ if (targetMergedElements === 0) {
7417
+ state.mergedElements = 0;
7418
+ state.root = getZeroHash(this.treeDepth);
7419
+ pending.length = 0;
7420
+ await this.resetSyncCursor(chainId, 0);
7421
+ return true;
7422
+ }
7423
+ const version = await this.storage?.getChairmanMerkleVersion?.(chainId, targetMergedElements);
7424
+ if (!version) return false;
7425
+ state.mergedElements = targetMergedElements;
7426
+ state.root = _MerkleEngine.normalizeHex32(version.rootHash, "version.rootHash");
7427
+ pending.length = 0;
7428
+ this.hydratedChains.add(chainId);
7429
+ await this.resetSyncCursor(chainId, targetMergedElements);
7430
+ return true;
7369
7431
  }
7370
7432
  /**
7371
- * Get merkle proofs for a set of cids using local/hybrid/remote logic.
7433
+ * Reset the sync cursor's memo field to `targetMemo` (and derive merkle cursor),
7434
+ * but only if the current cursor is ahead of the target.
7435
+ * Nullifier cursor is left unchanged — nullifiers are independent of tree state.
7372
7436
  */
7437
+ async resetSyncCursor(chainId, targetMemo) {
7438
+ if (!this.storage?.getSyncCursor || !this.storage?.setSyncCursor) return;
7439
+ const cursor = await this.storage.getSyncCursor(chainId);
7440
+ if (!cursor || cursor.memo <= targetMemo) return;
7441
+ cursor.memo = targetMemo;
7442
+ cursor.merkle = this.currentMerkleRootIndex(targetMemo);
7443
+ await this.storage.setSyncCursor(chainId, cursor);
7444
+ }
7445
+ // ── Proof generation ──
7446
+ async getProofByCid(input) {
7447
+ return this.getProofByCids({ chainId: input.chainId, cids: [input.cid], totalElements: input.totalElements });
7448
+ }
7373
7449
  async getProofByCids(input) {
7374
7450
  const cids = [...input.cids];
7375
7451
  if (cids.length === 0) throw new SdkError("MERKLE", "No cids provided", { chainId: input.chainId });
@@ -7384,15 +7460,16 @@ var MerkleEngine = class _MerkleEngine {
7384
7460
  await this.hydrateFromStorage(input.chainId);
7385
7461
  const canUseLocal = this.mode !== "remote";
7386
7462
  if (canUseLocal) {
7387
- const tree = await this.storage?.getMerkleTree?.(input.chainId);
7388
- const hasDb = typeof this.storage?.getMerkleLeaf === "function" && typeof this.storage?.getMerkleNode === "function" && typeof tree?.totalElements === "number" && typeof tree?.root === "string";
7389
- if (hasDb && tree) {
7390
- if (tree.totalElements < contractTreeElements) {
7463
+ const version = contractTreeElements > 0 ? await this.storage?.getChairmanMerkleVersion?.(input.chainId, contractTreeElements) : void 0;
7464
+ const hasDb = typeof this.storage?.getMerkleLeaf === "function" && typeof this.storage?.getChairmanMerkleNode === "function" && (contractTreeElements === 0 || !!version);
7465
+ if (hasDb) {
7466
+ const state = this.ensureChainState(input.chainId);
7467
+ if (contractTreeElements > 0 && state.mergedElements < contractTreeElements) {
7391
7468
  if (this.mode === "local") {
7392
7469
  throw new SdkError("MERKLE", "Local merkle db is behind contract", {
7393
7470
  chainId: input.chainId,
7394
7471
  cids,
7395
- localTotalElements: tree.totalElements,
7472
+ localMergedElements: state.mergedElements,
7396
7473
  contractTreeElements
7397
7474
  });
7398
7475
  }
@@ -7404,30 +7481,13 @@ var MerkleEngine = class _MerkleEngine {
7404
7481
  proof.push({ leaf_index: cid, path: new Array(this.treeDepth + 1).fill("0") });
7405
7482
  continue;
7406
7483
  }
7407
- const leaf = await this.storage.getMerkleLeaf(input.chainId, cid);
7408
- if (!leaf) throw new Error(`missing_leaf:${cid}`);
7409
- const path = [leaf.commitment];
7410
- for (let level = 1; level <= this.treeDepth; level++) {
7411
- const siblingIndex = cid >> level - 1 ^ 1;
7412
- if (level === 1) {
7413
- const siblingLeaf = await this.storage.getMerkleLeaf(input.chainId, siblingIndex);
7414
- path.push(siblingLeaf?.commitment ?? getZeroHash(0));
7415
- continue;
7416
- }
7417
- const targetLevel = level - 1;
7418
- const siblingNode = await this.storage.getMerkleNode(input.chainId, `${targetLevel}-${siblingIndex}`);
7419
- path.push(siblingNode?.hash ?? getZeroHash(targetLevel));
7420
- }
7484
+ const path = await this.buildLocalProofPath(input.chainId, cid, version);
7421
7485
  proof.push({ leaf_index: cid, path });
7422
7486
  }
7423
- let effectiveRoot = tree.root;
7424
- if (tree.totalElements > contractTreeElements && contractTreeElements > 0) {
7425
- const checkpoint = await this.storage.getMerkleNode(input.chainId, `checkpoint-${contractTreeElements}`);
7426
- if (checkpoint) effectiveRoot = checkpoint.hash;
7427
- }
7487
+ const effectiveRoot = contractTreeElements > 0 ? _MerkleEngine.normalizeHex32(version.rootHash, "version.rootHash") : getZeroHash(this.treeDepth);
7428
7488
  return {
7429
7489
  proof,
7430
- merkle_root: _MerkleEngine.normalizeHex32(effectiveRoot, "merkleTree.root"),
7490
+ merkle_root: effectiveRoot,
7431
7491
  latest_cid: totalElements > 0n ? Number(totalElements - 1n) : -1
7432
7492
  };
7433
7493
  } catch (error) {
@@ -7437,7 +7497,7 @@ var MerkleEngine = class _MerkleEngine {
7437
7497
  }
7438
7498
  }
7439
7499
  } else if (this.mode === "local" && needsTreeProof.length) {
7440
- throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter_merkle_db" });
7500
+ throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter_or_version" });
7441
7501
  }
7442
7502
  }
7443
7503
  if (needsTreeProof.length === 0) {
@@ -7462,15 +7522,64 @@ var MerkleEngine = class _MerkleEngine {
7462
7522
  };
7463
7523
  }
7464
7524
  /**
7465
- * Fetch a remote merkle root (used when no proofs are needed).
7525
+ * Build a local proof path by traversing the chairmanMerkle tree.
7526
+ *
7527
+ * Levels 0-4: sibling hashes from subtree internal nodes (st-{level}-{pos}).
7528
+ * Levels 5-31: sibling hashes from chairmanMerkle tree traversal (top-down from version root).
7466
7529
  */
7530
+ async buildLocalProofPath(chainId, cid, version) {
7531
+ const leaf = await this.storage.getMerkleLeaf(chainId, cid);
7532
+ if (!leaf) throw new Error(`missing_leaf:${cid}`);
7533
+ const path = [leaf.commitment];
7534
+ for (let level = 1; level <= SUBTREE_DEPTH; level++) {
7535
+ const siblingPos = cid >> level - 1 ^ 1;
7536
+ if (level === 1) {
7537
+ const siblingLeaf = await this.storage.getMerkleLeaf(chainId, siblingPos);
7538
+ path.push(siblingLeaf?.commitment ?? getZeroHash(0));
7539
+ } else {
7540
+ const targetLevel = level - 1;
7541
+ const node = await this.storage.getChairmanMerkleNode(chainId, `st-${targetLevel}-${siblingPos}`);
7542
+ path.push(node?.hash ?? getZeroHash(targetLevel));
7543
+ }
7544
+ }
7545
+ const batchIndex = cid >> SUBTREE_DEPTH;
7546
+ const MAIN_DEPTH = this.treeDepth - SUBTREE_DEPTH;
7547
+ const mainSiblings = [];
7548
+ let nodeId = version.rootId;
7549
+ for (let depth = 0; depth < MAIN_DEPTH; depth++) {
7550
+ const childLevel = this.treeDepth - depth - 1;
7551
+ if (!nodeId) {
7552
+ mainSiblings.push(getZeroHash(childLevel));
7553
+ continue;
7554
+ }
7555
+ const node = await this.storage.getChairmanMerkleNode(chainId, nodeId);
7556
+ if (!node) {
7557
+ mainSiblings.push(getZeroHash(childLevel));
7558
+ nodeId = null;
7559
+ continue;
7560
+ }
7561
+ const remainingDepth = MAIN_DEPTH - depth - 1;
7562
+ const goRight = (batchIndex >> remainingDepth & 1) === 1;
7563
+ if (goRight) {
7564
+ const leftNode = node.leftId ? await this.storage.getChairmanMerkleNode(chainId, node.leftId) : null;
7565
+ mainSiblings.push(leftNode?.hash ?? getZeroHash(childLevel));
7566
+ nodeId = node.rightId;
7567
+ } else {
7568
+ const rightNode = node.rightId ? await this.storage.getChairmanMerkleNode(chainId, node.rightId) : null;
7569
+ mainSiblings.push(rightNode?.hash ?? getZeroHash(childLevel));
7570
+ nodeId = node.leftId;
7571
+ }
7572
+ }
7573
+ for (let i = mainSiblings.length - 1; i >= 0; i--) {
7574
+ path.push(mainSiblings[i]);
7575
+ }
7576
+ return path;
7577
+ }
7578
+ // ── Remote helpers ──
7467
7579
  async fetchRemoteRootOnly(chainId) {
7468
7580
  const remote = await this.fetchRemoteProofFromService({ chainId, cids: [0] });
7469
7581
  return _MerkleEngine.normalizeHex32(remote.merkle_root, "remote.merkle_root");
7470
7582
  }
7471
- /**
7472
- * Fetch proofs from the remote merkle service.
7473
- */
7474
7583
  async fetchRemoteProofFromService(input) {
7475
7584
  const chain = this.getChain(input.chainId);
7476
7585
  if (!chain.merkleProofUrl) {
@@ -7479,9 +7588,7 @@ var MerkleEngine = class _MerkleEngine {
7479
7588
  const client = new MerkleClient(chain.merkleProofUrl);
7480
7589
  return client.getProofByCids(input.cids);
7481
7590
  }
7482
- /**
7483
- * Build membership witnesses for provided UTXOs from a remote proof response.
7484
- */
7591
+ // ── Witness builders (unchanged) ──
7485
7592
  buildAccMemberWitnesses(input) {
7486
7593
  return input.utxos.map((utxo, idx) => {
7487
7594
  const remoteProof = input.remote.proof[idx];
@@ -7495,9 +7602,6 @@ var MerkleEngine = class _MerkleEngine {
7495
7602
  };
7496
7603
  });
7497
7604
  }
7498
- /**
7499
- * Convert UTXOs into circuit input secrets, decrypting memos and padding if needed.
7500
- */
7501
7605
  async buildInputSecretsFromUtxos(input) {
7502
7606
  if (!Array.isArray(input.utxos) || input.utxos.length === 0) {
7503
7607
  throw new SdkError("MERKLE", "No utxos provided", { count: 0 });