@unicitylabs/sphere-sdk 0.14.5 → 0.14.6-dev.1

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.
@@ -9483,7 +9483,18 @@ function pause(ms, signal) {
9483
9483
  );
9484
9484
  });
9485
9485
  }
9486
- async function awaitProofBounded(deps, transaction, external) {
9486
+ async function retryTransient(attempt, signal, intervalMs) {
9487
+ for (; ; ) {
9488
+ try {
9489
+ return await attempt();
9490
+ } catch (err) {
9491
+ if (signal.aborted) throw err;
9492
+ if (!isTransientGatewayError(err)) throw err;
9493
+ await pause(intervalMs, signal);
9494
+ }
9495
+ }
9496
+ }
9497
+ function certificationDeadline(timeoutMs, external, label) {
9487
9498
  const controller = new AbortController();
9488
9499
  const relayAbort = () => {
9489
9500
  controller.abort(external?.reason);
@@ -9493,34 +9504,29 @@ async function awaitProofBounded(deps, transaction, external) {
9493
9504
  else external.addEventListener("abort", relayAbort, { once: true });
9494
9505
  }
9495
9506
  const timer = setTimeout(() => {
9496
- controller.abort(
9497
- new SphereError(
9498
- `inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
9499
- "AGGREGATOR_ERROR"
9500
- )
9501
- );
9502
- }, deps.timeoutMs);
9503
- try {
9504
- for (; ; ) {
9505
- try {
9506
- return await (0, import_InclusionProofUtils.waitInclusionProof)(
9507
- deps.client,
9508
- deps.trustBase,
9509
- deps.predicateVerifier,
9510
- transaction,
9511
- controller.signal,
9512
- deps.intervalMs
9513
- );
9514
- } catch (err) {
9515
- if (controller.signal.aborted) throw err;
9516
- if (!isTransientGatewayError(err)) throw err;
9517
- await pause(deps.intervalMs, controller.signal);
9518
- }
9507
+ controller.abort(new SphereError(`${label} within ${String(timeoutMs)} ms`, "AGGREGATOR_ERROR"));
9508
+ }, timeoutMs);
9509
+ return {
9510
+ signal: controller.signal,
9511
+ dispose: () => {
9512
+ clearTimeout(timer);
9513
+ external?.removeEventListener("abort", relayAbort);
9519
9514
  }
9520
- } finally {
9521
- clearTimeout(timer);
9522
- external?.removeEventListener("abort", relayAbort);
9523
- }
9515
+ };
9516
+ }
9517
+ async function awaitProofBounded(deps, transaction, signal) {
9518
+ return retryTransient(
9519
+ () => (0, import_InclusionProofUtils.waitInclusionProof)(
9520
+ deps.client,
9521
+ deps.trustBase,
9522
+ deps.predicateVerifier,
9523
+ transaction,
9524
+ signal,
9525
+ deps.intervalMs
9526
+ ),
9527
+ signal,
9528
+ deps.intervalMs
9529
+ );
9524
9530
  }
9525
9531
 
9526
9532
  // token-engine/errors.ts
@@ -9562,6 +9568,21 @@ var CheckpointTrustbaseMismatchError = class extends SphereError {
9562
9568
  }
9563
9569
  };
9564
9570
 
9571
+ // token-engine/certification-outcome.ts
9572
+ function certificationFailure(err, failLabel, submitError, submitRejectedCleanly) {
9573
+ if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
9574
+ return new TransferConflictError(
9575
+ `${failLabel}: the source state was already consumed by a different transaction (lost race \u2014 abort this intent and re-plan under a new transferId)`,
9576
+ err
9577
+ );
9578
+ }
9579
+ if (submitRejectedCleanly) return submitError;
9580
+ return new ProofUnconfirmedError(
9581
+ `${failLabel}: certification unconfirmed \u2014 the source spend may be on-chain; keep the intent open and resume under the same transferId`,
9582
+ submitError ?? err
9583
+ );
9584
+ }
9585
+
9565
9586
  // token-engine/token-blob.ts
9566
9587
  var TOKEN_BLOB_TAG = 39051n;
9567
9588
  var TOKEN_BLOB_VERSION = 1;
@@ -10084,10 +10105,28 @@ var SphereTokenEngine = class {
10084
10105
  * failure, not a resume case).
10085
10106
  */
10086
10107
  async submitAndAwaitProof(certificationData, transaction, failLabel, failCode, options) {
10108
+ const deadline = certificationDeadline(
10109
+ this.proofTimeoutMs,
10110
+ options?.signal,
10111
+ `${failLabel}: not certified`
10112
+ );
10113
+ try {
10114
+ return await this.submitThenProve(certificationData, transaction, failLabel, failCode, deadline.signal);
10115
+ } finally {
10116
+ deadline.dispose();
10117
+ }
10118
+ }
10119
+ /** One certification attempt under one deadline: the metered submit retries
10120
+ * transient answers instead of falling through to a proof that cannot exist. */
10121
+ async submitThenProve(certificationData, transaction, failLabel, failCode, signal) {
10087
10122
  let submitError = null;
10088
10123
  let submitRejectedCleanly = false;
10089
10124
  try {
10090
- const response = await this.deps.client.submitCertificationRequest(certificationData);
10125
+ const response = await retryTransient(
10126
+ () => this.deps.client.submitCertificationRequest(certificationData),
10127
+ signal,
10128
+ this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
10129
+ );
10091
10130
  if (response.status !== import_CertificationResponse.CertificationStatus.SUCCESS) {
10092
10131
  submitError = new SphereError(`${failLabel}: ${response.status}`, failCode);
10093
10132
  submitRejectedCleanly = CLEAN_REJECT_STATUSES.has(response.status);
@@ -10101,33 +10140,21 @@ var SphereTokenEngine = class {
10101
10140
  client: this.deps.client,
10102
10141
  trustBase: this.deps.trustBase,
10103
10142
  predicateVerifier: this.deps.predicateVerifier,
10104
- timeoutMs: this.proofTimeoutMs,
10105
10143
  intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
10106
10144
  // #683
10107
10145
  },
10108
10146
  transaction,
10109
- options?.signal
10147
+ signal
10110
10148
  );
10111
10149
  } catch (err) {
10112
- if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
10113
- throw new TransferConflictError(
10114
- `${failLabel}: the source state was already consumed by a different transaction (lost race \u2014 abort this intent and re-plan under a new transferId)`,
10115
- err
10116
- );
10117
- }
10118
- if (submitRejectedCleanly) throw submitError;
10119
- throw new ProofUnconfirmedError(
10120
- `${failLabel}: certification unconfirmed \u2014 the source spend may be on-chain; keep the intent open and resume under the same transferId`,
10121
- submitError ?? err
10122
- );
10150
+ throw certificationFailure(err, failLabel, submitError, submitRejectedCleanly);
10123
10151
  }
10124
10152
  }
10125
10153
  /**
10126
- * One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
10127
- * default deadline is unreliable — see lateDeliveringSignal — and a lost one
10128
- * means an op that never settles, which also wedges PaymentsFacade.stop().
10129
- * A deadline hit throws, and the caller maps that to ProofUnconfirmedError
10130
- * (keep-open), which is the correct posture: the spend may be on-chain.
10154
+ * One inclusion-proof wait, with the deadline OWNED here (#739) so it is bounded
10155
+ * and configurable rather than the SDK's fixed default. A hit throws, and the
10156
+ * caller maps it to ProofUnconfirmedError (keep-open) — the correct posture,
10157
+ * since the spend may be on-chain.
10131
10158
  */
10132
10159
  /** Fail fast if this engine's key does not own the token's current state. */
10133
10160
  assertOwned(token) {
@@ -11462,7 +11489,7 @@ function createMachineStores(kv) {
11462
11489
  }
11463
11490
 
11464
11491
  // modules/payments-v2/machine/TransferMachine.ts
11465
- var CERTIFY_WIDTH = 8;
11492
+ var CERTIFY_WIDTH = 20;
11466
11493
  var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
11467
11494
  "CERTIFICATION_UNCONFIRMED",
11468
11495
  "CHECKPOINT_PERSIST_FAILED",
@@ -11558,12 +11585,15 @@ var TransferMachine = class {
11558
11585
  payload: r.payload,
11559
11586
  ...r.payload.memo !== void 0 ? { memo: r.payload.memo } : {}
11560
11587
  };
11561
- const { committed, conflicts } = await this.resumeOps(ctx, engine, r);
11588
+ const outcomes = await this.resumeOps(ctx, engine, r);
11589
+ const { committed, cls, firstError } = summarize(outcomes);
11590
+ const conflicts = this.toConflicts(engine, r, outcomes);
11562
11591
  if (committed.length === 0 && conflicts.length > 0) throw conflicts[0].error;
11563
11592
  let deliveryPending = false;
11564
11593
  if (committed.length > 0) {
11565
11594
  deliveryPending = await this.deliverSet(r.transferId, r.payload.recipient, r.payload.memo, committed);
11566
11595
  }
11596
+ if (cls !== null && cls !== "conflict") throw firstError;
11567
11597
  await this.applyCommitted(engine, r.transferId, committed);
11568
11598
  if (conflicts.length > 0) {
11569
11599
  return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
@@ -11648,25 +11678,28 @@ var TransferMachine = class {
11648
11678
  );
11649
11679
  return { recipientBlob: outputs[0].blob.token, changeBlob: outputs[1].blob.token };
11650
11680
  }
11681
+ /** Raw outcomes; `summarize` alone decides what they mean — same as the send path. */
11651
11682
  async resumeOps(ctx, engine, r) {
11652
- const committed = [];
11653
- const conflicts = [];
11683
+ const outcomes = [];
11654
11684
  for (const op of buildOps(r.payload)) {
11655
11685
  const journaled = op.kind === "direct" ? r.journaled.get(op.opIndex) : void 0;
11656
11686
  if (journaled !== void 0) {
11657
- committed.push({ op, certified: true, recipientBlob: hexToBytes4(journaled.blobHex) });
11687
+ outcomes.push({ op, certified: true, recipientBlob: hexToBytes4(journaled.blobHex) });
11658
11688
  continue;
11659
11689
  }
11660
- const source = r.sources.get(op.sourceTokenId);
11661
- const outcome = await this.certifyOne(ctx, engine, op, source);
11662
- if (outcome.certified) {
11663
- committed.push(outcome);
11664
- continue;
11665
- }
11666
- if (classifyError(outcome.error) !== "conflict") throw outcome.error;
11667
- conflicts.push({ op, error: outcome.error, amount: conflictAmount(engine, r.payload, op, source) });
11690
+ outcomes.push(await this.certifyOne(ctx, engine, op, r.sources.get(op.sourceTokenId)));
11691
+ const last = outcomes[outcomes.length - 1];
11692
+ if (last.error !== void 0 && classifyError(last.error) !== "conflict") break;
11668
11693
  }
11669
- return { committed, conflicts };
11694
+ return outcomes;
11695
+ }
11696
+ /** Conflicted ops with the amount each leaves undelivered (#690 shortfall math). */
11697
+ toConflicts(engine, r, outcomes) {
11698
+ return outcomes.filter((o) => !o.certified && classifyError(o.error) === "conflict").map((o) => ({
11699
+ op: o.op,
11700
+ error: o.error,
11701
+ amount: conflictAmount(engine, r.payload, o.op, r.sources.get(o.op.sourceTokenId))
11702
+ }));
11670
11703
  }
11671
11704
  async deliverSet(transferId, recipientPubkey, memo, committed) {
11672
11705
  const journaled = committed.filter((o) => o.error === void 0 && o.recipientBlob !== void 0);