@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.
@@ -297,41 +297,35 @@ function resolveProofTimeoutMs(configured) {
297
297
  }
298
298
  return configured;
299
299
  }
300
- function lateDeliveringSignal(signal) {
301
- return new Proxy(signal, {
302
- get(target, prop) {
303
- if (prop === "addEventListener") {
304
- return (type, listener, options) => {
305
- if (type === "abort" && target.aborted) {
306
- queueMicrotask(() => {
307
- listener.call(target, new Event("abort"));
308
- });
309
- return;
310
- }
311
- target.addEventListener(type, listener, options);
312
- };
313
- }
314
- const value = Reflect.get(target, prop, target);
315
- return typeof value === "function" ? value.bind(target) : value;
316
- }
317
- });
300
+ var TRANSIENT_HTTP_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
301
+ function isTransientGatewayError(err) {
302
+ return err instanceof JsonRpcNetworkError && TRANSIENT_HTTP_STATUSES.has(err.status);
318
303
  }
319
- function rejectOnAbort(signal) {
320
- return new Promise((_resolve, reject) => {
321
- if (signal.aborted) {
322
- reject(signal.reason);
323
- return;
324
- }
304
+ function pause(ms, signal) {
305
+ return new Promise((resolve) => {
306
+ const timer = setTimeout(resolve, ms);
325
307
  signal.addEventListener(
326
308
  "abort",
327
309
  () => {
328
- reject(signal.reason);
310
+ clearTimeout(timer);
311
+ resolve();
329
312
  },
330
313
  { once: true }
331
314
  );
332
315
  });
333
316
  }
334
- async function awaitProofBounded(deps, transaction, external) {
317
+ async function retryTransient(attempt, signal, intervalMs) {
318
+ for (; ; ) {
319
+ try {
320
+ return await attempt();
321
+ } catch (err) {
322
+ if (signal.aborted) throw err;
323
+ if (!isTransientGatewayError(err)) throw err;
324
+ await pause(intervalMs, signal);
325
+ }
326
+ }
327
+ }
328
+ function certificationDeadline(timeoutMs, external, label) {
335
329
  const controller = new AbortController();
336
330
  const relayAbort = () => {
337
331
  controller.abort(external?.reason);
@@ -341,41 +335,29 @@ async function awaitProofBounded(deps, transaction, external) {
341
335
  else external.addEventListener("abort", relayAbort, { once: true });
342
336
  }
343
337
  const timer = setTimeout(() => {
344
- controller.abort(
345
- new SphereError(
346
- `inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
347
- "AGGREGATOR_ERROR"
348
- )
349
- );
350
- }, deps.timeoutMs);
351
- const wait = waitInclusionProof(
352
- deps.client,
353
- deps.trustBase,
354
- deps.predicateVerifier,
355
- transaction,
356
- lateDeliveringSignal(controller.signal),
338
+ controller.abort(new SphereError(`${label} within ${String(timeoutMs)} ms`, "AGGREGATOR_ERROR"));
339
+ }, timeoutMs);
340
+ return {
341
+ signal: controller.signal,
342
+ dispose: () => {
343
+ clearTimeout(timer);
344
+ external?.removeEventListener("abort", relayAbort);
345
+ }
346
+ };
347
+ }
348
+ async function awaitProofBounded(deps, transaction, signal) {
349
+ return retryTransient(
350
+ () => waitInclusionProof(
351
+ deps.client,
352
+ deps.trustBase,
353
+ deps.predicateVerifier,
354
+ transaction,
355
+ signal,
356
+ deps.intervalMs
357
+ ),
358
+ signal,
357
359
  deps.intervalMs
358
360
  );
359
- wait.catch(() => void 0);
360
- try {
361
- return await Promise.race([wait, rejectOnAbort(controller.signal)]);
362
- } finally {
363
- clearTimeout(timer);
364
- external?.removeEventListener("abort", relayAbort);
365
- }
366
- }
367
-
368
- // core/uuid.ts
369
- function randomUUID() {
370
- const c = globalThis.crypto;
371
- if (typeof c !== "undefined" && typeof c.randomUUID === "function") {
372
- return c.randomUUID();
373
- }
374
- const b = c.getRandomValues(new Uint8Array(16));
375
- b[6] = b[6] & 15 | 64;
376
- b[8] = b[8] & 63 | 128;
377
- const h = Array.from(b, (x) => x.toString(16).padStart(2, "0"));
378
- return `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}`;
379
361
  }
380
362
 
381
363
  // token-engine/errors.ts
@@ -417,6 +399,34 @@ var CheckpointTrustbaseMismatchError = class extends SphereError {
417
399
  }
418
400
  };
419
401
 
402
+ // token-engine/certification-outcome.ts
403
+ function certificationFailure(err, failLabel, submitError, submitRejectedCleanly) {
404
+ if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
405
+ return new TransferConflictError(
406
+ `${failLabel}: the source state was already consumed by a different transaction (lost race \u2014 abort this intent and re-plan under a new transferId)`,
407
+ err
408
+ );
409
+ }
410
+ if (submitRejectedCleanly) return submitError;
411
+ return new ProofUnconfirmedError(
412
+ `${failLabel}: certification unconfirmed \u2014 the source spend may be on-chain; keep the intent open and resume under the same transferId`,
413
+ submitError ?? err
414
+ );
415
+ }
416
+
417
+ // core/uuid.ts
418
+ function randomUUID() {
419
+ const c = globalThis.crypto;
420
+ if (typeof c !== "undefined" && typeof c.randomUUID === "function") {
421
+ return c.randomUUID();
422
+ }
423
+ const b = c.getRandomValues(new Uint8Array(16));
424
+ b[6] = b[6] & 15 | 64;
425
+ b[8] = b[8] & 63 | 128;
426
+ const h = Array.from(b, (x) => x.toString(16).padStart(2, "0"));
427
+ return `${h[0]}${h[1]}${h[2]}${h[3]}-${h[4]}${h[5]}-${h[6]}${h[7]}-${h[8]}${h[9]}-${h[10]}${h[11]}${h[12]}${h[13]}${h[14]}${h[15]}`;
428
+ }
429
+
420
430
  // token-engine/token-blob.ts
421
431
  var TOKEN_BLOB_TAG = 39051n;
422
432
  var TOKEN_BLOB_VERSION = 1;
@@ -1359,10 +1369,28 @@ var SphereTokenEngine = class {
1359
1369
  * failure, not a resume case).
1360
1370
  */
1361
1371
  async submitAndAwaitProof(certificationData, transaction, failLabel, failCode, options) {
1372
+ const deadline = certificationDeadline(
1373
+ this.proofTimeoutMs,
1374
+ options?.signal,
1375
+ `${failLabel}: not certified`
1376
+ );
1377
+ try {
1378
+ return await this.submitThenProve(certificationData, transaction, failLabel, failCode, deadline.signal);
1379
+ } finally {
1380
+ deadline.dispose();
1381
+ }
1382
+ }
1383
+ /** One certification attempt under one deadline: the metered submit retries
1384
+ * transient answers instead of falling through to a proof that cannot exist. */
1385
+ async submitThenProve(certificationData, transaction, failLabel, failCode, signal) {
1362
1386
  let submitError = null;
1363
1387
  let submitRejectedCleanly = false;
1364
1388
  try {
1365
- const response = await this.deps.client.submitCertificationRequest(certificationData);
1389
+ const response = await retryTransient(
1390
+ () => this.deps.client.submitCertificationRequest(certificationData),
1391
+ signal,
1392
+ this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
1393
+ );
1366
1394
  if (response.status !== CertificationStatus.SUCCESS) {
1367
1395
  submitError = new SphereError(`${failLabel}: ${response.status}`, failCode);
1368
1396
  submitRejectedCleanly = CLEAN_REJECT_STATUSES.has(response.status);
@@ -1376,33 +1404,21 @@ var SphereTokenEngine = class {
1376
1404
  client: this.deps.client,
1377
1405
  trustBase: this.deps.trustBase,
1378
1406
  predicateVerifier: this.deps.predicateVerifier,
1379
- timeoutMs: this.proofTimeoutMs,
1380
1407
  intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
1381
1408
  // #683
1382
1409
  },
1383
1410
  transaction,
1384
- options?.signal
1411
+ signal
1385
1412
  );
1386
1413
  } catch (err) {
1387
- if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
1388
- throw new TransferConflictError(
1389
- `${failLabel}: the source state was already consumed by a different transaction (lost race \u2014 abort this intent and re-plan under a new transferId)`,
1390
- err
1391
- );
1392
- }
1393
- if (submitRejectedCleanly) throw submitError;
1394
- throw new ProofUnconfirmedError(
1395
- `${failLabel}: certification unconfirmed \u2014 the source spend may be on-chain; keep the intent open and resume under the same transferId`,
1396
- submitError ?? err
1397
- );
1414
+ throw certificationFailure(err, failLabel, submitError, submitRejectedCleanly);
1398
1415
  }
1399
1416
  }
1400
1417
  /**
1401
- * One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
1402
- * default deadline is unreliable — see lateDeliveringSignal — and a lost one
1403
- * means an op that never settles, which also wedges PaymentsFacade.stop().
1404
- * A deadline hit throws, and the caller maps that to ProofUnconfirmedError
1405
- * (keep-open), which is the correct posture: the spend may be on-chain.
1418
+ * One inclusion-proof wait, with the deadline OWNED here (#739) so it is bounded
1419
+ * and configurable rather than the SDK's fixed default. A hit throws, and the
1420
+ * caller maps it to ProofUnconfirmedError (keep-open) — the correct posture,
1421
+ * since the spend may be on-chain.
1406
1422
  */
1407
1423
  /** Fail fast if this engine's key does not own the token's current state. */
1408
1424
  assertOwned(token) {