@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.
package/dist/index.js CHANGED
@@ -9413,7 +9413,18 @@ function pause(ms, signal) {
9413
9413
  );
9414
9414
  });
9415
9415
  }
9416
- async function awaitProofBounded(deps, transaction, external) {
9416
+ async function retryTransient(attempt, signal, intervalMs) {
9417
+ for (; ; ) {
9418
+ try {
9419
+ return await attempt();
9420
+ } catch (err) {
9421
+ if (signal.aborted) throw err;
9422
+ if (!isTransientGatewayError(err)) throw err;
9423
+ await pause(intervalMs, signal);
9424
+ }
9425
+ }
9426
+ }
9427
+ function certificationDeadline(timeoutMs, external, label) {
9417
9428
  const controller = new AbortController();
9418
9429
  const relayAbort = () => {
9419
9430
  controller.abort(external?.reason);
@@ -9423,34 +9434,29 @@ async function awaitProofBounded(deps, transaction, external) {
9423
9434
  else external.addEventListener("abort", relayAbort, { once: true });
9424
9435
  }
9425
9436
  const timer = setTimeout(() => {
9426
- controller.abort(
9427
- new SphereError(
9428
- `inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
9429
- "AGGREGATOR_ERROR"
9430
- )
9431
- );
9432
- }, deps.timeoutMs);
9433
- try {
9434
- for (; ; ) {
9435
- try {
9436
- return await waitInclusionProof(
9437
- deps.client,
9438
- deps.trustBase,
9439
- deps.predicateVerifier,
9440
- transaction,
9441
- controller.signal,
9442
- deps.intervalMs
9443
- );
9444
- } catch (err) {
9445
- if (controller.signal.aborted) throw err;
9446
- if (!isTransientGatewayError(err)) throw err;
9447
- await pause(deps.intervalMs, controller.signal);
9448
- }
9437
+ controller.abort(new SphereError(`${label} within ${String(timeoutMs)} ms`, "AGGREGATOR_ERROR"));
9438
+ }, timeoutMs);
9439
+ return {
9440
+ signal: controller.signal,
9441
+ dispose: () => {
9442
+ clearTimeout(timer);
9443
+ external?.removeEventListener("abort", relayAbort);
9449
9444
  }
9450
- } finally {
9451
- clearTimeout(timer);
9452
- external?.removeEventListener("abort", relayAbort);
9453
- }
9445
+ };
9446
+ }
9447
+ async function awaitProofBounded(deps, transaction, signal) {
9448
+ return retryTransient(
9449
+ () => waitInclusionProof(
9450
+ deps.client,
9451
+ deps.trustBase,
9452
+ deps.predicateVerifier,
9453
+ transaction,
9454
+ signal,
9455
+ deps.intervalMs
9456
+ ),
9457
+ signal,
9458
+ deps.intervalMs
9459
+ );
9454
9460
  }
9455
9461
 
9456
9462
  // token-engine/errors.ts
@@ -9492,6 +9498,21 @@ var CheckpointTrustbaseMismatchError = class extends SphereError {
9492
9498
  }
9493
9499
  };
9494
9500
 
9501
+ // token-engine/certification-outcome.ts
9502
+ function certificationFailure(err, failLabel, submitError, submitRejectedCleanly) {
9503
+ if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
9504
+ return new TransferConflictError(
9505
+ `${failLabel}: the source state was already consumed by a different transaction (lost race \u2014 abort this intent and re-plan under a new transferId)`,
9506
+ err
9507
+ );
9508
+ }
9509
+ if (submitRejectedCleanly) return submitError;
9510
+ return new ProofUnconfirmedError(
9511
+ `${failLabel}: certification unconfirmed \u2014 the source spend may be on-chain; keep the intent open and resume under the same transferId`,
9512
+ submitError ?? err
9513
+ );
9514
+ }
9515
+
9495
9516
  // token-engine/token-blob.ts
9496
9517
  var TOKEN_BLOB_TAG = 39051n;
9497
9518
  var TOKEN_BLOB_VERSION = 1;
@@ -10014,10 +10035,28 @@ var SphereTokenEngine = class {
10014
10035
  * failure, not a resume case).
10015
10036
  */
10016
10037
  async submitAndAwaitProof(certificationData, transaction, failLabel, failCode, options) {
10038
+ const deadline = certificationDeadline(
10039
+ this.proofTimeoutMs,
10040
+ options?.signal,
10041
+ `${failLabel}: not certified`
10042
+ );
10043
+ try {
10044
+ return await this.submitThenProve(certificationData, transaction, failLabel, failCode, deadline.signal);
10045
+ } finally {
10046
+ deadline.dispose();
10047
+ }
10048
+ }
10049
+ /** One certification attempt under one deadline: the metered submit retries
10050
+ * transient answers instead of falling through to a proof that cannot exist. */
10051
+ async submitThenProve(certificationData, transaction, failLabel, failCode, signal) {
10017
10052
  let submitError = null;
10018
10053
  let submitRejectedCleanly = false;
10019
10054
  try {
10020
- const response = await this.deps.client.submitCertificationRequest(certificationData);
10055
+ const response = await retryTransient(
10056
+ () => this.deps.client.submitCertificationRequest(certificationData),
10057
+ signal,
10058
+ this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
10059
+ );
10021
10060
  if (response.status !== CertificationStatus.SUCCESS) {
10022
10061
  submitError = new SphereError(`${failLabel}: ${response.status}`, failCode);
10023
10062
  submitRejectedCleanly = CLEAN_REJECT_STATUSES.has(response.status);
@@ -10031,33 +10070,21 @@ var SphereTokenEngine = class {
10031
10070
  client: this.deps.client,
10032
10071
  trustBase: this.deps.trustBase,
10033
10072
  predicateVerifier: this.deps.predicateVerifier,
10034
- timeoutMs: this.proofTimeoutMs,
10035
10073
  intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
10036
10074
  // #683
10037
10075
  },
10038
10076
  transaction,
10039
- options?.signal
10077
+ signal
10040
10078
  );
10041
10079
  } catch (err) {
10042
- if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
10043
- throw new TransferConflictError(
10044
- `${failLabel}: the source state was already consumed by a different transaction (lost race \u2014 abort this intent and re-plan under a new transferId)`,
10045
- err
10046
- );
10047
- }
10048
- if (submitRejectedCleanly) throw submitError;
10049
- throw new ProofUnconfirmedError(
10050
- `${failLabel}: certification unconfirmed \u2014 the source spend may be on-chain; keep the intent open and resume under the same transferId`,
10051
- submitError ?? err
10052
- );
10080
+ throw certificationFailure(err, failLabel, submitError, submitRejectedCleanly);
10053
10081
  }
10054
10082
  }
10055
10083
  /**
10056
- * One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
10057
- * default deadline is unreliable — see lateDeliveringSignal — and a lost one
10058
- * means an op that never settles, which also wedges PaymentsFacade.stop().
10059
- * A deadline hit throws, and the caller maps that to ProofUnconfirmedError
10060
- * (keep-open), which is the correct posture: the spend may be on-chain.
10084
+ * One inclusion-proof wait, with the deadline OWNED here (#739) so it is bounded
10085
+ * and configurable rather than the SDK's fixed default. A hit throws, and the
10086
+ * caller maps it to ProofUnconfirmedError (keep-open) — the correct posture,
10087
+ * since the spend may be on-chain.
10061
10088
  */
10062
10089
  /** Fail fast if this engine's key does not own the token's current state. */
10063
10090
  assertOwned(token) {
@@ -11392,7 +11419,7 @@ function createMachineStores(kv) {
11392
11419
  }
11393
11420
 
11394
11421
  // modules/payments-v2/machine/TransferMachine.ts
11395
- var CERTIFY_WIDTH = 8;
11422
+ var CERTIFY_WIDTH = 20;
11396
11423
  var KEEP_OPEN_CODES = /* @__PURE__ */ new Set([
11397
11424
  "CERTIFICATION_UNCONFIRMED",
11398
11425
  "CHECKPOINT_PERSIST_FAILED",
@@ -11488,12 +11515,15 @@ var TransferMachine = class {
11488
11515
  payload: r.payload,
11489
11516
  ...r.payload.memo !== void 0 ? { memo: r.payload.memo } : {}
11490
11517
  };
11491
- const { committed, conflicts } = await this.resumeOps(ctx, engine, r);
11518
+ const outcomes = await this.resumeOps(ctx, engine, r);
11519
+ const { committed, cls, firstError } = summarize(outcomes);
11520
+ const conflicts = this.toConflicts(engine, r, outcomes);
11492
11521
  if (committed.length === 0 && conflicts.length > 0) throw conflicts[0].error;
11493
11522
  let deliveryPending = false;
11494
11523
  if (committed.length > 0) {
11495
11524
  deliveryPending = await this.deliverSet(r.transferId, r.payload.recipient, r.payload.memo, committed);
11496
11525
  }
11526
+ if (cls !== null && cls !== "conflict") throw firstError;
11497
11527
  await this.applyCommitted(engine, r.transferId, committed);
11498
11528
  if (conflicts.length > 0) {
11499
11529
  return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
@@ -11578,25 +11608,28 @@ var TransferMachine = class {
11578
11608
  );
11579
11609
  return { recipientBlob: outputs[0].blob.token, changeBlob: outputs[1].blob.token };
11580
11610
  }
11611
+ /** Raw outcomes; `summarize` alone decides what they mean — same as the send path. */
11581
11612
  async resumeOps(ctx, engine, r) {
11582
- const committed = [];
11583
- const conflicts = [];
11613
+ const outcomes = [];
11584
11614
  for (const op of buildOps(r.payload)) {
11585
11615
  const journaled = op.kind === "direct" ? r.journaled.get(op.opIndex) : void 0;
11586
11616
  if (journaled !== void 0) {
11587
- committed.push({ op, certified: true, recipientBlob: hexToBytes4(journaled.blobHex) });
11617
+ outcomes.push({ op, certified: true, recipientBlob: hexToBytes4(journaled.blobHex) });
11588
11618
  continue;
11589
11619
  }
11590
- const source = r.sources.get(op.sourceTokenId);
11591
- const outcome = await this.certifyOne(ctx, engine, op, source);
11592
- if (outcome.certified) {
11593
- committed.push(outcome);
11594
- continue;
11595
- }
11596
- if (classifyError(outcome.error) !== "conflict") throw outcome.error;
11597
- conflicts.push({ op, error: outcome.error, amount: conflictAmount(engine, r.payload, op, source) });
11620
+ outcomes.push(await this.certifyOne(ctx, engine, op, r.sources.get(op.sourceTokenId)));
11621
+ const last = outcomes[outcomes.length - 1];
11622
+ if (last.error !== void 0 && classifyError(last.error) !== "conflict") break;
11598
11623
  }
11599
- return { committed, conflicts };
11624
+ return outcomes;
11625
+ }
11626
+ /** Conflicted ops with the amount each leaves undelivered (#690 shortfall math). */
11627
+ toConflicts(engine, r, outcomes) {
11628
+ return outcomes.filter((o) => !o.certified && classifyError(o.error) === "conflict").map((o) => ({
11629
+ op: o.op,
11630
+ error: o.error,
11631
+ amount: conflictAmount(engine, r.payload, o.op, r.sources.get(o.op.sourceTokenId))
11632
+ }));
11600
11633
  }
11601
11634
  async deliverSet(transferId, recipientPubkey, memo, committed) {
11602
11635
  const journaled = committed.filter((o) => o.error === void 0 && o.recipientBlob !== void 0);