@escro/sdk 0.2.0 → 0.4.0

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
@@ -3,7 +3,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
5
  // src/client.ts
6
- import { Connection as Connection2, PublicKey as PublicKey2 } from "@solana/web3.js";
6
+ import { Connection as Connection2, PublicKey as PublicKey3 } from "@solana/web3.js";
7
7
 
8
8
  // ../shared/dist/index.js
9
9
  import { createHash } from "crypto";
@@ -22,20 +22,10 @@ var EscrowState = /* @__PURE__ */ ((EscrowState2) => {
22
22
  EscrowState2["REFUNDED"] = "REFUNDED";
23
23
  return EscrowState2;
24
24
  })(EscrowState || {});
25
- function canonicalize(value) {
26
- if (value === null) return "null";
27
- if (Array.isArray(value)) {
28
- return "[" + value.map((v) => v === void 0 ? "null" : canonicalize(v)).join(",") + "]";
29
- }
30
- if (typeof value === "object") {
31
- const obj = value;
32
- const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
33
- return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
34
- }
35
- return JSON.stringify(value);
36
- }
37
25
  var PROGRAM_ID_DEVNET = "EjLpJ3obUYk1f2KDensU7UdGBZTxSdxufm7CSnMA3H5C";
38
26
  var PROGRAM_ID_MAINNET = "4CwLNRF4mFXutiBGjtMHQBxh8whW8T8EyvJXdRhY1rvr";
27
+ var CURRENT_ACCOUNT_VERSION = 1;
28
+ var MAX_DISPUTE_REASON_BYTES = 200;
39
29
  var ONCHAIN_REFUND_GRACE_SECONDS = 900;
40
30
  var ONCHAIN_TIMEOUT_SECONDS = 86400;
41
31
  var CLOCK_SKEW_SECONDS = 60;
@@ -55,6 +45,26 @@ function assertProgramIdNotPlaceholder(id, cluster) {
55
45
  );
56
46
  }
57
47
  }
48
+ function canonicalize(value) {
49
+ if (value === null) return "null";
50
+ if (Array.isArray(value)) {
51
+ return "[" + value.map((v) => v === void 0 ? "null" : canonicalize(v)).join(",") + "]";
52
+ }
53
+ if (typeof value === "object") {
54
+ const obj = value;
55
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
56
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
57
+ }
58
+ return JSON.stringify(value);
59
+ }
60
+ function disputeReasonError(reason) {
61
+ if (reason.length === 0) return "reason must not be empty";
62
+ const bytes = new TextEncoder().encode(reason).length;
63
+ if (bytes > MAX_DISPUTE_REASON_BYTES) {
64
+ return `reason is ${String(bytes)} bytes in UTF-8; the program accepts at most ${String(MAX_DISPUTE_REASON_BYTES)}`;
65
+ }
66
+ return null;
67
+ }
58
68
  function hashBody(rawBody) {
59
69
  return createHash("sha256").update(rawBody, "utf8").digest("hex");
60
70
  }
@@ -100,6 +110,26 @@ function verifyBotDescriptorSignature(descriptor) {
100
110
  return false;
101
111
  }
102
112
  }
113
+ function spec(discriminator, escrowAccountIndex, cause) {
114
+ return {
115
+ discriminator: Uint8Array.from(discriminator),
116
+ escrowAccountIndex,
117
+ signerAccountIndex: 0,
118
+ cause
119
+ };
120
+ }
121
+ var ESCROW_INSTRUCTIONS = {
122
+ create_escrow: spec([253, 215, 165, 116, 36, 108, 68, 80], 4, null),
123
+ claim_task: spec([49, 222, 219, 238, 155, 68, 221, 136], 1, null),
124
+ submit_deliverable: spec([38, 137, 64, 44, 237, 11, 125, 101], 1, null),
125
+ raise_dispute: spec([41, 243, 1, 51, 150, 95, 246, 73], 1, null),
126
+ cancel_escrow: spec([156, 203, 54, 179, 38, 72, 33, 21], 3, "CANCELLED"),
127
+ release_payment: spec([24, 34, 191, 86, 145, 160, 183, 233], 3, "EXPLICIT_RELEASE"),
128
+ timeout_release: spec([98, 176, 26, 12, 83, 105, 154, 68], 3, "TIMEOUT_RELEASE"),
129
+ refund_buyer: spec([199, 139, 203, 146, 192, 150, 53, 218], 3, "DEADLINE_REFUND"),
130
+ arbitration_release: spec([118, 157, 141, 26, 104, 117, 94, 229], 4, "ARBITRATION_RELEASE"),
131
+ arbitration_refund: spec([255, 194, 84, 248, 185, 249, 120, 122], 4, "ARBITRATION_REFUND")
132
+ };
103
133
 
104
134
  // src/payments.ts
105
135
  var RAILS = ["escrow", "x402"];
@@ -114,9 +144,10 @@ var EscrowPaymentRouter = class {
114
144
 
115
145
  // src/errors.ts
116
146
  var EscroError = class extends Error {
117
- constructor(message, code = "ESCRO_ERROR") {
147
+ constructor(message, code = "ESCRO_ERROR", retryAfterMs2) {
118
148
  super(message);
119
149
  __publicField(this, "code", code);
150
+ __publicField(this, "retryAfterMs", retryAfterMs2);
120
151
  this.name = "EscroError";
121
152
  }
122
153
  };
@@ -147,6 +178,18 @@ var EscrowTimeoutError = class extends EscroError {
147
178
  this.name = "EscrowTimeoutError";
148
179
  }
149
180
  };
181
+ var EscroConfirmError = class extends EscroError {
182
+ constructor(address, signature, code, message, retryAfterMs2) {
183
+ super(
184
+ `Confirm failed for escrow ${address} (signature ${signature}): ${message}`,
185
+ code,
186
+ retryAfterMs2
187
+ );
188
+ __publicField(this, "address", address);
189
+ __publicField(this, "signature", signature);
190
+ this.name = "EscroConfirmError";
191
+ }
192
+ };
150
193
  var UnauthorizedError = class extends EscroError {
151
194
  constructor(message = "Unauthorized") {
152
195
  super(message, "UNAUTHORIZED");
@@ -177,7 +220,10 @@ async function handleError(res, address) {
177
220
  if (res.status === 404) throw new EscrowNotFoundError(address ?? msg);
178
221
  if (res.status === 409) throw new EscrowStateError(msg);
179
222
  if (res.status === 401 || res.status === 403) throw new UnauthorizedError(msg);
180
- throw new EscroError(msg, body.code ?? "UNKNOWN");
223
+ throw new EscroError(msg, body.code ?? "UNKNOWN", retryAfterMs(res.headers.get("retry-after")));
224
+ }
225
+ function retryAfterMs(header) {
226
+ return header !== null && /^\d+$/.test(header.trim()) ? Number(header.trim()) * 1e3 : void 0;
181
227
  }
182
228
  var ApiClient = class {
183
229
  constructor(baseUrl, wallet) {
@@ -253,6 +299,22 @@ var ApiClient = class {
253
299
  const body = await res.json();
254
300
  return body.wallets;
255
301
  }
302
+ /**
303
+ * GET /rep/:wallet : one wallet's EscroRep RAW components (ARCHITECTURE §9).
304
+ * Unauthenticated public read (no auth headers). Unknown wallets return zeroed
305
+ * components, never 404; a 404 means the deployment mounted no reputation
306
+ * source and surfaces as {@link ReputationUnavailableError}. Shares the public
307
+ * per-wallet rate budget with every other caller, so fan-outs should use
308
+ * {@link listReputations} instead.
309
+ */
310
+ async getReputation(wallet) {
311
+ const res = await fetch(this.url(`/rep/${encodeURIComponent(wallet)}`));
312
+ if (!res.ok) {
313
+ if (res.status === 404) throw new ReputationUnavailableError();
314
+ await handleError(res);
315
+ }
316
+ return await res.json();
317
+ }
256
318
  /**
257
319
  * POST /v1/escrows — create escrow, returns unsigned tx + ids.
258
320
  * Requires auth.
@@ -312,8 +374,11 @@ var ApiClient = class {
312
374
  return res.json();
313
375
  }
314
376
  /**
315
- * POST /v1/escrows/:address/release — DB-only state update.
316
- * Requires auth (buyer). No on-chain tx returned.
377
+ * POST /v1/escrows/:address/release: records the landed release once the API's RPC shows
378
+ * the escrow closed. Requires auth (buyer). No on-chain tx returned. A 409
379
+ * `ESCROW_NOT_SETTLED`, a 5xx or a 429 throws an {@link EscroError} with the server's code,
380
+ * or `HTTP_<status>` when the answer carries none; {@link Escro.releasePaymentWithOutcome}
381
+ * decides which to retry. Other refusals throw as every route does.
317
382
  */
318
383
  async releasePaymentDb(address) {
319
384
  const path = `/v1/escrows/${address}/release`;
@@ -321,7 +386,19 @@ var ApiClient = class {
321
386
  method: "POST",
322
387
  headers: authHeaders(this.wallet, "POST", path)
323
388
  });
324
- if (!res.ok) await handleError(res, address);
389
+ if (res.ok) return;
390
+ if (res.status !== 409 && res.status !== 429 && res.status < 500) {
391
+ await handleError(res, address);
392
+ }
393
+ const body = await res.json().catch(() => ({}));
394
+ if (res.status === 409 && body.code !== "ESCROW_NOT_SETTLED") {
395
+ throw new EscrowStateError(body.error ?? `HTTP ${String(res.status)}`);
396
+ }
397
+ throw new EscroError(
398
+ body.error ?? `HTTP ${String(res.status)}`,
399
+ body.code ?? `HTTP_${String(res.status)}`,
400
+ retryAfterMs(res.headers.get("retry-after"))
401
+ );
325
402
  }
326
403
  /**
327
404
  * POST /v1/escrows/:address/cancel — returns unsigned tx.
@@ -342,7 +419,7 @@ var ApiClient = class {
342
419
  */
343
420
  async raiseDispute(address, params) {
344
421
  const path = `/v1/escrows/${address}/dispute`;
345
- const rawBody = JSON.stringify({ reason: params.reason, evidence: params.evidence });
422
+ const rawBody = JSON.stringify({ reason: params.reason });
346
423
  const res = await fetch(this.url(path), {
347
424
  method: "POST",
348
425
  headers: {
@@ -354,6 +431,35 @@ var ApiClient = class {
354
431
  if (!res.ok) await handleError(res, address);
355
432
  return res.json();
356
433
  }
434
+ /**
435
+ * POST /v1/escrows/:address/confirm: the API records the create, claim, submit,
436
+ * cancel or dispute the landed `signature` carries, after verifying it on chain.
437
+ * A non-2xx answer throws an {@link EscroError} with the server's code, or
438
+ * `HTTP_<status>` when the answer carries none; a network failure rejects with
439
+ * fetch's own error. {@link Escro.confirm} decides which to retry.
440
+ */
441
+ async confirm(address, signature, evidence) {
442
+ const path = `/v1/escrows/${address}/confirm`;
443
+ const rawBody = JSON.stringify({ signature, evidence });
444
+ const res = await fetch(this.url(path), {
445
+ method: "POST",
446
+ headers: {
447
+ ...authHeaders(this.wallet, "POST", path, rawBody),
448
+ "content-type": "application/json"
449
+ },
450
+ body: rawBody
451
+ });
452
+ if (!res.ok) {
453
+ const body2 = await res.json().catch(() => ({}));
454
+ throw new EscroError(
455
+ body2.error ?? `HTTP ${String(res.status)}`,
456
+ body2.code ?? `HTTP_${String(res.status)}`,
457
+ retryAfterMs(res.headers.get("retry-after"))
458
+ );
459
+ }
460
+ const body = await res.json();
461
+ return deserialize(body.escrow);
462
+ }
357
463
  /**
358
464
  * GET /v1/stats — public protocol-wide stats.
359
465
  * No auth required. Upstream typing in {@link Escro.getStats} narrows the
@@ -401,7 +507,7 @@ async function pollUntil(address, fetcher, predicate, timeoutMs = MS_24_H) {
401
507
  import { createHash as createHash2 } from "crypto";
402
508
  import {
403
509
  ComputeBudgetProgram,
404
- PublicKey,
510
+ PublicKey as PublicKey2,
405
511
  Transaction,
406
512
  TransactionInstruction
407
513
  } from "@solana/web3.js";
@@ -411,6 +517,59 @@ import {
411
517
  getAssociatedTokenAddressSync,
412
518
  createAssociatedTokenAccountIdempotentInstruction
413
519
  } from "@solana/spl-token";
520
+
521
+ // ../shared/dist/solana.js
522
+ import { PublicKey } from "@solana/web3.js";
523
+ import bs583 from "bs58";
524
+ var utf8 = (s) => new TextEncoder().encode(s);
525
+ function findPlatformConfigPda(programId) {
526
+ return PublicKey.findProgramAddressSync([utf8("config")], programId);
527
+ }
528
+ function derivePlatformConfigPda(programId) {
529
+ return findPlatformConfigPda(programId)[0];
530
+ }
531
+ var PLATFORM_CONFIG_LAYOUT = {
532
+ version: 8,
533
+ fee_bps: 9,
534
+ oracle_authority: 11,
535
+ usdc_mint: 43,
536
+ treasury: 75,
537
+ bump: 107,
538
+ length: 108
539
+ };
540
+ var PLATFORM_CONFIG_DISCRIMINATOR = [
541
+ 160,
542
+ 78,
543
+ 128,
544
+ 0,
545
+ 248,
546
+ 83,
547
+ 230,
548
+ 160
549
+ ];
550
+ function decodePlatformConfig(data) {
551
+ const L = PLATFORM_CONFIG_LAYOUT;
552
+ if (data.length !== L.length) {
553
+ throw new Error(
554
+ `PlatformConfig account is ${String(data.length)} bytes, expected ${String(L.length)}`
555
+ );
556
+ }
557
+ if (PLATFORM_CONFIG_DISCRIMINATOR.some((byte2, i) => data[i] !== byte2)) {
558
+ throw new Error("PlatformConfig discriminator mismatch: not a PlatformConfig account");
559
+ }
560
+ const byte = (at) => data[at] ?? 0;
561
+ const key = (at) => bs583.encode(data.subarray(at, at + 32));
562
+ return {
563
+ version: byte(L.version),
564
+ feeBps: byte(L.fee_bps) | byte(L.fee_bps + 1) << 8,
565
+ oracleAuthority: key(L.oracle_authority),
566
+ usdcMint: key(L.usdc_mint),
567
+ treasury: key(L.treasury),
568
+ bump: byte(L.bump)
569
+ };
570
+ }
571
+
572
+ // src/solana.ts
414
573
  function disc(name) {
415
574
  return Buffer.from(createHash2("sha256").update(`global:${name}`).digest()).subarray(0, 8);
416
575
  }
@@ -418,7 +577,7 @@ var DISC_RELEASE_PAYMENT = disc("release_payment");
418
577
  function resolveProgramId(cluster) {
419
578
  const id = getProgramId(cluster);
420
579
  assertProgramIdNotPlaceholder(id, cluster);
421
- return new PublicKey(id);
580
+ return new PublicKey2(id);
422
581
  }
423
582
  function allowedProgramIds(programId) {
424
583
  return /* @__PURE__ */ new Set([
@@ -451,25 +610,17 @@ var FORBIDDEN_TOKEN_IX_TAGS = /* @__PURE__ */ new Set([
451
610
  15
452
611
  // BurnChecked
453
612
  ]);
454
- var EXPECTED_PLATFORM_CONFIG_VERSION = 1;
455
- var VERSION_OFFSET = 8;
456
- var MINT_OFFSET = 43;
457
- var TREASURY_OFFSET = 75;
458
613
  async function fetchPlatformConfig(connection, cluster) {
459
- const programId = resolveProgramId(cluster);
460
- const [pda] = PublicKey.findProgramAddressSync([Buffer.from("config")], programId);
614
+ const pda = derivePlatformConfigPda(resolveProgramId(cluster));
461
615
  const info = await connection.getAccountInfo(pda);
462
616
  if (!info) throw new Error("PlatformConfig account not found on-chain");
463
- const version = info.data[VERSION_OFFSET];
464
- if (version !== EXPECTED_PLATFORM_CONFIG_VERSION) {
617
+ const config = decodePlatformConfig(info.data);
618
+ if (config.version !== CURRENT_ACCOUNT_VERSION) {
465
619
  throw new Error(
466
- `PlatformConfig version mismatch: expected ${String(EXPECTED_PLATFORM_CONFIG_VERSION)}, got ${String(version)}`
620
+ `PlatformConfig version mismatch: expected ${String(CURRENT_ACCOUNT_VERSION)}, got ${String(config.version)}`
467
621
  );
468
622
  }
469
- return {
470
- mint: new PublicKey(info.data.subarray(MINT_OFFSET, MINT_OFFSET + 32)),
471
- treasury: new PublicKey(info.data.subarray(TREASURY_OFFSET, TREASURY_OFFSET + 32))
472
- };
623
+ return { mint: new PublicKey2(config.usdcMint), treasury: new PublicKey2(config.treasury) };
473
624
  }
474
625
  function validateTransaction(tx, cluster) {
475
626
  const programId = resolveProgramId(cluster);
@@ -513,7 +664,7 @@ async function signAndSend(base64Tx, wallet, connection, cluster) {
513
664
  async function buildReleasePaymentTx(params) {
514
665
  const { buyer, escrowPda, worker, mint, treasury, connection, cluster } = params;
515
666
  const programId = resolveProgramId(cluster);
516
- const [platformConfig] = PublicKey.findProgramAddressSync([Buffer.from("config")], programId);
667
+ const platformConfig = derivePlatformConfigPda(programId);
517
668
  const vault = getAssociatedTokenAddressSync(
518
669
  mint,
519
670
  escrowPda,
@@ -563,6 +714,33 @@ async function buildReleasePaymentTx(params) {
563
714
  }
564
715
 
565
716
  // src/client.ts
717
+ var CONFIRM_WINDOW_MS = 3e4;
718
+ var CONFIRM_FIRST_DELAY_MS = 500;
719
+ var CONFIRM_MAX_DELAY_MS = 4e3;
720
+ var RETRYABLE_CONFIRM_CODES = [
721
+ "TX_NOT_FOUND",
722
+ "RPC_UNAVAILABLE",
723
+ "HTTP_502",
724
+ "HTTP_503",
725
+ "HTTP_504",
726
+ "RATE_LIMITED",
727
+ "HTTP_429"
728
+ ];
729
+ function isRetryableConfirmCode(code) {
730
+ return RETRYABLE_CONFIRM_CODES.includes(code);
731
+ }
732
+ function isRetryableReleaseCode(code) {
733
+ return code === "ESCROW_NOT_SETTLED" || isRetryableConfirmCode(code);
734
+ }
735
+ function isUrl(value) {
736
+ try {
737
+ new URL(value);
738
+ return true;
739
+ } catch {
740
+ return false;
741
+ }
742
+ }
743
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
566
744
  var Escro = class {
567
745
  constructor(options) {
568
746
  __publicField(this, "connection");
@@ -578,29 +756,40 @@ var Escro = class {
578
756
  // ── Buyer Bot ────────────────────────────────────────────────────────────────
579
757
  /**
580
758
  * Create a new escrow: registers with the API, signs and submits the
581
- * on-chain `create_escrow` transaction.
759
+ * on-chain `create_escrow` transaction, then confirms it with the API.
582
760
  *
583
761
  * @returns `{ escrowId, escrowPda, signature }` — use `escrowPda` for all subsequent calls.
584
762
  */
585
763
  async createEscrow(params) {
586
764
  const { escrowId, escrowPda, unsignedTx } = await this.api.createEscrow(params);
587
- const signature = await signAndSend(
588
- unsignedTx,
589
- this.options.wallet,
590
- this.connection,
591
- this.cluster
592
- );
765
+ const signature = await this.send(unsignedTx);
766
+ await this.confirm(escrowPda, signature);
593
767
  return { escrowId, escrowPda, signature };
594
768
  }
595
769
  /**
596
770
  * Release payment to the worker. Buyer must be the wallet in the constructor.
597
771
  * Builds and submits the `release_payment` on-chain tx FIRST (the chain is the
598
- * source of truth), then confirms DB state. The DB confirmation is best-effort:
599
- * the oracle sweep reconciles it if it fails.
772
+ * source of truth), then has the API record it, retrying as
773
+ * {@link Escro.releasePaymentWithOutcome} does. Use that method to see whether the
774
+ * API recorded the release.
600
775
  *
601
776
  * @returns Solana transaction signature.
602
777
  */
603
778
  async releasePayment(address) {
779
+ const { signature } = await this.releasePaymentWithOutcome(address);
780
+ return signature;
781
+ }
782
+ /**
783
+ * {@link Escro.releasePayment}, reporting whether the API recorded the release.
784
+ * `ESCROW_NOT_SETTLED` (the API's RPC does not yet show the escrow closed), the codes
785
+ * {@link Escro.confirm} retries and a network failure are retried for about 30 s. A
786
+ * failure never throws: the release already landed, so the signature is returned with
787
+ * `confirm.landed: false` and an `EscroConfirmError` (the API's code, or
788
+ * `NETWORK_ERROR` when the last call never reached it).
789
+ *
790
+ * @returns `{ signature, confirm }`.
791
+ */
792
+ async releasePaymentWithOutcome(address) {
604
793
  const escrow = await this.api.getEscrow(address);
605
794
  if (!escrow.taker) {
606
795
  throw new EscrowNotFoundError(`Escrow ${address} has no assigned worker`);
@@ -608,8 +797,8 @@ var Escro = class {
608
797
  const { treasury, mint } = await fetchPlatformConfig(this.connection, this.cluster);
609
798
  const unsignedTx = await buildReleasePaymentTx({
610
799
  buyer: this.options.wallet.publicKey,
611
- escrowPda: new PublicKey2(address),
612
- worker: new PublicKey2(escrow.taker),
800
+ escrowPda: new PublicKey3(address),
801
+ worker: new PublicKey3(escrow.taker),
613
802
  mint,
614
803
  treasury,
615
804
  connection: this.connection,
@@ -622,31 +811,47 @@ var Escro = class {
622
811
  this.cluster
623
812
  );
624
813
  try {
625
- await this.api.releasePaymentDb(address);
626
- } catch {
814
+ await this.retryRecord(
815
+ address,
816
+ signature,
817
+ () => this.api.releasePaymentDb(address),
818
+ isRetryableReleaseCode
819
+ );
820
+ } catch (err) {
821
+ if (!(err instanceof EscroConfirmError)) throw err;
822
+ return { signature, confirm: { landed: false, error: err } };
627
823
  }
628
- return signature;
824
+ return { signature, confirm: { landed: true } };
629
825
  }
630
826
  /**
631
827
  * Raise a dispute for an escrow. Either the buyer or the assigned worker
632
- * may call this. Signs and submits the `raise_dispute` on-chain tx.
828
+ * may call this. Signs and submits the `raise_dispute` on-chain tx, then confirms it with the API.
633
829
  *
634
830
  * @returns Solana transaction signature.
635
831
  */
636
832
  async raiseDispute(address, params) {
637
- const { unsignedTx } = await this.api.raiseDispute(address, params);
638
- return signAndSend(unsignedTx, this.options.wallet, this.connection, this.cluster);
833
+ const reasonError = disputeReasonError(params.reason);
834
+ if (reasonError !== null) throw new EscroError(reasonError, "VALIDATION_ERROR");
835
+ if (params.evidence !== void 0 && !isUrl(params.evidence)) {
836
+ throw new EscroError(`evidence must be a URL: ${params.evidence}`, "VALIDATION_ERROR");
837
+ }
838
+ const { unsignedTx } = await this.api.raiseDispute(address, { reason: params.reason });
839
+ const signature = await this.send(unsignedTx);
840
+ await this.confirm(address, signature, {
841
+ ...params.evidence !== void 0 && { evidence: params.evidence }
842
+ });
843
+ return signature;
639
844
  }
640
845
  /**
641
846
  * Cancel a funded escrow before any worker has claimed it.
642
847
  * Buyer must be the wallet in the constructor. Only valid when state is `FUNDED`.
643
- * Signs and submits the `cancel_escrow` on-chain tx; full USDC refund to buyer.
848
+ * Signs and submits the `cancel_escrow` on-chain tx, then confirms it with the API; full USDC refund to buyer.
644
849
  *
645
850
  * @returns Solana transaction signature.
646
851
  */
647
852
  async cancelEscrow(address) {
648
853
  const { unsignedTx } = await this.api.cancelEscrow(address);
649
- return signAndSend(unsignedTx, this.options.wallet, this.connection, this.cluster);
854
+ return this.sendAndConfirm(address, unsignedTx);
650
855
  }
651
856
  // ── Payments (dual-path; x402 reserved) ───────────────────────────────────────
652
857
  /**
@@ -671,23 +876,23 @@ var Escro = class {
671
876
  // ── Worker Bot ───────────────────────────────────────────────────────────────
672
877
  /**
673
878
  * Claim an assigned task. Worker must be the wallet in the constructor.
674
- * Signs and submits the `claim_task` on-chain tx.
879
+ * Signs and submits the `claim_task` on-chain tx, then confirms it with the API.
675
880
  *
676
881
  * @returns Solana transaction signature.
677
882
  */
678
883
  async claimTask(address) {
679
884
  const { unsignedTx } = await this.api.claimTask(address);
680
- return signAndSend(unsignedTx, this.options.wallet, this.connection, this.cluster);
885
+ return this.sendAndConfirm(address, unsignedTx);
681
886
  }
682
887
  /**
683
888
  * Submit a deliverable for the buyer to review. Worker must be the wallet.
684
- * Signs and submits the `submit_deliverable` on-chain tx.
889
+ * Signs and submits the `submit_deliverable` on-chain tx, then confirms it with the API.
685
890
  *
686
891
  * @returns Solana transaction signature.
687
892
  */
688
893
  async submitDeliverable(address, params) {
689
894
  const { unsignedTx } = await this.api.submitDeliverable(address, params);
690
- return signAndSend(unsignedTx, this.options.wallet, this.connection, this.cluster);
895
+ return this.sendAndConfirm(address, unsignedTx);
691
896
  }
692
897
  /**
693
898
  * List tasks assigned to the wallet's public key.
@@ -797,6 +1002,70 @@ var Escro = class {
797
1002
  return data;
798
1003
  }
799
1004
  // ── Shared ───────────────────────────────────────────────────────────────────
1005
+ /**
1006
+ * Ask the API to record the create, claim, submit, cancel or dispute that the
1007
+ * landed `signature` carries; the API verifies it on chain first. Retries a
1008
+ * not-yet-visible transaction, an RPC failure, a gateway error or a rate limit
1009
+ * for about 30 s (waiting at least a sent Retry-After), never a rejection. Throws
1010
+ * {@link EscroConfirmError}; calling again is safe (a replay of a recorded
1011
+ * transition succeeds).
1012
+ */
1013
+ async confirm(address, signature, opts = {}) {
1014
+ return this.retryRecord(
1015
+ address,
1016
+ signature,
1017
+ () => this.api.confirm(address, signature, opts.evidence),
1018
+ isRetryableConfirmCode
1019
+ );
1020
+ }
1021
+ // Calls `record` until the API records the landed `signature`: a code `retryable` accepts or
1022
+ // a network failure is retried for about 30 s (waiting at least a sent Retry-After), any
1023
+ // other refusal is not. Throws EscroConfirmError.
1024
+ async retryRecord(address, signature, record, retryable) {
1025
+ const deadline = Date.now() + CONFIRM_WINDOW_MS;
1026
+ let delay = CONFIRM_FIRST_DELAY_MS;
1027
+ for (; ; ) {
1028
+ let code;
1029
+ let message;
1030
+ let retryAfterMs2;
1031
+ let wait = delay;
1032
+ try {
1033
+ return await record();
1034
+ } catch (err) {
1035
+ if (err instanceof EscroError) {
1036
+ if (!retryable(err.code)) {
1037
+ throw new EscroConfirmError(
1038
+ address,
1039
+ signature,
1040
+ err.code,
1041
+ err.message,
1042
+ err.retryAfterMs
1043
+ );
1044
+ }
1045
+ code = err.code;
1046
+ message = err.message;
1047
+ retryAfterMs2 = err.retryAfterMs;
1048
+ wait = Math.max(delay, err.retryAfterMs ?? 0);
1049
+ } else {
1050
+ code = "NETWORK_ERROR";
1051
+ message = err instanceof Error ? err.message : String(err);
1052
+ }
1053
+ }
1054
+ if (Date.now() + wait > deadline) {
1055
+ throw new EscroConfirmError(address, signature, code, message, retryAfterMs2);
1056
+ }
1057
+ await sleep2(wait);
1058
+ delay = Math.min(delay * 2, CONFIRM_MAX_DELAY_MS);
1059
+ }
1060
+ }
1061
+ send(unsignedTx) {
1062
+ return signAndSend(unsignedTx, this.options.wallet, this.connection, this.cluster);
1063
+ }
1064
+ async sendAndConfirm(address, unsignedTx) {
1065
+ const signature = await this.send(unsignedTx);
1066
+ await this.confirm(address, signature);
1067
+ return signature;
1068
+ }
800
1069
  /**
801
1070
  * Fetch an escrow by its PDA address.
802
1071
  * If `options.poll` is true, polls with exponential backoff until the escrow
@@ -930,9 +1199,9 @@ function createBittensorSubnetAdapter(config) {
930
1199
  id,
931
1200
  capabilities: config.capabilities,
932
1201
  healthCheck: config.checkHealth,
933
- async execute(spec) {
1202
+ async execute(spec2) {
934
1203
  const budgetTao = await config.swap(config.taskBudgetMicroUsdc);
935
- const content = await config.callSubnet(spec, budgetTao);
1204
+ const content = await config.callSubnet(spec2, budgetTao);
936
1205
  const contentHash = sha256Hex2(content);
937
1206
  const proofUri = await config.pin(content);
938
1207
  return {
@@ -1015,16 +1284,16 @@ function scoreGradientsDemoDeliverable(content) {
1015
1284
  }
1016
1285
  return matches / expected.length;
1017
1286
  }
1018
- function referencePlaceholder(spec) {
1287
+ function referencePlaceholder(spec2) {
1019
1288
  return canonicalize({
1020
1289
  checkpoint: "reference-sn56-checkpoint",
1021
1290
  eval: { metric: "accuracy", score: 0 },
1022
1291
  note: "reference stub - replace the callSubnet port with a real SN56 miner call.",
1023
- taskType: spec.taskType
1292
+ taskType: spec2.taskType
1024
1293
  });
1025
1294
  }
1026
- var defaultCallSubnet = (spec) => Promise.resolve(
1027
- spec.metadata.demo === GRADIENTS_DEMO_MARKER ? solveGradientsDemo() : referencePlaceholder(spec)
1295
+ var defaultCallSubnet = (spec2) => Promise.resolve(
1296
+ spec2.metadata.demo === GRADIENTS_DEMO_MARKER ? solveGradientsDemo() : referencePlaceholder(spec2)
1028
1297
  );
1029
1298
  function createGradientsAdapter(config = {}) {
1030
1299
  return createBittensorSubnetAdapter({
@@ -1136,17 +1405,17 @@ function scoreRidgesDemoDeliverable(content) {
1136
1405
  }
1137
1406
  return coveredInputs.size / DEMO_INPUTS.length;
1138
1407
  }
1139
- function referencePlaceholder2(spec) {
1408
+ function referencePlaceholder2(spec2) {
1140
1409
  return canonicalize({
1141
1410
  language: "typescript",
1142
1411
  solution: "// reference stub - replace the runAgent port with a real Ridges agent call.",
1143
1412
  passed: false,
1144
1413
  tests: [],
1145
- taskType: spec.taskType
1414
+ taskType: spec2.taskType
1146
1415
  });
1147
1416
  }
1148
- var defaultRunAgent = (spec) => Promise.resolve(
1149
- spec.metadata.demo === RIDGES_DEMO_MARKER ? solveRidgesDemo() : referencePlaceholder2(spec)
1417
+ var defaultRunAgent = (spec2) => Promise.resolve(
1418
+ spec2.metadata.demo === RIDGES_DEMO_MARKER ? solveRidgesDemo() : referencePlaceholder2(spec2)
1150
1419
  );
1151
1420
  var defaultPin2 = (content) => Promise.resolve(`ipfs://placeholder-${sha256Hex3(content).slice(0, 16)}`);
1152
1421
  function createRidgesAdapter(config = {}) {
@@ -1159,8 +1428,8 @@ function createRidgesAdapter(config = {}) {
1159
1428
  id: service,
1160
1429
  capabilities,
1161
1430
  healthCheck: checkHealth,
1162
- async execute(spec) {
1163
- const content = await runAgent(spec);
1431
+ async execute(spec2) {
1432
+ const content = await runAgent(spec2);
1164
1433
  const contentHash = sha256Hex3(content);
1165
1434
  const proofUri = await pin(content);
1166
1435
  return {
@@ -1269,15 +1538,15 @@ function score404genDemoDeliverable(content) {
1269
1538
  }
1270
1539
  return Math.min(batch.assets.length / DEMO_EXPECTATION.requestedAssets, 1);
1271
1540
  }
1272
- function referencePlaceholder3(spec) {
1541
+ function referencePlaceholder3(spec2) {
1273
1542
  return canonicalize({
1274
1543
  assets: [{ format: "GLB", polygonCount: 0, uri: "ipfs://placeholder" }],
1275
1544
  note: "reference stub - replace the callSubnet port with a real SN17 miner call.",
1276
- taskType: spec.taskType
1545
+ taskType: spec2.taskType
1277
1546
  });
1278
1547
  }
1279
- var defaultCallSubnet2 = (spec) => Promise.resolve(
1280
- spec.metadata.demo === GEN404_DEMO_MARKER ? solveGen404Demo() : referencePlaceholder3(spec)
1548
+ var defaultCallSubnet2 = (spec2) => Promise.resolve(
1549
+ spec2.metadata.demo === GEN404_DEMO_MARKER ? solveGen404Demo() : referencePlaceholder3(spec2)
1281
1550
  );
1282
1551
  function create404genAdapter(config = {}) {
1283
1552
  return createBittensorSubnetAdapter({
@@ -1377,17 +1646,17 @@ function scoreLiumDemoDeliverable(content) {
1377
1646
  }
1378
1647
  return parsed.artifact === computeLiumDemoArtifact() ? 1 : 0;
1379
1648
  }
1380
- function referencePlaceholder4(spec) {
1649
+ function referencePlaceholder4(spec2) {
1381
1650
  return canonicalize({
1382
1651
  job: "lium-sn51-generic",
1383
1652
  status: "completed",
1384
1653
  artifact: null,
1385
1654
  note: "reference stub - replace the runJob port with a real SN51 GPU-rental call.",
1386
- taskType: spec.taskType
1655
+ taskType: spec2.taskType
1387
1656
  });
1388
1657
  }
1389
- var defaultRunJob = (spec) => Promise.resolve(
1390
- spec.metadata.demo === LIUM_DEMO_MARKER ? solveLiumDemo() : referencePlaceholder4(spec)
1658
+ var defaultRunJob = (spec2) => Promise.resolve(
1659
+ spec2.metadata.demo === LIUM_DEMO_MARKER ? solveLiumDemo() : referencePlaceholder4(spec2)
1391
1660
  );
1392
1661
  function createLiumAdapter(config = {}) {
1393
1662
  return createBittensorSubnetAdapter({
@@ -1496,17 +1765,17 @@ function scoreAkashDemoDeliverable(content) {
1496
1765
  }
1497
1766
  return parsed.artifact === computeAkashDemoArtifact() ? 1 : 0;
1498
1767
  }
1499
- function referencePlaceholder5(spec) {
1768
+ function referencePlaceholder5(spec2) {
1500
1769
  return canonicalize({
1501
1770
  job: "akash-generic",
1502
1771
  status: "completed",
1503
1772
  artifact: null,
1504
1773
  note: "reference stub - replace the runDeployment port with a real Akash deployment call.",
1505
- taskType: spec.taskType
1774
+ taskType: spec2.taskType
1506
1775
  });
1507
1776
  }
1508
- var defaultRunDeployment = (spec) => Promise.resolve(
1509
- spec.metadata.demo === AKASH_DEMO_MARKER ? solveAkashDemo() : referencePlaceholder5(spec)
1777
+ var defaultRunDeployment = (spec2) => Promise.resolve(
1778
+ spec2.metadata.demo === AKASH_DEMO_MARKER ? solveAkashDemo() : referencePlaceholder5(spec2)
1510
1779
  );
1511
1780
  function createAkashAdapter(config = {}) {
1512
1781
  const capabilities = config.capabilities ?? [...AKASH_CAPABILITIES];
@@ -1519,9 +1788,9 @@ function createAkashAdapter(config = {}) {
1519
1788
  id: AKASH_ADAPTER_ID,
1520
1789
  capabilities,
1521
1790
  healthCheck: checkHealth,
1522
- async execute(spec) {
1791
+ async execute(spec2) {
1523
1792
  const budgetAkt = await swap(taskBudgetMicroUsdc);
1524
- const content = await runDeployment(spec, budgetAkt);
1793
+ const content = await runDeployment(spec2, budgetAkt);
1525
1794
  const contentHash = sha256Hex4(content);
1526
1795
  const proofUri = await pin(content);
1527
1796
  return {
@@ -1638,17 +1907,17 @@ function scoreRenderDemoDeliverable(content) {
1638
1907
  }
1639
1908
  return parsed.frame === computeRenderDemoArtifact() ? 1 : 0;
1640
1909
  }
1641
- function referencePlaceholder6(spec) {
1910
+ function referencePlaceholder6(spec2) {
1642
1911
  return canonicalize({
1643
1912
  job: "render-generic",
1644
1913
  status: "completed",
1645
1914
  frame: null,
1646
1915
  note: "reference stub - replace the runJob port with a real Render Network job submission.",
1647
- taskType: spec.taskType
1916
+ taskType: spec2.taskType
1648
1917
  });
1649
1918
  }
1650
- var defaultRunJob2 = (spec) => Promise.resolve(
1651
- spec.metadata.demo === RENDER_DEMO_MARKER ? solveRenderDemo() : referencePlaceholder6(spec)
1919
+ var defaultRunJob2 = (spec2) => Promise.resolve(
1920
+ spec2.metadata.demo === RENDER_DEMO_MARKER ? solveRenderDemo() : referencePlaceholder6(spec2)
1652
1921
  );
1653
1922
  function createRenderAdapter(config = {}) {
1654
1923
  const service = config.service ?? DEFAULT_SERVICE2;
@@ -1662,9 +1931,9 @@ function createRenderAdapter(config = {}) {
1662
1931
  id: service,
1663
1932
  capabilities,
1664
1933
  healthCheck: checkHealth,
1665
- async execute(spec) {
1934
+ async execute(spec2) {
1666
1935
  const budgetRender = await swap(taskBudgetMicroUsdc);
1667
- const content = await runJob(spec, budgetRender);
1936
+ const content = await runJob(spec2, budgetRender);
1668
1937
  const contentHash = sha256Hex5(content);
1669
1938
  const proofUri = await pin(content);
1670
1939
  return {
@@ -1691,15 +1960,15 @@ function sha256Hex6(content) {
1691
1960
  var LCG_MULT = 1103515245n;
1692
1961
  var LCG_INC = 12345n;
1693
1962
  var LCG_MOD = 1n << 31n;
1694
- function readComputeParams(spec) {
1695
- const rawSeed = spec.metadata.seed;
1696
- const rawSteps = spec.metadata.steps;
1963
+ function readComputeParams(spec2) {
1964
+ const rawSeed = spec2.metadata.seed;
1965
+ const rawSteps = spec2.metadata.steps;
1697
1966
  const seed = typeof rawSeed === "number" && Number.isInteger(rawSeed) ? rawSeed : GENSYN_DEMO_SEED;
1698
1967
  const steps = typeof rawSteps === "number" && Number.isInteger(rawSteps) && rawSteps > 0 ? rawSteps : GENSYN_DEMO_STEPS;
1699
1968
  return { seed, steps };
1700
1969
  }
1701
- var reproduceGensynCompute = (spec) => {
1702
- const { seed, steps } = readComputeParams(spec);
1970
+ var reproduceGensynCompute = (spec2) => {
1971
+ const { seed, steps } = readComputeParams(spec2);
1703
1972
  const states = [];
1704
1973
  let x = BigInt(seed) % LCG_MOD;
1705
1974
  if (x < 0n) {
@@ -1721,10 +1990,10 @@ function commitmentOf(base) {
1721
1990
  )
1722
1991
  );
1723
1992
  }
1724
- function buildReeProof(spec, exec, content) {
1993
+ function buildReeProof(spec2, exec, content) {
1725
1994
  const base = {
1726
1995
  scheme: GENSYN_REE_SCHEME,
1727
- specHash: sha256Hex6(canonicalize(spec)),
1996
+ specHash: sha256Hex6(canonicalize(spec2)),
1728
1997
  outputHash: sha256Hex6(content),
1729
1998
  steps: exec.steps,
1730
1999
  transcriptHash: sha256Hex6(canonicalize(exec.states))
@@ -1831,11 +2100,11 @@ function createGensynAdapter(config = {}) {
1831
2100
  id: GENSYN_ADAPTER_ID,
1832
2101
  capabilities,
1833
2102
  healthCheck: checkHealth,
1834
- async execute(spec) {
1835
- const exec = runCompute(spec);
2103
+ async execute(spec2) {
2104
+ const exec = runCompute(spec2);
1836
2105
  const content = gensynDeliverable(exec);
1837
2106
  const contentHash = sha256Hex6(content);
1838
- const proof = buildReeProof(spec, exec, content);
2107
+ const proof = buildReeProof(spec2, exec, content);
1839
2108
  const proofUri = await pin(content);
1840
2109
  return {
1841
2110
  content,
@@ -2010,21 +2279,21 @@ function solveZkmlDemo() {
2010
2279
  output: { label: "cat", confidence: 0.98 }
2011
2280
  });
2012
2281
  }
2013
- function referencePlaceholderOutput(spec) {
2282
+ function referencePlaceholderOutput(spec2) {
2014
2283
  return {
2015
2284
  content: canonicalize({
2016
2285
  note: "reference stub - replace the runInference port with a real zk-verified inference call.",
2017
- taskType: spec.taskType
2286
+ taskType: spec2.taskType
2018
2287
  }),
2019
2288
  modelCommitment: sha256Hex7("zkml-reference-stub-model"),
2020
- inputCommitment: sha256Hex7(canonicalize(spec))
2289
+ inputCommitment: sha256Hex7(canonicalize(spec2))
2021
2290
  };
2022
2291
  }
2023
- var defaultRunInference = (spec) => spec.metadata.demo === ZKML_DEMO_MARKER ? {
2292
+ var defaultRunInference = (spec2) => spec2.metadata.demo === ZKML_DEMO_MARKER ? {
2024
2293
  content: solveZkmlDemo(),
2025
2294
  modelCommitment: DEMO_MODEL_COMMITMENT,
2026
2295
  inputCommitment: DEMO_INPUT_COMMITMENT
2027
- } : referencePlaceholderOutput(spec);
2296
+ } : referencePlaceholderOutput(spec2);
2028
2297
  function createZkmlAdapter(config = {}) {
2029
2298
  const id = config.id ?? ZKML_ADAPTER_ID;
2030
2299
  const capabilities = config.capabilities ?? [...ZKML_CAPABILITIES];
@@ -2039,10 +2308,10 @@ function createZkmlAdapter(config = {}) {
2039
2308
  return {
2040
2309
  id,
2041
2310
  capabilities,
2042
- async execute(spec) {
2043
- const output = await runInference(spec);
2311
+ async execute(spec2) {
2312
+ const output = await runInference(spec2);
2044
2313
  const contentHash = sha256Hex7(output.content);
2045
- const proof = await prove({ spec, output, outputHash: contentHash });
2314
+ const proof = await prove({ spec: spec2, output, outputHash: contentHash });
2046
2315
  const proofUri = await pin(proof);
2047
2316
  return {
2048
2317
  content: output.content,
@@ -2069,6 +2338,7 @@ export {
2069
2338
  ApiClient,
2070
2339
  ENABLED_RAILS,
2071
2340
  Escro,
2341
+ EscroConfirmError,
2072
2342
  EscroError,
2073
2343
  EscroRailNotEnabledError,
2074
2344
  EscrowNotFoundError,