@unicitylabs/sphere-sdk 0.14.5 → 0.14.6

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.
@@ -9397,7 +9397,18 @@ function pause(ms, signal) {
9397
9397
  );
9398
9398
  });
9399
9399
  }
9400
- async function awaitProofBounded(deps, transaction, external) {
9400
+ async function retryTransient(attempt, signal, intervalMs) {
9401
+ for (; ; ) {
9402
+ try {
9403
+ return await attempt();
9404
+ } catch (err) {
9405
+ if (signal.aborted) throw err;
9406
+ if (!isTransientGatewayError(err)) throw err;
9407
+ await pause(intervalMs, signal);
9408
+ }
9409
+ }
9410
+ }
9411
+ function certificationDeadline(timeoutMs, external, label) {
9401
9412
  const controller = new AbortController();
9402
9413
  const relayAbort = () => {
9403
9414
  controller.abort(external?.reason);
@@ -9407,34 +9418,29 @@ async function awaitProofBounded(deps, transaction, external) {
9407
9418
  else external.addEventListener("abort", relayAbort, { once: true });
9408
9419
  }
9409
9420
  const timer = setTimeout(() => {
9410
- controller.abort(
9411
- new SphereError(
9412
- `inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
9413
- "AGGREGATOR_ERROR"
9414
- )
9415
- );
9416
- }, deps.timeoutMs);
9417
- try {
9418
- for (; ; ) {
9419
- try {
9420
- return await waitInclusionProof(
9421
- deps.client,
9422
- deps.trustBase,
9423
- deps.predicateVerifier,
9424
- transaction,
9425
- controller.signal,
9426
- deps.intervalMs
9427
- );
9428
- } catch (err) {
9429
- if (controller.signal.aborted) throw err;
9430
- if (!isTransientGatewayError(err)) throw err;
9431
- await pause(deps.intervalMs, controller.signal);
9432
- }
9421
+ controller.abort(new SphereError(`${label} within ${String(timeoutMs)} ms`, "AGGREGATOR_ERROR"));
9422
+ }, timeoutMs);
9423
+ return {
9424
+ signal: controller.signal,
9425
+ dispose: () => {
9426
+ clearTimeout(timer);
9427
+ external?.removeEventListener("abort", relayAbort);
9433
9428
  }
9434
- } finally {
9435
- clearTimeout(timer);
9436
- external?.removeEventListener("abort", relayAbort);
9437
- }
9429
+ };
9430
+ }
9431
+ async function awaitProofBounded(deps, transaction, signal) {
9432
+ return retryTransient(
9433
+ () => waitInclusionProof(
9434
+ deps.client,
9435
+ deps.trustBase,
9436
+ deps.predicateVerifier,
9437
+ transaction,
9438
+ signal,
9439
+ deps.intervalMs
9440
+ ),
9441
+ signal,
9442
+ deps.intervalMs
9443
+ );
9438
9444
  }
9439
9445
 
9440
9446
  // token-engine/errors.ts
@@ -9476,6 +9482,21 @@ var CheckpointTrustbaseMismatchError = class extends SphereError {
9476
9482
  }
9477
9483
  };
9478
9484
 
9485
+ // token-engine/certification-outcome.ts
9486
+ function certificationFailure(err, failLabel, submitError, submitRejectedCleanly) {
9487
+ if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
9488
+ return new TransferConflictError(
9489
+ `${failLabel}: the source state was already consumed by a different transaction (lost race \u2014 abort this intent and re-plan under a new transferId)`,
9490
+ err
9491
+ );
9492
+ }
9493
+ if (submitRejectedCleanly) return submitError;
9494
+ return new ProofUnconfirmedError(
9495
+ `${failLabel}: certification unconfirmed \u2014 the source spend may be on-chain; keep the intent open and resume under the same transferId`,
9496
+ submitError ?? err
9497
+ );
9498
+ }
9499
+
9479
9500
  // token-engine/token-blob.ts
9480
9501
  var TOKEN_BLOB_TAG = 39051n;
9481
9502
  var TOKEN_BLOB_VERSION = 1;
@@ -9998,10 +10019,28 @@ var SphereTokenEngine = class {
9998
10019
  * failure, not a resume case).
9999
10020
  */
10000
10021
  async submitAndAwaitProof(certificationData, transaction, failLabel, failCode, options) {
10022
+ const deadline = certificationDeadline(
10023
+ this.proofTimeoutMs,
10024
+ options?.signal,
10025
+ `${failLabel}: not certified`
10026
+ );
10027
+ try {
10028
+ return await this.submitThenProve(certificationData, transaction, failLabel, failCode, deadline.signal);
10029
+ } finally {
10030
+ deadline.dispose();
10031
+ }
10032
+ }
10033
+ /** One certification attempt under one deadline: the metered submit retries
10034
+ * transient answers instead of falling through to a proof that cannot exist. */
10035
+ async submitThenProve(certificationData, transaction, failLabel, failCode, signal) {
10001
10036
  let submitError = null;
10002
10037
  let submitRejectedCleanly = false;
10003
10038
  try {
10004
- const response = await this.deps.client.submitCertificationRequest(certificationData);
10039
+ const response = await retryTransient(
10040
+ () => this.deps.client.submitCertificationRequest(certificationData),
10041
+ signal,
10042
+ this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
10043
+ );
10005
10044
  if (response.status !== CertificationStatus.SUCCESS) {
10006
10045
  submitError = new SphereError(`${failLabel}: ${response.status}`, failCode);
10007
10046
  submitRejectedCleanly = CLEAN_REJECT_STATUSES.has(response.status);
@@ -10015,33 +10054,21 @@ var SphereTokenEngine = class {
10015
10054
  client: this.deps.client,
10016
10055
  trustBase: this.deps.trustBase,
10017
10056
  predicateVerifier: this.deps.predicateVerifier,
10018
- timeoutMs: this.proofTimeoutMs,
10019
10057
  intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
10020
10058
  // #683
10021
10059
  },
10022
10060
  transaction,
10023
- options?.signal
10061
+ signal
10024
10062
  );
10025
10063
  } catch (err) {
10026
- if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
10027
- throw new TransferConflictError(
10028
- `${failLabel}: the source state was already consumed by a different transaction (lost race \u2014 abort this intent and re-plan under a new transferId)`,
10029
- err
10030
- );
10031
- }
10032
- if (submitRejectedCleanly) throw submitError;
10033
- throw new ProofUnconfirmedError(
10034
- `${failLabel}: certification unconfirmed \u2014 the source spend may be on-chain; keep the intent open and resume under the same transferId`,
10035
- submitError ?? err
10036
- );
10064
+ throw certificationFailure(err, failLabel, submitError, submitRejectedCleanly);
10037
10065
  }
10038
10066
  }
10039
10067
  /**
10040
- * One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
10041
- * default deadline is unreliable — see lateDeliveringSignal — and a lost one
10042
- * means an op that never settles, which also wedges PaymentsFacade.stop().
10043
- * A deadline hit throws, and the caller maps that to ProofUnconfirmedError
10044
- * (keep-open), which is the correct posture: the spend may be on-chain.
10068
+ * One inclusion-proof wait, with the deadline OWNED here (#739) so it is bounded
10069
+ * and configurable rather than the SDK's fixed default. A hit throws, and the
10070
+ * caller maps it to ProofUnconfirmedError (keep-open) — the correct posture,
10071
+ * since the spend may be on-chain.
10045
10072
  */
10046
10073
  /** Fail fast if this engine's key does not own the token's current state. */
10047
10074
  assertOwned(token) {
@@ -11376,7 +11403,7 @@ function createMachineStores(kv) {
11376
11403
  }
11377
11404
 
11378
11405
  // modules/payments-v2/machine/TransferMachine.ts
11379
- var CERTIFY_WIDTH = 8;
11406
+ var CERTIFY_WIDTH = 20;
11380
11407
  var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
11381
11408
  "CERTIFICATION_UNCONFIRMED",
11382
11409
  "CHECKPOINT_PERSIST_FAILED",
@@ -11472,12 +11499,15 @@ var TransferMachine = class {
11472
11499
  payload: r.payload,
11473
11500
  ...r.payload.memo !== void 0 ? { memo: r.payload.memo } : {}
11474
11501
  };
11475
- const { committed, conflicts } = await this.resumeOps(ctx, engine, r);
11502
+ const outcomes = await this.resumeOps(ctx, engine, r);
11503
+ const { committed, cls, firstError } = summarize(outcomes);
11504
+ const conflicts = this.toConflicts(engine, r, outcomes);
11476
11505
  if (committed.length === 0 && conflicts.length > 0) throw conflicts[0].error;
11477
11506
  let deliveryPending = false;
11478
11507
  if (committed.length > 0) {
11479
11508
  deliveryPending = await this.deliverSet(r.transferId, r.payload.recipient, r.payload.memo, committed);
11480
11509
  }
11510
+ if (cls !== null && cls !== "conflict") throw firstError;
11481
11511
  await this.applyCommitted(engine, r.transferId, committed);
11482
11512
  if (conflicts.length > 0) {
11483
11513
  return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
@@ -11562,25 +11592,28 @@ var TransferMachine = class {
11562
11592
  );
11563
11593
  return { recipientBlob: outputs[0].blob.token, changeBlob: outputs[1].blob.token };
11564
11594
  }
11595
+ /** Raw outcomes; `summarize` alone decides what they mean — same as the send path. */
11565
11596
  async resumeOps(ctx, engine, r) {
11566
- const committed = [];
11567
- const conflicts = [];
11597
+ const outcomes = [];
11568
11598
  for (const op of buildOps(r.payload)) {
11569
11599
  const journaled = op.kind === "direct" ? r.journaled.get(op.opIndex) : void 0;
11570
11600
  if (journaled !== void 0) {
11571
- committed.push({ op, certified: true, recipientBlob: hexToBytes4(journaled.blobHex) });
11601
+ outcomes.push({ op, certified: true, recipientBlob: hexToBytes4(journaled.blobHex) });
11572
11602
  continue;
11573
11603
  }
11574
- const source = r.sources.get(op.sourceTokenId);
11575
- const outcome = await this.certifyOne(ctx, engine, op, source);
11576
- if (outcome.certified) {
11577
- committed.push(outcome);
11578
- continue;
11579
- }
11580
- if (classifyError(outcome.error) !== "conflict") throw outcome.error;
11581
- conflicts.push({ op, error: outcome.error, amount: conflictAmount(engine, r.payload, op, source) });
11604
+ outcomes.push(await this.certifyOne(ctx, engine, op, r.sources.get(op.sourceTokenId)));
11605
+ const last = outcomes[outcomes.length - 1];
11606
+ if (last.error !== void 0 && classifyError(last.error) !== "conflict") break;
11582
11607
  }
11583
- return { committed, conflicts };
11608
+ return outcomes;
11609
+ }
11610
+ /** Conflicted ops with the amount each leaves undelivered (#690 shortfall math). */
11611
+ toConflicts(engine, r, outcomes) {
11612
+ return outcomes.filter((o) => !o.certified && classifyError(o.error) === "conflict").map((o) => ({
11613
+ op: o.op,
11614
+ error: o.error,
11615
+ amount: conflictAmount(engine, r.payload, o.op, r.sources.get(o.op.sourceTokenId))
11616
+ }));
11584
11617
  }
11585
11618
  async deliverSet(transferId, recipientPubkey, memo, committed) {
11586
11619
  const journaled = committed.filter((o) => o.error === void 0 && o.recipientBlob !== void 0);