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

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/node.js CHANGED
@@ -3352,7 +3352,7 @@ var MemoryStore = class {
3352
3352
  async getMerkleLeaf(chainId, cid) {
3353
3353
  const rows = this.merkleLeavesByChain.get(chainId);
3354
3354
  const row = rows?.[cid];
3355
- if (!row) return void 0;
3355
+ if (!row || row.cid !== cid) return void 0;
3356
3356
  return { chainId, cid: row.cid, commitment: row.commitment };
3357
3357
  }
3358
3358
  /**
@@ -4369,7 +4369,7 @@ var KeyValueStore = class {
4369
4369
  if (!this.merkleLeafCids[String(chainId)]?.has(cid)) return void 0;
4370
4370
  const raw = await this.options.client.get(this.sharedRecordKey("merkleLeaves", chainId, cid));
4371
4371
  const row = this.parseJson(raw, null);
4372
- if (!row) return void 0;
4372
+ if (!row || row.cid !== cid) return void 0;
4373
4373
  return { chainId, cid: row.cid, commitment: row.commitment };
4374
4374
  }
4375
4375
  async appendMerkleLeaves(chainId, leaves) {
@@ -5131,10 +5131,10 @@ var ProofEngine = class {
5131
5131
  merkle_root_index: parsed.merkle_root_index ?? context.merkle_root_index,
5132
5132
  relayer: parsed.relayer ?? context.relayer,
5133
5133
  recipient: parsed.recipient ?? context.recipient,
5134
- withdraw_amount: parsed.withdraw_amount ? BigInt(parsed.withdraw_amount) : context.withdraw_amount,
5134
+ withdraw_amount: parsed.withdraw_amount != null ? BigInt(parsed.withdraw_amount) : context.withdraw_amount,
5135
5135
  extra_data: parsed.extra_data ?? context.extra_data,
5136
- relayer_fee: parsed.relayer_fee ? BigInt(parsed.relayer_fee) : context.relayer_fee,
5137
- gas_drop_value: parsed.gas_drop_value ? BigInt(parsed.gas_drop_value) : context.gas_drop_value,
5136
+ relayer_fee: parsed.relayer_fee != null ? BigInt(parsed.relayer_fee) : context.relayer_fee,
5137
+ gas_drop_value: parsed.gas_drop_value != null ? BigInt(parsed.gas_drop_value) : context.gas_drop_value,
5138
5138
  array_hash_digest: parsed.array_hash_digest ?? context.array_hash_digest,
5139
5139
  gnark_output: parsed.gnark_output,
5140
5140
  witness_json: parsed.witness_json,
@@ -6258,10 +6258,31 @@ var SyncEngine = class {
6258
6258
 
6259
6259
  // src/planner/planner.ts
6260
6260
  import { maxUint256, toHex as toHex4 } from "viem";
6261
+
6262
+ // src/utils/validators.ts
6263
+ import { getAddress as getAddress2 } from "viem";
6261
6264
  var requireHex = (value, name) => {
6262
6265
  if (isHexStrict(value, { minBytes: 1 })) return value;
6263
6266
  throw new SdkError("CONFIG", `${name} must be a hex string starting with 0x`);
6264
6267
  };
6268
+ var requireNumber = (value, name) => {
6269
+ if (typeof value === "number" && Number.isFinite(value)) return value;
6270
+ throw new SdkError("CONFIG", `${name} must be a finite number`);
6271
+ };
6272
+ var requireAddress = (value, name) => {
6273
+ if (typeof value !== "string") {
6274
+ throw new SdkError("CONFIG", `${name} must be a string address`);
6275
+ }
6276
+ return getAddress2(value);
6277
+ };
6278
+ var requireBigint = (value, name) => {
6279
+ if (typeof value === "bigint") return value;
6280
+ if (typeof value === "string" && value.length) return BigInt(value);
6281
+ if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
6282
+ throw new SdkError("CONFIG", `${name} must be a bigint-compatible value`);
6283
+ };
6284
+
6285
+ // src/planner/planner.ts
6265
6286
  var parsePlanInput = (input) => {
6266
6287
  const action = input.action;
6267
6288
  if (action !== "transfer" && action !== "withdraw") {
@@ -6339,9 +6360,12 @@ var recordsFee = (input, _records, expectedOutput, action, relayerFee, expectedI
6339
6360
  }
6340
6361
  } else {
6341
6362
  cost = expectedIsWithFee ? expectedOutput : expectedOutput + fee;
6363
+ outputAmount = expectedIsWithFee ? expectedOutput - fee : expectedOutput;
6364
+ if (outputAmount < 0n) outputAmount = 0n;
6342
6365
  }
6343
6366
  if (total < cost) {
6344
6367
  cost = 0n;
6368
+ outputAmount = 0n;
6345
6369
  }
6346
6370
  break;
6347
6371
  }
@@ -6827,27 +6851,6 @@ var Planner = class {
6827
6851
  };
6828
6852
 
6829
6853
  // src/tx/txBuilder.ts
6830
- import { getAddress as getAddress2 } from "viem";
6831
- var requireNumber = (value, name) => {
6832
- if (typeof value === "number" && Number.isFinite(value)) return value;
6833
- throw new SdkError("CONFIG", `Missing ${name}`);
6834
- };
6835
- var requireHex2 = (value, name) => {
6836
- if (isHexStrict(value, { minBytes: 1 })) return value;
6837
- throw new SdkError("CONFIG", `Missing ${name}`);
6838
- };
6839
- var requireAddress = (value, name) => {
6840
- if (typeof value !== "string") {
6841
- throw new SdkError("CONFIG", `Missing ${name}`);
6842
- }
6843
- return getAddress2(value);
6844
- };
6845
- var requireBigint = (value, name) => {
6846
- if (typeof value === "bigint") return value;
6847
- if (typeof value === "string" && value.length) return BigInt(value);
6848
- if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
6849
- throw new SdkError("CONFIG", `Missing ${name}`);
6850
- };
6851
6854
  var TxBuilder = class {
6852
6855
  /**
6853
6856
  * Build relayer request for transfer proofs.
@@ -6861,7 +6864,7 @@ var TxBuilder = class {
6861
6864
  if (!Array.isArray(extraData) || extraData.length !== 3) {
6862
6865
  throw new SdkError("CONFIG", "Transfer requires extra_data as bytes[3]");
6863
6866
  }
6864
- extraData.forEach((entry, idx) => requireHex2(entry, `extra_data[${idx}]`));
6867
+ extraData.forEach((entry, idx) => requireHex(entry, `extra_data[${idx}]`));
6865
6868
  const request = {
6866
6869
  kind: "relayer",
6867
6870
  method: "POST",
@@ -6894,7 +6897,7 @@ var TxBuilder = class {
6894
6897
  if (Array.isArray(extraData)) {
6895
6898
  throw new SdkError("CONFIG", "Withdraw requires extra_data as bytes");
6896
6899
  }
6897
- const extraDataHex = requireHex2(extraData, "extra_data");
6900
+ const extraDataHex = requireHex(extraData, "extra_data");
6898
6901
  const request = {
6899
6902
  kind: "relayer",
6900
6903
  method: "POST",
@@ -7295,7 +7298,7 @@ var MerkleEngine = class _MerkleEngine {
7295
7298
  const isZero = BigInt(onChainNorm) === 0n;
7296
7299
  if (!isZero && onChainNorm !== result.rootHash) {
7297
7300
  const target = state.mergedElements;
7298
- await this.rollback(chainId, target);
7301
+ await this._rollback(chainId, target);
7299
7302
  throw new SdkError("MERKLE", "Local merkle root mismatch with on-chain root \u2014 rolled back", {
7300
7303
  chainId,
7301
7304
  rootIndex,
@@ -7328,21 +7331,31 @@ var MerkleEngine = class _MerkleEngine {
7328
7331
  }
7329
7332
  // ── Rollback (tree O(1) + sync cursor reset) ──
7330
7333
  /**
7331
- * Unified rollback: rewind tree to a previous batch boundary AND reset the
7332
- * sync cursor so memo sync restarts from the same point.
7334
+ * Public rollback: step back one batch (32 elements) from the current position.
7335
+ * Upper-layer code calls this on any error to reset and retry.
7333
7336
  *
7334
7337
  * What gets rolled back:
7335
7338
  * - ChairmanMerkle tree version pointer (O(1) — old nodes still in storage)
7336
7339
  * - Pending leaves buffer (cleared)
7337
7340
  * - Sync cursor: memo + merkle fields (nullifier left unchanged — independent)
7338
7341
  *
7342
+ * @returns true if rollback succeeded, false if already at 0 or target version doesn't exist.
7343
+ */
7344
+ async rollback(chainId) {
7345
+ const state = this.ensureChainState(chainId);
7346
+ const target = Math.max(0, state.mergedElements - SUBTREE_SIZE);
7347
+ return this._rollback(chainId, target);
7348
+ }
7349
+ /**
7350
+ * Internal rollback to an exact batch boundary.
7351
+ *
7339
7352
  * @param targetMergedElements Must be a non-negative multiple of 32.
7340
7353
  * Pass 0 to reset to the empty tree.
7341
7354
  * @returns true if rollback succeeded, false if the target version doesn't exist.
7342
7355
  */
7343
- async rollback(chainId, targetMergedElements) {
7356
+ async _rollback(chainId, targetMergedElements) {
7344
7357
  if (targetMergedElements < 0 || targetMergedElements % SUBTREE_SIZE !== 0) {
7345
- throw new SdkError("MERKLE", "rollback target must be a non-negative multiple of 32", { targetMergedElements });
7358
+ throw new SdkError("MERKLE", "_rollback target must be a non-negative multiple of 32", { targetMergedElements });
7346
7359
  }
7347
7360
  const state = this.ensureChainState(chainId);
7348
7361
  const pending = this.ensurePendingLeaves(chainId);
@@ -7393,44 +7406,64 @@ var MerkleEngine = class _MerkleEngine {
7393
7406
  await this.hydrateFromStorage(input.chainId);
7394
7407
  const canUseLocal = this.mode !== "remote";
7395
7408
  if (canUseLocal) {
7396
- const version = contractTreeElements > 0 ? await this.storage?.getChairmanMerkleVersion?.(input.chainId, contractTreeElements) : void 0;
7397
- const hasDb = typeof this.storage?.getMerkleLeaf === "function" && typeof this.storage?.getChairmanMerkleNode === "function" && (contractTreeElements === 0 || !!version);
7398
- if (hasDb) {
7409
+ const hasMerkleLeaf = typeof this.storage?.getMerkleLeaf === "function";
7410
+ const hasChairmanNode = typeof this.storage?.getChairmanMerkleNode === "function";
7411
+ if (hasMerkleLeaf && hasChairmanNode) {
7399
7412
  const state = this.ensureChainState(input.chainId);
7400
- if (contractTreeElements > 0 && state.mergedElements < contractTreeElements) {
7401
- if (this.mode === "local") {
7402
- throw new SdkError("MERKLE", "Local merkle db is behind contract", {
7403
- chainId: input.chainId,
7404
- cids,
7405
- localMergedElements: state.mergedElements,
7406
- contractTreeElements
7407
- });
7408
- }
7409
- } else {
7410
- try {
7411
- const proof = [];
7412
- for (const cid of cids) {
7413
- if (cid >= contractTreeElements) {
7414
- proof.push({ leaf_index: cid, path: new Array(this.treeDepth + 1).fill("0") });
7415
- continue;
7416
- }
7417
- const path2 = await this.buildLocalProofPath(input.chainId, cid, version);
7418
- proof.push({ leaf_index: cid, path: path2 });
7413
+ let version = contractTreeElements > 0 ? await this.storage?.getChairmanMerkleVersion?.(input.chainId, contractTreeElements) : void 0;
7414
+ let effectiveTreeElements = contractTreeElements;
7415
+ if (!version && contractTreeElements > 0 && state.mergedElements > 0) {
7416
+ const latest = await this.storage?.getLatestChairmanMerkleVersion?.(input.chainId);
7417
+ if (latest && latest.version > 0) {
7418
+ const allCovered = needsTreeProof.every((cid) => cid < latest.version);
7419
+ if (allCovered) {
7420
+ version = latest;
7421
+ effectiveTreeElements = latest.version;
7419
7422
  }
7420
- const effectiveRoot = contractTreeElements > 0 ? _MerkleEngine.normalizeHex32(version.rootHash, "version.rootHash") : getZeroHash(this.treeDepth);
7421
- return {
7422
- proof,
7423
- merkle_root: effectiveRoot,
7424
- latest_cid: totalElements > 0n ? Number(totalElements - 1n) : -1
7425
- };
7426
- } catch (error) {
7423
+ }
7424
+ }
7425
+ const hasDb = contractTreeElements === 0 || !!version;
7426
+ if (hasDb) {
7427
+ if (effectiveTreeElements > 0 && state.mergedElements < effectiveTreeElements) {
7427
7428
  if (this.mode === "local") {
7428
- throw new SdkError("MERKLE", "Local merkle proof build failed", { chainId: input.chainId, cids }, error);
7429
+ throw new SdkError("MERKLE", "Local merkle db is behind contract", {
7430
+ chainId: input.chainId,
7431
+ cids,
7432
+ localMergedElements: state.mergedElements,
7433
+ contractTreeElements: effectiveTreeElements
7434
+ });
7435
+ }
7436
+ } else {
7437
+ try {
7438
+ const proof = [];
7439
+ for (const cid of cids) {
7440
+ if (cid >= effectiveTreeElements) {
7441
+ proof.push({ leaf_index: cid, path: new Array(this.treeDepth + 1).fill("0") });
7442
+ continue;
7443
+ }
7444
+ const path2 = await this.buildLocalProofPath(input.chainId, cid, version);
7445
+ proof.push({ leaf_index: cid, path: path2 });
7446
+ }
7447
+ const effectiveRoot = effectiveTreeElements > 0 ? _MerkleEngine.normalizeHex32(version.rootHash, "version.rootHash") : getZeroHash(this.treeDepth);
7448
+ const effectiveLatestCid = effectiveTreeElements > 0 ? effectiveTreeElements - 1 : -1;
7449
+ return {
7450
+ proof,
7451
+ merkle_root: effectiveRoot,
7452
+ latest_cid: effectiveLatestCid
7453
+ };
7454
+ } catch (error) {
7455
+ if (this.mode === "local") {
7456
+ throw new SdkError("MERKLE", "Local merkle proof build failed", { chainId: input.chainId, cids }, error);
7457
+ }
7429
7458
  }
7430
7459
  }
7460
+ } else {
7461
+ if (this.mode === "local" && needsTreeProof.length) {
7462
+ throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter_or_version" });
7463
+ }
7431
7464
  }
7432
7465
  } else if (this.mode === "local" && needsTreeProof.length) {
7433
- throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter_or_version" });
7466
+ throw new SdkError("MERKLE", "Local merkle db unavailable", { chainId: input.chainId, cids, reason: "missing_adapter" });
7434
7467
  }
7435
7468
  }
7436
7469
  if (needsTreeProof.length === 0) {
@@ -8709,6 +8742,9 @@ function hydrateWalletState(state) {
8709
8742
  }
8710
8743
 
8711
8744
  // src/store/fileStore.ts
8745
+ function isEnoent(err) {
8746
+ return !!err && typeof err === "object" && "code" in err && err.code === "ENOENT";
8747
+ }
8712
8748
  var FileStore = class {
8713
8749
  /**
8714
8750
  * Create a FileStore with a base directory and optional limits.
@@ -8771,26 +8807,28 @@ var FileStore = class {
8771
8807
  return path.join(this.options.baseDir, `shared.merkle.${chainId}.jsonl`);
8772
8808
  }
8773
8809
  async readMerkleFile(filePath) {
8810
+ let raw;
8774
8811
  try {
8775
- const raw = await readFile(filePath, "utf8");
8776
- const out = [];
8777
- const lines = raw.split("\n").filter((l) => l.trim().length > 0);
8778
- for (const line of lines) {
8779
- try {
8780
- const row = JSON.parse(line);
8781
- const cid = Number(row?.cid);
8782
- const commitment2 = row?.commitment;
8783
- if (!Number.isFinite(cid) || cid < 0) continue;
8784
- if (typeof commitment2 !== "string" || !commitment2.startsWith("0x")) continue;
8785
- out.push({ cid: Math.floor(cid), commitment: commitment2 });
8786
- } catch {
8787
- }
8788
- }
8789
- out.sort((a, b) => a.cid - b.cid);
8790
- return out.length ? out : void 0;
8791
- } catch {
8812
+ raw = await readFile(filePath, "utf8");
8813
+ } catch (err) {
8814
+ if (!isEnoent(err)) throw err;
8792
8815
  return void 0;
8793
8816
  }
8817
+ const out = [];
8818
+ const lines = raw.split("\n").filter((l) => l.trim().length > 0);
8819
+ for (const line of lines) {
8820
+ try {
8821
+ const row = JSON.parse(line);
8822
+ const cid = Number(row?.cid);
8823
+ const commitment2 = row?.commitment;
8824
+ if (!Number.isFinite(cid) || cid < 0) continue;
8825
+ if (typeof commitment2 !== "string" || !commitment2.startsWith("0x")) continue;
8826
+ out.push({ cid: Math.floor(cid), commitment: commitment2 });
8827
+ } catch {
8828
+ }
8829
+ }
8830
+ out.sort((a, b) => a.cid - b.cid);
8831
+ return out.length ? out : void 0;
8794
8832
  }
8795
8833
  /**
8796
8834
  * Infer the next merkle cid from the tail of the jsonl file.
@@ -8807,10 +8845,14 @@ var FileStore = class {
8807
8845
  }
8808
8846
  const last = JSON.parse(lines[lines.length - 1]);
8809
8847
  const cid = Number(last?.cid);
8810
- const next = Number.isFinite(cid) ? Math.max(0, Math.floor(cid) + 1) : 0;
8848
+ if (!Number.isFinite(cid)) {
8849
+ throw new Error(`corrupted merkle jsonl: missing or non-numeric cid in tail of ${this.merkleFilePath(chainId)}`);
8850
+ }
8851
+ const next = Math.max(0, Math.floor(cid) + 1);
8811
8852
  this.merkleNextCid.set(chainId, next);
8812
8853
  return next;
8813
- } catch {
8854
+ } catch (err) {
8855
+ if (!isEnoent(err)) throw err;
8814
8856
  this.merkleNextCid.set(chainId, 0);
8815
8857
  return 0;
8816
8858
  }
@@ -8837,7 +8879,8 @@ var FileStore = class {
8837
8879
  for (const [k, v] of hydrated.utxos.entries()) this.utxos.set(k, v);
8838
8880
  const operations = Array.isArray(parsed.operations) ? parsed.operations : [];
8839
8881
  this.operations = operations;
8840
- } catch {
8882
+ } catch (err) {
8883
+ if (!isEnoent(err)) throw err;
8841
8884
  }
8842
8885
  try {
8843
8886
  const raw = await readFile(this.sharedFilePath(), "utf8");
@@ -8869,7 +8912,8 @@ var FileStore = class {
8869
8912
  if (entryNullifiersRaw && typeof entryNullifiersRaw === "object") {
8870
8913
  this.entryNullifiers = entryNullifiersRaw;
8871
8914
  }
8872
- } catch {
8915
+ } catch (err) {
8916
+ if (!isEnoent(err)) throw err;
8873
8917
  }
8874
8918
  const pruned = this.pruneOperations();
8875
8919
  if (pruned) void this.saveWallet().catch(() => void 0);
@@ -9140,7 +9184,7 @@ var FileStore = class {
9140
9184
  async getMerkleLeaf(chainId, cid) {
9141
9185
  const rows = await this.getMerkleLeaves(chainId);
9142
9186
  const row = rows?.[cid];
9143
- if (!row) return void 0;
9187
+ if (!row || row.cid !== cid) return void 0;
9144
9188
  return { chainId, cid: row.cid, commitment: row.commitment };
9145
9189
  }
9146
9190
  /**