@escro/sdk 0.4.1 → 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.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,29 +743,87 @@ 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");
@@ -710,7 +882,7 @@ async function buildReleasePaymentTx(params) {
710
882
  { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
711
883
  { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }
712
884
  ],
713
- data: DISC_RELEASE_PAYMENT
885
+ data: Buffer.from(ESCROW_INSTRUCTIONS.release_payment.discriminator)
714
886
  });
715
887
  const { blockhash } = await connection.getLatestBlockhash("confirmed");
716
888
  const tx = new Transaction();
@@ -741,14 +913,6 @@ function isRetryableConfirmCode(code) {
741
913
  function isRetryableReleaseCode(code) {
742
914
  return code === "ESCROW_NOT_SETTLED" || isRetryableConfirmCode(code);
743
915
  }
744
- function isUrl(value) {
745
- try {
746
- new URL(value);
747
- return true;
748
- } catch {
749
- return false;
750
- }
751
- }
752
916
  var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
753
917
  var Escro = class {
754
918
  constructor(options) {
@@ -771,7 +935,15 @@ var Escro = class {
771
935
  */
772
936
  async createEscrow(params) {
773
937
  const { escrowId, escrowPda, unsignedTx } = await this.api.createEscrow(params);
774
- 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
+ });
775
947
  await this.confirm(escrowPda, signature);
776
948
  return { escrowId, escrowPda, signature };
777
949
  }
@@ -815,12 +987,7 @@ var Escro = class {
815
987
  connection: this.connection,
816
988
  cluster: this.cluster
817
989
  });
818
- const signature = await signAndSend(
819
- unsignedTx,
820
- this.options.wallet,
821
- this.connection,
822
- this.cluster
823
- );
990
+ const signature = await this.send(unsignedTx, { name: "release_payment", escrowPda: address });
824
991
  try {
825
992
  await this.retryRecord(
826
993
  address,
@@ -843,11 +1010,18 @@ var Escro = class {
843
1010
  async raiseDispute(address, params) {
844
1011
  const reasonError = disputeReasonError(params.reason);
845
1012
  if (reasonError !== null) throw new EscroError(reasonError, "VALIDATION_ERROR");
846
- if (params.evidence !== void 0 && !isUrl(params.evidence)) {
847
- 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
+ );
848
1018
  }
849
1019
  const { unsignedTx } = await this.api.raiseDispute(address, { reason: params.reason });
850
- 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
+ });
851
1025
  await this.confirm(address, signature, {
852
1026
  ...params.evidence !== void 0 && { evidence: params.evidence }
853
1027
  });
@@ -862,7 +1036,7 @@ var Escro = class {
862
1036
  */
863
1037
  async cancelEscrow(address) {
864
1038
  const { unsignedTx } = await this.api.cancelEscrow(address);
865
- return this.sendAndConfirm(address, unsignedTx);
1039
+ return this.sendAndConfirm(address, unsignedTx, { name: "cancel_escrow", escrowPda: address });
866
1040
  }
867
1041
  // ── Payments (dual-path; x402 reserved) ───────────────────────────────────────
868
1042
  /**
@@ -893,7 +1067,7 @@ var Escro = class {
893
1067
  */
894
1068
  async claimTask(address) {
895
1069
  const { unsignedTx } = await this.api.claimTask(address);
896
- return this.sendAndConfirm(address, unsignedTx);
1070
+ return this.sendAndConfirm(address, unsignedTx, { name: "claim_task", escrowPda: address });
897
1071
  }
898
1072
  /**
899
1073
  * Submit a deliverable for the buyer to review. Worker must be the wallet.
@@ -903,7 +1077,12 @@ var Escro = class {
903
1077
  */
904
1078
  async submitDeliverable(address, params) {
905
1079
  const { unsignedTx } = await this.api.submitDeliverable(address, params);
906
- 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
+ });
907
1086
  }
908
1087
  /**
909
1088
  * List tasks assigned to the wallet's public key.
@@ -1069,11 +1248,11 @@ var Escro = class {
1069
1248
  delay = Math.min(delay * 2, CONFIRM_MAX_DELAY_MS);
1070
1249
  }
1071
1250
  }
1072
- send(unsignedTx) {
1073
- 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);
1074
1253
  }
1075
- async sendAndConfirm(address, unsignedTx) {
1076
- const signature = await this.send(unsignedTx);
1254
+ async sendAndConfirm(address, unsignedTx, expected) {
1255
+ const signature = await this.send(unsignedTx, expected);
1077
1256
  await this.confirm(address, signature);
1078
1257
  return signature;
1079
1258
  }