@unicitylabs/sphere-sdk 0.14.4 → 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.
@@ -9466,41 +9466,35 @@ function resolveProofTimeoutMs(configured) {
9466
9466
  }
9467
9467
  return configured;
9468
9468
  }
9469
- function lateDeliveringSignal(signal) {
9470
- return new Proxy(signal, {
9471
- get(target, prop) {
9472
- if (prop === "addEventListener") {
9473
- return (type, listener, options) => {
9474
- if (type === "abort" && target.aborted) {
9475
- queueMicrotask(() => {
9476
- listener.call(target, new Event("abort"));
9477
- });
9478
- return;
9479
- }
9480
- target.addEventListener(type, listener, options);
9481
- };
9482
- }
9483
- const value = Reflect.get(target, prop, target);
9484
- return typeof value === "function" ? value.bind(target) : value;
9485
- }
9486
- });
9469
+ var TRANSIENT_HTTP_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
9470
+ function isTransientGatewayError(err) {
9471
+ return err instanceof import_JsonRpcNetworkError.JsonRpcNetworkError && TRANSIENT_HTTP_STATUSES.has(err.status);
9487
9472
  }
9488
- function rejectOnAbort(signal) {
9489
- return new Promise((_resolve, reject) => {
9490
- if (signal.aborted) {
9491
- reject(signal.reason);
9492
- return;
9493
- }
9473
+ function pause(ms, signal) {
9474
+ return new Promise((resolve) => {
9475
+ const timer = setTimeout(resolve, ms);
9494
9476
  signal.addEventListener(
9495
9477
  "abort",
9496
9478
  () => {
9497
- reject(signal.reason);
9479
+ clearTimeout(timer);
9480
+ resolve();
9498
9481
  },
9499
9482
  { once: true }
9500
9483
  );
9501
9484
  });
9502
9485
  }
9503
- 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) {
9504
9498
  const controller = new AbortController();
9505
9499
  const relayAbort = () => {
9506
9500
  controller.abort(external?.reason);
@@ -9510,28 +9504,29 @@ async function awaitProofBounded(deps, transaction, external) {
9510
9504
  else external.addEventListener("abort", relayAbort, { once: true });
9511
9505
  }
9512
9506
  const timer = setTimeout(() => {
9513
- controller.abort(
9514
- new SphereError(
9515
- `inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
9516
- "AGGREGATOR_ERROR"
9517
- )
9518
- );
9519
- }, deps.timeoutMs);
9520
- const wait = (0, import_InclusionProofUtils.waitInclusionProof)(
9521
- deps.client,
9522
- deps.trustBase,
9523
- deps.predicateVerifier,
9524
- transaction,
9525
- lateDeliveringSignal(controller.signal),
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);
9514
+ }
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,
9526
9528
  deps.intervalMs
9527
9529
  );
9528
- wait.catch(() => void 0);
9529
- try {
9530
- return await Promise.race([wait, rejectOnAbort(controller.signal)]);
9531
- } finally {
9532
- clearTimeout(timer);
9533
- external?.removeEventListener("abort", relayAbort);
9534
- }
9535
9530
  }
9536
9531
 
9537
9532
  // token-engine/errors.ts
@@ -9573,6 +9568,21 @@ var CheckpointTrustbaseMismatchError = class extends SphereError {
9573
9568
  }
9574
9569
  };
9575
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
+
9576
9586
  // token-engine/token-blob.ts
9577
9587
  var TOKEN_BLOB_TAG = 39051n;
9578
9588
  var TOKEN_BLOB_VERSION = 1;
@@ -10095,10 +10105,28 @@ var SphereTokenEngine = class {
10095
10105
  * failure, not a resume case).
10096
10106
  */
10097
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) {
10098
10122
  let submitError = null;
10099
10123
  let submitRejectedCleanly = false;
10100
10124
  try {
10101
- 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
+ );
10102
10130
  if (response.status !== import_CertificationResponse.CertificationStatus.SUCCESS) {
10103
10131
  submitError = new SphereError(`${failLabel}: ${response.status}`, failCode);
10104
10132
  submitRejectedCleanly = CLEAN_REJECT_STATUSES.has(response.status);
@@ -10112,33 +10140,21 @@ var SphereTokenEngine = class {
10112
10140
  client: this.deps.client,
10113
10141
  trustBase: this.deps.trustBase,
10114
10142
  predicateVerifier: this.deps.predicateVerifier,
10115
- timeoutMs: this.proofTimeoutMs,
10116
10143
  intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
10117
10144
  // #683
10118
10145
  },
10119
10146
  transaction,
10120
- options?.signal
10147
+ signal
10121
10148
  );
10122
10149
  } catch (err) {
10123
- if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
10124
- throw new TransferConflictError(
10125
- `${failLabel}: the source state was already consumed by a different transaction (lost race \u2014 abort this intent and re-plan under a new transferId)`,
10126
- err
10127
- );
10128
- }
10129
- if (submitRejectedCleanly) throw submitError;
10130
- throw new ProofUnconfirmedError(
10131
- `${failLabel}: certification unconfirmed \u2014 the source spend may be on-chain; keep the intent open and resume under the same transferId`,
10132
- submitError ?? err
10133
- );
10150
+ throw certificationFailure(err, failLabel, submitError, submitRejectedCleanly);
10134
10151
  }
10135
10152
  }
10136
10153
  /**
10137
- * One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
10138
- * default deadline is unreliable — see lateDeliveringSignal — and a lost one
10139
- * means an op that never settles, which also wedges PaymentsFacade.stop().
10140
- * A deadline hit throws, and the caller maps that to ProofUnconfirmedError
10141
- * (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.
10142
10158
  */
10143
10159
  /** Fail fast if this engine's key does not own the token's current state. */
10144
10160
  assertOwned(token) {
@@ -11473,7 +11489,7 @@ function createMachineStores(kv) {
11473
11489
  }
11474
11490
 
11475
11491
  // modules/payments-v2/machine/TransferMachine.ts
11476
- var CERTIFY_WIDTH = 8;
11492
+ var CERTIFY_WIDTH = 20;
11477
11493
  var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
11478
11494
  "CERTIFICATION_UNCONFIRMED",
11479
11495
  "CHECKPOINT_PERSIST_FAILED",
@@ -11569,12 +11585,15 @@ var TransferMachine = class {
11569
11585
  payload: r.payload,
11570
11586
  ...r.payload.memo !== void 0 ? { memo: r.payload.memo } : {}
11571
11587
  };
11572
- 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);
11573
11591
  if (committed.length === 0 && conflicts.length > 0) throw conflicts[0].error;
11574
11592
  let deliveryPending = false;
11575
11593
  if (committed.length > 0) {
11576
11594
  deliveryPending = await this.deliverSet(r.transferId, r.payload.recipient, r.payload.memo, committed);
11577
11595
  }
11596
+ if (cls !== null && cls !== "conflict") throw firstError;
11578
11597
  await this.applyCommitted(engine, r.transferId, committed);
11579
11598
  if (conflicts.length > 0) {
11580
11599
  return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
@@ -11659,25 +11678,28 @@ var TransferMachine = class {
11659
11678
  );
11660
11679
  return { recipientBlob: outputs[0].blob.token, changeBlob: outputs[1].blob.token };
11661
11680
  }
11681
+ /** Raw outcomes; `summarize` alone decides what they mean — same as the send path. */
11662
11682
  async resumeOps(ctx, engine, r) {
11663
- const committed = [];
11664
- const conflicts = [];
11683
+ const outcomes = [];
11665
11684
  for (const op of buildOps(r.payload)) {
11666
11685
  const journaled = op.kind === "direct" ? r.journaled.get(op.opIndex) : void 0;
11667
11686
  if (journaled !== void 0) {
11668
- committed.push({ op, certified: true, recipientBlob: hexToBytes4(journaled.blobHex) });
11669
- continue;
11670
- }
11671
- const source = r.sources.get(op.sourceTokenId);
11672
- const outcome = await this.certifyOne(ctx, engine, op, source);
11673
- if (outcome.certified) {
11674
- committed.push(outcome);
11687
+ outcomes.push({ op, certified: true, recipientBlob: hexToBytes4(journaled.blobHex) });
11675
11688
  continue;
11676
11689
  }
11677
- if (classifyError(outcome.error) !== "conflict") throw outcome.error;
11678
- 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;
11679
11693
  }
11680
- 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
+ }));
11681
11703
  }
11682
11704
  async deliverSet(transferId, recipientPubkey, memo, committed) {
11683
11705
  const journaled = committed.filter((o) => o.error === void 0 && o.recipientBlob !== void 0);