@escro/sdk 0.4.0 → 0.4.2

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.d.cts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Keypair } from '@solana/web3.js';
2
2
 
3
+ type SolanaCluster = "devnet" | "mainnet";
4
+
3
5
  /**
4
6
  * All possible lifecycle states of an AgentEscrow account on-chain.
5
7
  *
@@ -132,6 +134,10 @@ interface ProtocolStatsFigures {
132
134
  }
133
135
  /** Protocol-wide aggregate statistics: public, unauthenticated. */
134
136
  interface ProtocolStats extends ProtocolStatsFigures {
137
+ /** The Solana cluster this API serves (its `CLUSTER`). */
138
+ cluster: SolanaCluster;
139
+ /** That cluster's one USDC mint (`getUsdcMint`): every figure here is in this token. */
140
+ usdcMint: string;
135
141
  /**
136
142
  * Escro-operated fleet figures (ARCHITECTURE section 9): the same fields over
137
143
  * escrows whose buyer and assigned worker are both in the API's operator
@@ -378,8 +384,6 @@ interface ReviewPolicy {
378
384
  evaluate(deliverable: ReviewDeliverable): ReviewResult | Promise<ReviewResult>;
379
385
  }
380
386
 
381
- type SolanaCluster = "devnet" | "mainnet";
382
-
383
387
  /** Dispute tally with its outcome split (from the wallet's perspective). */
384
388
  interface DisputeRecord {
385
389
  /** Total disputes this wallet was party to (raised by either side). */
@@ -747,9 +751,11 @@ declare class Escro {
747
751
  * {@link Escro.releasePayment}, reporting whether the API recorded the release.
748
752
  * `ESCROW_NOT_SETTLED` (the API's RPC does not yet show the escrow closed), the codes
749
753
  * {@link Escro.confirm} retries and a network failure are retried for about 30 s. A
750
- * failure never throws: the release already landed, so the signature is returned with
751
- * `confirm.landed: false` and an `EscroConfirmError` (the API's code, or
752
- * `NETWORK_ERROR` when the last call never reached it).
754
+ * failed record never throws: the release already landed, so the signature is returned
755
+ * with `confirm.landed: false` and an `EscroConfirmError` (the API's code, or
756
+ * `NETWORK_ERROR` when the last call never reached it). A release that failed at
757
+ * execution on chain throws an {@link EscroError} with code `TX_FAILED`, and the API is
758
+ * never asked to record it.
753
759
  *
754
760
  * @returns `{ signature, confirm }`.
755
761
  */
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Keypair } from '@solana/web3.js';
2
2
 
3
+ type SolanaCluster = "devnet" | "mainnet";
4
+
3
5
  /**
4
6
  * All possible lifecycle states of an AgentEscrow account on-chain.
5
7
  *
@@ -132,6 +134,10 @@ interface ProtocolStatsFigures {
132
134
  }
133
135
  /** Protocol-wide aggregate statistics: public, unauthenticated. */
134
136
  interface ProtocolStats extends ProtocolStatsFigures {
137
+ /** The Solana cluster this API serves (its `CLUSTER`). */
138
+ cluster: SolanaCluster;
139
+ /** That cluster's one USDC mint (`getUsdcMint`): every figure here is in this token. */
140
+ usdcMint: string;
135
141
  /**
136
142
  * Escro-operated fleet figures (ARCHITECTURE section 9): the same fields over
137
143
  * escrows whose buyer and assigned worker are both in the API's operator
@@ -378,8 +384,6 @@ interface ReviewPolicy {
378
384
  evaluate(deliverable: ReviewDeliverable): ReviewResult | Promise<ReviewResult>;
379
385
  }
380
386
 
381
- type SolanaCluster = "devnet" | "mainnet";
382
-
383
387
  /** Dispute tally with its outcome split (from the wallet's perspective). */
384
388
  interface DisputeRecord {
385
389
  /** Total disputes this wallet was party to (raised by either side). */
@@ -747,9 +751,11 @@ declare class Escro {
747
751
  * {@link Escro.releasePayment}, reporting whether the API recorded the release.
748
752
  * `ESCROW_NOT_SETTLED` (the API's RPC does not yet show the escrow closed), the codes
749
753
  * {@link Escro.confirm} retries and a network failure are retried for about 30 s. A
750
- * failure never throws: the release already landed, so the signature is returned with
751
- * `confirm.landed: false` and an `EscroConfirmError` (the API's code, or
752
- * `NETWORK_ERROR` when the last call never reached it).
754
+ * failed record never throws: the release already landed, so the signature is returned
755
+ * with `confirm.landed: false` and an `EscroConfirmError` (the API's code, or
756
+ * `NETWORK_ERROR` when the last call never reached it). A release that failed at
757
+ * execution on chain throws an {@link EscroError} with code `TX_FAILED`, and the API is
758
+ * never asked to record it.
753
759
  *
754
760
  * @returns `{ signature, confirm }`.
755
761
  */
package/dist/index.js CHANGED
@@ -7,10 +7,12 @@ import { Connection as Connection2, PublicKey as PublicKey3 } from "@solana/web3
7
7
 
8
8
  // ../shared/dist/index.js
9
9
  import { createHash } from "crypto";
10
+ import { createHash as createHash2 } from "crypto";
10
11
  import nacl from "tweetnacl";
11
12
  import bs58 from "bs58";
12
13
  import nacl2 from "tweetnacl";
13
14
  import bs582 from "bs58";
15
+ import bs583 from "bs58";
14
16
  var EscrowState = /* @__PURE__ */ ((EscrowState2) => {
15
17
  EscrowState2["CREATED"] = "CREATED";
16
18
  EscrowState2["FUNDED"] = "FUNDED";
@@ -57,6 +59,33 @@ function canonicalize(value) {
57
59
  }
58
60
  return JSON.stringify(value);
59
61
  }
62
+ var SPEC_KEYS = [
63
+ "version",
64
+ "taskType",
65
+ "description",
66
+ "acceptanceCriteria",
67
+ "deliverableFormat",
68
+ "metadata"
69
+ ];
70
+ var CRITERION_KEYS = ["id", "description", "weight", "required"];
71
+ var FORMAT_KEYS = ["type", "schema", "mimeType", "maxSizeBytes", "language"];
72
+ function pick(value, keys) {
73
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
74
+ const source = value;
75
+ return Object.fromEntries(keys.filter((k) => k in source).map((k) => [k, source[k]]));
76
+ }
77
+ function normalizeTaskSpec(spec2) {
78
+ const top = pick(spec2, SPEC_KEYS);
79
+ const criteria = top.acceptanceCriteria;
80
+ return {
81
+ ...top,
82
+ acceptanceCriteria: Array.isArray(criteria) ? criteria.map((c) => pick(c, CRITERION_KEYS)) : criteria,
83
+ deliverableFormat: pick(top.deliverableFormat, FORMAT_KEYS)
84
+ };
85
+ }
86
+ function computeTaskSpecHash(spec2) {
87
+ return createHash("sha256").update(canonicalize(normalizeTaskSpec(spec2)), "utf8").digest("hex");
88
+ }
60
89
  function disputeReasonError(reason) {
61
90
  if (reason.length === 0) return "reason must not be empty";
62
91
  const bytes = new TextEncoder().encode(reason).length;
@@ -65,8 +94,16 @@ function disputeReasonError(reason) {
65
94
  }
66
95
  return null;
67
96
  }
97
+ var SAFE_URL_SCHEMES = ["http:", "https:", "ipfs:", "ar:"];
98
+ function isSafeUrl(value) {
99
+ try {
100
+ return SAFE_URL_SCHEMES.includes(new URL(value).protocol);
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
68
105
  function hashBody(rawBody) {
69
- return createHash("sha256").update(rawBody, "utf8").digest("hex");
106
+ return createHash2("sha256").update(rawBody, "utf8").digest("hex");
70
107
  }
71
108
  function buildSignedMessage(timestamp, method, fullPath, bodyHash) {
72
109
  return `escro:${String(timestamp)}:${method}:${fullPath}:${bodyHash}`;
@@ -110,6 +147,18 @@ function verifyBotDescriptorSignature(descriptor) {
110
147
  return false;
111
148
  }
112
149
  }
150
+ var ESCROW_INSTRUCTION_NAMES = [
151
+ "create_escrow",
152
+ "claim_task",
153
+ "submit_deliverable",
154
+ "raise_dispute",
155
+ "cancel_escrow",
156
+ "release_payment",
157
+ "timeout_release",
158
+ "refund_buyer",
159
+ "arbitration_release",
160
+ "arbitration_refund"
161
+ ];
113
162
  function spec(discriminator, escrowAccountIndex, cause) {
114
163
  return {
115
164
  discriminator: Uint8Array.from(discriminator),
@@ -130,6 +179,66 @@ var ESCROW_INSTRUCTIONS = {
130
179
  arbitration_release: spec([118, 157, 141, 26, 104, 117, 94, 229], 4, "ARBITRATION_RELEASE"),
131
180
  arbitration_refund: spec([255, 194, 84, 248, 185, 249, 120, 122], 4, "ARBITRATION_REFUND")
132
181
  };
182
+ function escrowInstructionName(data) {
183
+ for (const name of ESCROW_INSTRUCTION_NAMES) {
184
+ const disc2 = ESCROW_INSTRUCTIONS[name].discriminator;
185
+ if (disc2.every((b, i) => data[i] === b)) return name;
186
+ }
187
+ return null;
188
+ }
189
+ var Reader = class {
190
+ constructor(buf) {
191
+ __publicField(this, "buf");
192
+ __publicField(this, "offset", 8);
193
+ this.buf = buf;
194
+ }
195
+ take(n) {
196
+ if (this.offset + n > this.buf.length) {
197
+ throw new Error(
198
+ `instruction data truncated: need ${String(n)} bytes at offset ${String(this.offset)}, have ${String(this.buf.length)}`
199
+ );
200
+ }
201
+ const out = this.buf.subarray(this.offset, this.offset + n);
202
+ this.offset += n;
203
+ return out;
204
+ }
205
+ u8() {
206
+ return this.take(1).readUInt8(0);
207
+ }
208
+ u64() {
209
+ return this.take(8).readBigUInt64LE(0);
210
+ }
211
+ i64() {
212
+ return this.take(8).readBigInt64LE(0);
213
+ }
214
+ string() {
215
+ const len = this.take(4).readUInt32LE(0);
216
+ return this.take(len).toString("utf8");
217
+ }
218
+ option(read) {
219
+ return this.u8() === 0 ? null : read();
220
+ }
221
+ bytes(n) {
222
+ return this.take(n);
223
+ }
224
+ };
225
+ function decodeCreateEscrowArgs(data) {
226
+ const r = new Reader(Buffer.from(data));
227
+ return {
228
+ taskId: r.string(),
229
+ amount: r.u64(),
230
+ deadline: r.i64(),
231
+ specHash: r.string(),
232
+ assignedWorker: r.option(() => bs583.encode(r.bytes(32)))
233
+ };
234
+ }
235
+ function decodeSubmitDeliverableArgs(data) {
236
+ const r = new Reader(Buffer.from(data));
237
+ return { deliverableHash: r.string(), proofUri: r.option(() => r.string()) };
238
+ }
239
+ function decodeRaiseDisputeArgs(data) {
240
+ return { reason: new Reader(Buffer.from(data)).string() };
241
+ }
133
242
 
134
243
  // src/payments.ts
135
244
  var RAILS = ["escrow", "x402"];
@@ -504,9 +613,7 @@ async function pollUntil(address, fetcher, predicate, timeoutMs = MS_24_H) {
504
613
  }
505
614
 
506
615
  // src/solana.ts
507
- import { createHash as createHash2 } from "crypto";
508
616
  import {
509
- ComputeBudgetProgram,
510
617
  PublicKey as PublicKey2,
511
618
  Transaction,
512
619
  TransactionInstruction
@@ -520,8 +627,14 @@ import {
520
627
 
521
628
  // ../shared/dist/solana.js
522
629
  import { PublicKey } from "@solana/web3.js";
523
- import bs583 from "bs58";
630
+ import bs584 from "bs58";
524
631
  var utf8 = (s) => new TextEncoder().encode(s);
632
+ function deriveEscrowPda(programId, buyer, taskId) {
633
+ return PublicKey.findProgramAddressSync(
634
+ [utf8("escrow"), buyer.toBytes(), utf8(taskId)],
635
+ programId
636
+ )[0];
637
+ }
525
638
  function findPlatformConfigPda(programId) {
526
639
  return PublicKey.findProgramAddressSync([utf8("config")], programId);
527
640
  }
@@ -558,7 +671,7 @@ function decodePlatformConfig(data) {
558
671
  throw new Error("PlatformConfig discriminator mismatch: not a PlatformConfig account");
559
672
  }
560
673
  const byte = (at) => data[at] ?? 0;
561
- const key = (at) => bs583.encode(data.subarray(at, at + 32));
674
+ const key = (at) => bs584.encode(data.subarray(at, at + 32));
562
675
  return {
563
676
  version: byte(L.version),
564
677
  feeBps: byte(L.fee_bps) | byte(L.fee_bps + 1) << 8,
@@ -570,10 +683,6 @@ function decodePlatformConfig(data) {
570
683
  }
571
684
 
572
685
  // src/solana.ts
573
- function disc(name) {
574
- return Buffer.from(createHash2("sha256").update(`global:${name}`).digest()).subarray(0, 8);
575
- }
576
- var DISC_RELEASE_PAYMENT = disc("release_payment");
577
686
  function resolveProgramId(cluster) {
578
687
  const id = getProgramId(cluster);
579
688
  assertProgramIdNotPlaceholder(id, cluster);
@@ -583,8 +692,7 @@ function allowedProgramIds(programId) {
583
692
  return /* @__PURE__ */ new Set([
584
693
  programId.toBase58(),
585
694
  TOKEN_PROGRAM_ID.toBase58(),
586
- ASSOCIATED_TOKEN_PROGRAM_ID.toBase58(),
587
- ComputeBudgetProgram.programId.toBase58()
695
+ ASSOCIATED_TOKEN_PROGRAM_ID.toBase58()
588
696
  ]);
589
697
  }
590
698
  var MAX_INSTRUCTIONS = 6;
@@ -622,6 +730,12 @@ async function fetchPlatformConfig(connection, cluster) {
622
730
  }
623
731
  return { mint: new PublicKey2(config.usdcMint), treasury: new PublicKey2(config.treasury) };
624
732
  }
733
+ function refuse(reason) {
734
+ throw new EscroError(
735
+ `Refusing to sign a server-built transaction: ${reason}`,
736
+ "UNEXPECTED_TRANSACTION"
737
+ );
738
+ }
625
739
  function validateTransaction(tx, cluster) {
626
740
  const programId = resolveProgramId(cluster);
627
741
  const allowed = allowedProgramIds(programId);
@@ -629,36 +743,103 @@ function validateTransaction(tx, cluster) {
629
743
  for (const ix of tx.instructions) {
630
744
  const id = ix.programId.toBase58();
631
745
  if (!allowed.has(id)) {
632
- throw new Error(
633
- `Unexpected program in transaction: ${id}. Allowed: ${[...allowed].join(", ")}`
634
- );
746
+ refuse(`Unexpected program in transaction: ${id}. Allowed: ${[...allowed].join(", ")}`);
635
747
  }
636
748
  if (id === tokenProgram) {
637
749
  const tag = ix.data.length > 0 ? ix.data[0] : void 0;
638
750
  if (tag !== void 0 && FORBIDDEN_TOKEN_IX_TAGS.has(tag)) {
639
- throw new Error(
640
- `Refusing to sign a fund-moving SPL Token instruction (tag ${String(tag)}) in a server-built transaction. Escro never requires the wallet to directly sign a token transfer/approve/burn/close; all token movement is a CPI inside the escro program.`
751
+ throw new EscroError(
752
+ `Refusing to sign a fund-moving SPL Token instruction (tag ${String(tag)}) in a server-built transaction`,
753
+ "UNEXPECTED_TRANSACTION"
641
754
  );
642
755
  }
643
756
  }
644
757
  }
645
758
  if (tx.instructions.length > MAX_INSTRUCTIONS) {
646
- throw new Error(
759
+ refuse(
647
760
  `Too many instructions (${String(tx.instructions.length)}). Max allowed: ${String(MAX_INSTRUCTIONS)}`
648
761
  );
649
762
  }
650
763
  }
651
- async function signAndSend(base64Tx, wallet, connection, cluster) {
652
- const buf = Buffer.from(base64Tx, "base64");
653
- const tx = Transaction.from(buf);
764
+ var ATA_CREATE_IDEMPOTENT = 1;
765
+ function verifyServerTransaction(tx, cluster, wallet, expected) {
654
766
  validateTransaction(tx, cluster);
767
+ const programId = resolveProgramId(cluster);
768
+ if (!tx.feePayer?.equals(wallet)) refuse("the fee payer is not the wallet");
769
+ const escro = tx.instructions.filter((ix2) => ix2.programId.equals(programId));
770
+ const others = tx.instructions.filter((ix2) => !ix2.programId.equals(programId));
771
+ for (const ix2 of others) {
772
+ const ataCreate = expected.name === "release_payment" && ix2.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID) && ix2.data.length === 1 && ix2.data[0] === ATA_CREATE_IDEMPOTENT;
773
+ if (!ataCreate) refuse(`unexpected ${ix2.programId.toBase58()} instruction`);
774
+ }
775
+ if (escro.length !== 1) refuse(`${String(escro.length)} escro instructions, expected 1`);
776
+ const [ix] = escro;
777
+ if (ix === void 0) refuse("no escro instruction");
778
+ const name = escrowInstructionName(ix.data);
779
+ if (name !== expected.name)
780
+ refuse(`${name ?? "an unknown instruction"}, expected ${expected.name}`);
781
+ const spec2 = ESCROW_INSTRUCTIONS[name];
782
+ const actor = ix.keys[spec2.signerAccountIndex];
783
+ if (actor === void 0 || !actor.pubkey.equals(wallet) || !actor.isSigner) {
784
+ refuse("the acting signer is not the wallet");
785
+ }
786
+ if (ix.keys[spec2.escrowAccountIndex]?.pubkey.toBase58() !== expected.escrowPda) {
787
+ refuse(`the escrow is not ${expected.escrowPda}`);
788
+ }
789
+ try {
790
+ switch (expected.name) {
791
+ case "create_escrow": {
792
+ const args = decodeCreateEscrowArgs(ix.data);
793
+ if (args.taskId !== expected.taskId) refuse("the task id differs");
794
+ if (deriveEscrowPda(programId, wallet, args.taskId).toBase58() !== expected.escrowPda) {
795
+ refuse("the escrow is not derived from the wallet and task id");
796
+ }
797
+ if (args.amount !== expected.amount) refuse("the amount differs");
798
+ if (args.deadline !== expected.deadline) refuse("the deadline differs");
799
+ if (args.specHash !== expected.specHash) refuse("the spec hash differs");
800
+ if (args.assignedWorker !== expected.assignedWorker) refuse("the assigned worker differs");
801
+ break;
802
+ }
803
+ case "submit_deliverable": {
804
+ const args = decodeSubmitDeliverableArgs(ix.data);
805
+ if (args.deliverableHash !== expected.deliverableHash)
806
+ refuse("the deliverable hash differs");
807
+ if (args.proofUri !== expected.proofUri) refuse("the proof URI differs");
808
+ break;
809
+ }
810
+ case "raise_dispute":
811
+ if (decodeRaiseDisputeArgs(ix.data).reason !== expected.reason)
812
+ refuse("the reason differs");
813
+ break;
814
+ default:
815
+ if (ix.data.length !== ESCROW_INSTRUCTIONS[name].discriminator.length) {
816
+ refuse(`${name} carries unexpected arguments`);
817
+ }
818
+ }
819
+ } catch (err) {
820
+ if (err instanceof EscroError) throw err;
821
+ refuse(`malformed ${name} arguments`);
822
+ }
823
+ }
824
+ async function signAndSend(base64Tx, wallet, connection, cluster, expected) {
825
+ const tx = Transaction.from(Buffer.from(base64Tx, "base64"));
826
+ verifyServerTransaction(tx, cluster, wallet.publicKey, expected);
655
827
  tx.partialSign(wallet);
656
828
  const raw = tx.serialize();
657
829
  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed");
658
830
  const signature = await connection.sendRawTransaction(raw, {
659
831
  skipPreflight: false
660
832
  });
661
- await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, "confirmed");
833
+ const { value } = await connection.confirmTransaction(
834
+ { signature, blockhash, lastValidBlockHeight },
835
+ "confirmed"
836
+ );
837
+ if (value.err !== null) {
838
+ throw new EscroError(
839
+ `Transaction ${signature} failed at execution: ${JSON.stringify(value.err)}`,
840
+ "TX_FAILED"
841
+ );
842
+ }
662
843
  return signature;
663
844
  }
664
845
  async function buildReleasePaymentTx(params) {
@@ -701,7 +882,7 @@ async function buildReleasePaymentTx(params) {
701
882
  { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
702
883
  { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }
703
884
  ],
704
- data: DISC_RELEASE_PAYMENT
885
+ data: Buffer.from(ESCROW_INSTRUCTIONS.release_payment.discriminator)
705
886
  });
706
887
  const { blockhash } = await connection.getLatestBlockhash("confirmed");
707
888
  const tx = new Transaction();
@@ -732,14 +913,6 @@ function isRetryableConfirmCode(code) {
732
913
  function isRetryableReleaseCode(code) {
733
914
  return code === "ESCROW_NOT_SETTLED" || isRetryableConfirmCode(code);
734
915
  }
735
- function isUrl(value) {
736
- try {
737
- new URL(value);
738
- return true;
739
- } catch {
740
- return false;
741
- }
742
- }
743
916
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
744
917
  var Escro = class {
745
918
  constructor(options) {
@@ -762,7 +935,15 @@ var Escro = class {
762
935
  */
763
936
  async createEscrow(params) {
764
937
  const { escrowId, escrowPda, unsignedTx } = await this.api.createEscrow(params);
765
- const signature = await this.send(unsignedTx);
938
+ const signature = await this.send(unsignedTx, {
939
+ name: "create_escrow",
940
+ escrowPda,
941
+ taskId: escrowId,
942
+ amount: BigInt(params.amountUsdc),
943
+ deadline: BigInt(params.deadlineUnix),
944
+ specHash: computeTaskSpecHash(params.taskSpec),
945
+ assignedWorker: params.assignedWorker
946
+ });
766
947
  await this.confirm(escrowPda, signature);
767
948
  return { escrowId, escrowPda, signature };
768
949
  }
@@ -783,9 +964,11 @@ var Escro = class {
783
964
  * {@link Escro.releasePayment}, reporting whether the API recorded the release.
784
965
  * `ESCROW_NOT_SETTLED` (the API's RPC does not yet show the escrow closed), the codes
785
966
  * {@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).
967
+ * failed record never throws: the release already landed, so the signature is returned
968
+ * with `confirm.landed: false` and an `EscroConfirmError` (the API's code, or
969
+ * `NETWORK_ERROR` when the last call never reached it). A release that failed at
970
+ * execution on chain throws an {@link EscroError} with code `TX_FAILED`, and the API is
971
+ * never asked to record it.
789
972
  *
790
973
  * @returns `{ signature, confirm }`.
791
974
  */
@@ -804,12 +987,7 @@ var Escro = class {
804
987
  connection: this.connection,
805
988
  cluster: this.cluster
806
989
  });
807
- const signature = await signAndSend(
808
- unsignedTx,
809
- this.options.wallet,
810
- this.connection,
811
- this.cluster
812
- );
990
+ const signature = await this.send(unsignedTx, { name: "release_payment", escrowPda: address });
813
991
  try {
814
992
  await this.retryRecord(
815
993
  address,
@@ -832,11 +1010,18 @@ var Escro = class {
832
1010
  async raiseDispute(address, params) {
833
1011
  const reasonError = disputeReasonError(params.reason);
834
1012
  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");
1013
+ if (params.evidence !== void 0 && !isSafeUrl(params.evidence)) {
1014
+ throw new EscroError(
1015
+ `evidence must be an http(s), ipfs or ar URL: ${params.evidence}`,
1016
+ "VALIDATION_ERROR"
1017
+ );
837
1018
  }
838
1019
  const { unsignedTx } = await this.api.raiseDispute(address, { reason: params.reason });
839
- const signature = await this.send(unsignedTx);
1020
+ const signature = await this.send(unsignedTx, {
1021
+ name: "raise_dispute",
1022
+ escrowPda: address,
1023
+ reason: params.reason
1024
+ });
840
1025
  await this.confirm(address, signature, {
841
1026
  ...params.evidence !== void 0 && { evidence: params.evidence }
842
1027
  });
@@ -851,7 +1036,7 @@ var Escro = class {
851
1036
  */
852
1037
  async cancelEscrow(address) {
853
1038
  const { unsignedTx } = await this.api.cancelEscrow(address);
854
- return this.sendAndConfirm(address, unsignedTx);
1039
+ return this.sendAndConfirm(address, unsignedTx, { name: "cancel_escrow", escrowPda: address });
855
1040
  }
856
1041
  // ── Payments (dual-path; x402 reserved) ───────────────────────────────────────
857
1042
  /**
@@ -882,7 +1067,7 @@ var Escro = class {
882
1067
  */
883
1068
  async claimTask(address) {
884
1069
  const { unsignedTx } = await this.api.claimTask(address);
885
- return this.sendAndConfirm(address, unsignedTx);
1070
+ return this.sendAndConfirm(address, unsignedTx, { name: "claim_task", escrowPda: address });
886
1071
  }
887
1072
  /**
888
1073
  * Submit a deliverable for the buyer to review. Worker must be the wallet.
@@ -892,7 +1077,12 @@ var Escro = class {
892
1077
  */
893
1078
  async submitDeliverable(address, params) {
894
1079
  const { unsignedTx } = await this.api.submitDeliverable(address, params);
895
- return this.sendAndConfirm(address, unsignedTx);
1080
+ return this.sendAndConfirm(address, unsignedTx, {
1081
+ name: "submit_deliverable",
1082
+ escrowPda: address,
1083
+ deliverableHash: params.contentHash,
1084
+ proofUri: params.proofUri ?? null
1085
+ });
896
1086
  }
897
1087
  /**
898
1088
  * List tasks assigned to the wallet's public key.
@@ -1058,11 +1248,11 @@ var Escro = class {
1058
1248
  delay = Math.min(delay * 2, CONFIRM_MAX_DELAY_MS);
1059
1249
  }
1060
1250
  }
1061
- send(unsignedTx) {
1062
- return signAndSend(unsignedTx, this.options.wallet, this.connection, this.cluster);
1251
+ send(unsignedTx, expected) {
1252
+ return signAndSend(unsignedTx, this.options.wallet, this.connection, this.cluster, expected);
1063
1253
  }
1064
- async sendAndConfirm(address, unsignedTx) {
1065
- const signature = await this.send(unsignedTx);
1254
+ async sendAndConfirm(address, unsignedTx, expected) {
1255
+ const signature = await this.send(unsignedTx, expected);
1066
1256
  await this.confirm(address, signature);
1067
1257
  return signature;
1068
1258
  }