@subly_fi/pay 0.2.0 → 0.3.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/pay.js CHANGED
@@ -1,11 +1,8 @@
1
- // demo/pay.ts
2
- import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
3
-
4
- // src/client/agent-wallet-signer.ts
1
+ // ../../src/client/agent-wallet-signer.ts
5
2
  import { signBytes } from "@solana/kit";
6
3
  import bs584 from "bs58";
7
4
 
8
- // src/solana/tx.ts
5
+ // ../../src/solana/tx.ts
9
6
  import bs58 from "bs58";
10
7
  import {
11
8
  appendTransactionMessageInstructions,
@@ -20,9 +17,8 @@ import {
20
17
  setTransactionMessageLifetimeUsingBlockhash
21
18
  } from "@solana/kit";
22
19
 
23
- // src/lib/hash.ts
20
+ // ../../src/lib/hash.ts
24
21
  import { createHash } from "node:crypto";
25
- var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
26
22
  function sha256TaggedHex(data) {
27
23
  return `sha256-${createHash("sha256").update(data).digest("hex")}`;
28
24
  }
@@ -46,7 +42,7 @@ function hashStableJson(value) {
46
42
  return sha256TaggedHex(stableStringify(value));
47
43
  }
48
44
 
49
- // src/solana/tx.ts
45
+ // ../../src/solana/tx.ts
50
46
  function decodeSerializedTransaction(serializedBase64) {
51
47
  return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
52
48
  }
@@ -66,11 +62,11 @@ function signatureBase58ForSigner(transaction, signer2) {
66
62
  return bs58.encode(signature);
67
63
  }
68
64
 
69
- // src/client/transaction-intent-validator.ts
65
+ // ../../src/client/transaction-intent-validator.ts
70
66
  import bs583 from "bs58";
71
67
  import { getCompiledTransactionMessageDecoder } from "@solana/kit";
72
68
 
73
- // src/config/constants.ts
69
+ // ../../src/config/constants.ts
74
70
  var PAYMENT_SCHEME = "subly-yield-exact";
75
71
  var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
76
72
  var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
@@ -86,7 +82,7 @@ var SUBLY_VAULT = {
86
82
  };
87
83
  var USDC_DECIMALS = 6;
88
84
 
89
- // src/domain/request-binding.ts
85
+ // ../../src/domain/request-binding.ts
90
86
  function computeRequestBindingHash(fields) {
91
87
  return hashStableJson({
92
88
  sellerRequestId: fields.sellerRequestId,
@@ -101,7 +97,7 @@ function computeRequestBindingHash(fields) {
101
97
  });
102
98
  }
103
99
 
104
- // src/lib/associated-token-account.ts
100
+ // ../../src/lib/associated-token-account.ts
105
101
  import { createHash as createHash2 } from "node:crypto";
106
102
  import bs582 from "bs58";
107
103
  var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
@@ -119,12 +115,12 @@ function deriveAssociatedTokenAddress(params) {
119
115
  "associatedTokenProgramId"
120
116
  );
121
117
  for (let bump = 255; bump >= 0; bump -= 1) {
122
- const address2 = createProgramAddress(
118
+ const address3 = createProgramAddress(
123
119
  [owner, tokenProgramId, mint, Uint8Array.of(bump)],
124
120
  associatedTokenProgramId
125
121
  );
126
- if (address2 !== null) {
127
- return bs582.encode(address2);
122
+ if (address3 !== null) {
123
+ return bs582.encode(address3);
128
124
  }
129
125
  }
130
126
  throw new Error("Unable to derive associated token account address");
@@ -193,7 +189,7 @@ function modPow(base, exponent, modulus) {
193
189
  return result;
194
190
  }
195
191
 
196
- // src/client/transaction-intent-validator.ts
192
+ // ../../src/client/transaction-intent-validator.ts
197
193
  var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
198
194
  var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
199
195
  var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
@@ -879,7 +875,7 @@ function readShortVec(bytes, startOffset) {
879
875
  return null;
880
876
  }
881
877
 
882
- // src/client/agent-wallet-signer.ts
878
+ // ../../src/client/agent-wallet-signer.ts
883
879
  var LocalKeypairAgentWalletSigner = class {
884
880
  validationMode = "structured_intent_transaction";
885
881
  keyPairSigner;
@@ -949,47 +945,7 @@ var LocalKeypairAgentWalletSigner = class {
949
945
  }
950
946
  };
951
947
 
952
- // src/client/lookup-tables.ts
953
- import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
954
- import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
955
- function lookupTableAddressesForTransaction(serializedTransaction) {
956
- const wire = Buffer.from(serializedTransaction, "base64");
957
- let offset = 0;
958
- let signatureCount = 0;
959
- let shift = 0;
960
- while (offset < wire.length) {
961
- const byte = wire[offset];
962
- signatureCount |= (byte & 127) << shift;
963
- offset += 1;
964
- if ((byte & 128) === 0) {
965
- break;
966
- }
967
- shift += 7;
968
- }
969
- const messageBytes = wire.subarray(offset + signatureCount * 64);
970
- const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
971
- const lookups = compiled.addressTableLookups ?? [];
972
- return lookups.map((lookup) => String(lookup.lookupTableAddress));
973
- }
974
- async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
975
- const addresses = lookupTableAddressesForTransaction(serializedTransaction);
976
- if (addresses.length === 0) {
977
- return {};
978
- }
979
- const tables = await fetchAllMaybeAddressLookupTable(
980
- rpc2,
981
- addresses.map((value) => address(value))
982
- );
983
- const result = {};
984
- for (const table of tables) {
985
- if (table.exists) {
986
- result[table.address] = table.data.addresses.map(String);
987
- }
988
- }
989
- return result;
990
- }
991
-
992
- // src/api/wallet-auth.ts
948
+ // ../../src/api/wallet-auth.ts
993
949
  import { createHash as createHash3 } from "node:crypto";
994
950
  import bs585 from "bs58";
995
951
  import nacl from "tweetnacl";
@@ -1007,7 +963,7 @@ function walletAuthMessage(params) {
1007
963
  );
1008
964
  }
1009
965
 
1010
- // src/client/wallet-auth-headers.ts
966
+ // ../../src/client/wallet-auth-headers.ts
1011
967
  async function walletAuthHeaders(params) {
1012
968
  const signedAtMs = String(Date.now());
1013
969
  const message = walletAuthMessage({
@@ -1023,7 +979,7 @@ async function walletAuthHeaders(params) {
1023
979
  };
1024
980
  }
1025
981
 
1026
- // src/client/onboarding.ts
982
+ // ../../src/client/onboarding.ts
1027
983
  var SELF_SERVE_POLICY_ID = "self-serve";
1028
984
  var OnboardingError = class extends Error {
1029
985
  constructor(step, message, detail = null) {
@@ -1038,9 +994,9 @@ var OnboardingError = class extends Error {
1038
994
  async function ensureWalletOnboarded(params) {
1039
995
  const fetchImpl = params.fetchImpl ?? fetch;
1040
996
  const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
1041
- const post = async (step, path, body) => {
997
+ const post = async (step, path, body2) => {
1042
998
  const url2 = `${baseUrl}${path}`;
1043
- const serialized = JSON.stringify(body);
999
+ const serialized = JSON.stringify(body2);
1044
1000
  const response = await fetchImpl(url2, {
1045
1001
  method: "POST",
1046
1002
  headers: {
@@ -1080,12 +1036,226 @@ async function ensureWalletOnboarded(params) {
1080
1036
  await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
1081
1037
  }
1082
1038
 
1083
- // src/x402/headers.ts
1039
+ // ../../src/client/relayer-yield-realizer.ts
1040
+ import { address as address2 } from "@solana/kit";
1041
+
1042
+ // ../../src/client/lookup-tables.ts
1043
+ import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
1044
+ import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
1045
+ function lookupTableAddressesForTransaction(serializedTransaction) {
1046
+ const wire = Buffer.from(serializedTransaction, "base64");
1047
+ let offset = 0;
1048
+ let signatureCount = 0;
1049
+ let shift = 0;
1050
+ while (offset < wire.length) {
1051
+ const byte = wire[offset];
1052
+ signatureCount |= (byte & 127) << shift;
1053
+ offset += 1;
1054
+ if ((byte & 128) === 0) {
1055
+ break;
1056
+ }
1057
+ shift += 7;
1058
+ }
1059
+ const messageBytes = wire.subarray(offset + signatureCount * 64);
1060
+ const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
1061
+ const lookups = compiled.addressTableLookups ?? [];
1062
+ return lookups.map((lookup) => String(lookup.lookupTableAddress));
1063
+ }
1064
+ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1065
+ const addresses = lookupTableAddressesForTransaction(serializedTransaction);
1066
+ if (addresses.length === 0) {
1067
+ return {};
1068
+ }
1069
+ const tables = await fetchAllMaybeAddressLookupTable(
1070
+ rpc2,
1071
+ addresses.map((value) => address(value))
1072
+ );
1073
+ const result = {};
1074
+ for (const table of tables) {
1075
+ if (table.exists) {
1076
+ result[table.address] = table.data.addresses.map(String);
1077
+ }
1078
+ }
1079
+ return result;
1080
+ }
1081
+
1082
+ // ../../src/client/relayer-yield-realizer.ts
1083
+ var RelayerRealizeError = class extends Error {
1084
+ constructor(code, message, detail = null) {
1085
+ super(message);
1086
+ this.code = code;
1087
+ this.detail = detail;
1088
+ this.name = "RelayerRealizeError";
1089
+ }
1090
+ code;
1091
+ detail;
1092
+ };
1093
+ var RelayerYieldRealizer = class {
1094
+ config;
1095
+ signer;
1096
+ rpc;
1097
+ fetchImpl;
1098
+ lookupTablesFor;
1099
+ constructor(config) {
1100
+ this.config = {
1101
+ facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
1102
+ usdcMint: config.usdcMint ?? SUBLY_VAULT.usdcMint,
1103
+ forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
1104
+ };
1105
+ this.signer = config.signer;
1106
+ this.rpc = config.rpc;
1107
+ this.fetchImpl = config.fetchImpl ?? fetch;
1108
+ this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(this.rpc, serializedTransaction));
1109
+ }
1110
+ async ensureUsdcAvailable(input) {
1111
+ const agentAta = deriveAssociatedTokenAddress({
1112
+ owner: this.signer.walletAddress,
1113
+ mint: this.config.usdcMint
1114
+ });
1115
+ let shortfallRawUsdc;
1116
+ if (this.config.forceRealizeFullAmount) {
1117
+ shortfallRawUsdc = input.amountRawUsdc;
1118
+ } else {
1119
+ const currentBalance = await this.readTokenBalance(agentAta);
1120
+ if (currentBalance >= input.amountRawUsdc) {
1121
+ return { realizedRawUsdc: 0n, txSignature: null };
1122
+ }
1123
+ shortfallRawUsdc = input.amountRawUsdc - currentBalance;
1124
+ }
1125
+ await this.assertSpendableYield(shortfallRawUsdc);
1126
+ const prepared = await this.postJson("/v1/withdrawals/prepare", {
1127
+ wallet: this.signer.walletAddress,
1128
+ amountRawUsdc: shortfallRawUsdc.toString()
1129
+ });
1130
+ const signed = await this.signer.signWithdrawal({
1131
+ intent: prepared.signingIntent,
1132
+ serializedTransaction: prepared.serializedTransaction,
1133
+ lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
1134
+ });
1135
+ let settled = await this.postJson("/v1/withdrawals/submit", {
1136
+ withdrawalId: prepared.withdrawalId,
1137
+ serializedTransaction: signed.serializedTransaction,
1138
+ agentSignature: signed.agentSignature
1139
+ });
1140
+ if (settled.status === "submitted") {
1141
+ settled = await this.pollUntilSettled(prepared.withdrawalId);
1142
+ }
1143
+ if (settled.status !== "confirmed" || settled.txSignature === null) {
1144
+ throw new RelayerRealizeError(
1145
+ "realize_not_confirmed",
1146
+ `yield realize withdrawal did not confirm (status=${settled.status})`,
1147
+ settled
1148
+ );
1149
+ }
1150
+ return {
1151
+ realizedRawUsdc: BigInt(settled.actualWithdrawRawUsdc ?? "0"),
1152
+ txSignature: settled.txSignature
1153
+ };
1154
+ }
1155
+ /** Refuses to realize more than the ledger's spendable yield (principal). */
1156
+ async assertSpendableYield(shortfallRawUsdc) {
1157
+ const url2 = `${this.config.facilitatorBaseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
1158
+ let response;
1159
+ try {
1160
+ response = await this.fetchImpl(url2, {
1161
+ headers: await walletAuthHeaders({
1162
+ signer: this.signer,
1163
+ method: "GET",
1164
+ url: url2
1165
+ })
1166
+ });
1167
+ } catch (error) {
1168
+ throw new RelayerRealizeError(
1169
+ "budget_unavailable",
1170
+ "could not read the spendable-yield budget",
1171
+ error
1172
+ );
1173
+ }
1174
+ if (response.status !== 200) {
1175
+ throw new RelayerRealizeError(
1176
+ "budget_unavailable",
1177
+ `budget endpoint returned ${response.status}`
1178
+ );
1179
+ }
1180
+ const body2 = await response.json();
1181
+ const spendable = BigInt(body2.budget?.spendableYieldRawUsdc ?? "0");
1182
+ if (spendable < shortfallRawUsdc) {
1183
+ throw new RelayerRealizeError(
1184
+ "insufficient_yield",
1185
+ `spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC; the principal is never spent \u2014 wait for more yield`,
1186
+ { spendableYieldRawUsdc: spendable.toString() }
1187
+ );
1188
+ }
1189
+ }
1190
+ /**
1191
+ * Polls GET /v1/withdrawals/:id (which reconciles a submitted intent against
1192
+ * the chain) until it leaves the "submitted" state or the timeout elapses.
1193
+ */
1194
+ async pollUntilSettled(withdrawalId, { timeoutMs = 9e4, intervalMs = 2500 } = {}) {
1195
+ const deadline = Date.now() + timeoutMs;
1196
+ let latest = null;
1197
+ while (Date.now() < deadline) {
1198
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1199
+ const url2 = `${this.config.facilitatorBaseUrl}/v1/withdrawals/${withdrawalId}`;
1200
+ const response = await this.fetchImpl(url2, {
1201
+ headers: await walletAuthHeaders({
1202
+ signer: this.signer,
1203
+ method: "GET",
1204
+ url: url2
1205
+ })
1206
+ });
1207
+ if (response.status !== 200) {
1208
+ continue;
1209
+ }
1210
+ latest = await response.json();
1211
+ if (latest.status !== "submitted") {
1212
+ return latest;
1213
+ }
1214
+ }
1215
+ return latest ?? {
1216
+ status: "submitted",
1217
+ txSignature: null,
1218
+ actualWithdrawRawUsdc: null
1219
+ };
1220
+ }
1221
+ async postJson(path, body2) {
1222
+ const url2 = `${this.config.facilitatorBaseUrl}${path}`;
1223
+ const serialized = JSON.stringify(body2);
1224
+ const response = await this.fetchImpl(url2, {
1225
+ method: "POST",
1226
+ headers: {
1227
+ ...await walletAuthHeaders({
1228
+ signer: this.signer,
1229
+ method: "POST",
1230
+ url: url2,
1231
+ body: serialized
1232
+ }),
1233
+ "content-type": "application/json"
1234
+ },
1235
+ body: serialized
1236
+ });
1237
+ const text = await response.text();
1238
+ if (response.status !== 200) {
1239
+ throw new RelayerRealizeError(
1240
+ path.endsWith("/submit") ? "submit_failed" : "prepare_failed",
1241
+ `${path} failed with ${response.status}: ${text}`
1242
+ );
1243
+ }
1244
+ return JSON.parse(text);
1245
+ }
1246
+ async readTokenBalance(ata) {
1247
+ try {
1248
+ const response = await this.rpc.getTokenAccountBalance(address2(ata), { commitment: "confirmed" }).send();
1249
+ return BigInt(response.value.amount);
1250
+ } catch {
1251
+ return 0n;
1252
+ }
1253
+ }
1254
+ };
1255
+
1256
+ // ../../src/x402/headers.ts
1084
1257
  import { z } from "zod";
1085
1258
  var PAYMENT_REQUIRED_HEADER = "payment-required";
1086
- var PAYMENT_SIGNATURE_HEADER = "payment-signature";
1087
- var PAYMENT_RESPONSE_HEADER = "payment-response";
1088
- var X402_VERSION = 2;
1089
1259
  var MAX_HEADER_JSON_BYTES = 16384;
1090
1260
  var X402HeaderError = class extends Error {
1091
1261
  reason;
@@ -1132,16 +1302,6 @@ var sublyPaymentPayloadSchema = z.object({
1132
1302
  temporarySettlementSignature: z.string().min(1).max(128)
1133
1303
  })
1134
1304
  }).loose();
1135
- function encodeX402Header(value) {
1136
- const json = JSON.stringify(value);
1137
- if (Buffer.byteLength(json, "utf8") > MAX_HEADER_JSON_BYTES) {
1138
- throw new X402HeaderError(
1139
- "header_too_large",
1140
- `x402 header JSON exceeds ${MAX_HEADER_JSON_BYTES} bytes`
1141
- );
1142
- }
1143
- return Buffer.from(json, "utf8").toString("base64");
1144
- }
1145
1305
  function decodeX402Header(headerValue) {
1146
1306
  if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
1147
1307
  throw new X402HeaderError(
@@ -1159,284 +1319,218 @@ function decodeX402Header(headerValue) {
1159
1319
  );
1160
1320
  }
1161
1321
  }
1162
- function decodePaymentRequiredHeader(headerValue) {
1163
- const parsed = paymentRequiredSchema.safeParse(decodeX402Header(headerValue));
1322
+
1323
+ // ../../src/x402/standard-requirements.ts
1324
+ import { z as z2 } from "zod";
1325
+ var STANDARD_EXACT_SCHEME = "exact";
1326
+ var standardExactRequirementSchema = z2.object({
1327
+ scheme: z2.literal(STANDARD_EXACT_SCHEME),
1328
+ /** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
1329
+ network: z2.string().min(1),
1330
+ /** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
1331
+ asset: z2.string().min(1),
1332
+ /** Exact price in the asset's atomic units, as a decimal string. */
1333
+ amount: z2.string().regex(/^[1-9]\d*$/),
1334
+ /** Recipient wallet; the transfer destination ATA is derived from it. */
1335
+ payTo: z2.string().min(1),
1336
+ maxTimeoutSeconds: z2.number().int().positive().optional(),
1337
+ extra: z2.object({
1338
+ /** Facilitator address that pays the tx fee (gas sponsorship). */
1339
+ feePayer: z2.string().min(1).optional()
1340
+ }).loose().optional()
1341
+ }).loose();
1342
+ var standardPaymentRequiredSchema = z2.object({
1343
+ x402Version: z2.number().int(),
1344
+ accepts: z2.array(z2.unknown()),
1345
+ error: z2.string().optional(),
1346
+ resource: z2.object({ url: z2.string().optional() }).loose().optional()
1347
+ }).loose();
1348
+ var StandardX402ChallengeError = class extends Error {
1349
+ reason;
1350
+ constructor(reason, message) {
1351
+ super(message);
1352
+ this.name = "StandardX402ChallengeError";
1353
+ this.reason = reason;
1354
+ }
1355
+ };
1356
+ function parseStandardChallenge(challenge) {
1357
+ const parsed = standardPaymentRequiredSchema.safeParse(challenge);
1164
1358
  if (!parsed.success) {
1165
- throw new X402HeaderError(
1359
+ throw new StandardX402ChallengeError(
1166
1360
  "invalid_payment_required",
1167
- "PAYMENT-REQUIRED header is not a valid x402 PaymentRequired object"
1361
+ "Response is not a valid x402 PaymentRequired object"
1168
1362
  );
1169
1363
  }
1170
- const sublyRequirements = parsed.data.accepts.flatMap((candidate) => {
1171
- const requirement = sublyPaymentRequirementsSchema.safeParse(candidate);
1172
- return requirement.success ? [requirement.data] : [];
1364
+ const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
1365
+ const requirement = standardExactRequirementSchema.safeParse(candidate);
1366
+ if (!requirement.success) {
1367
+ return [];
1368
+ }
1369
+ return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
1173
1370
  });
1174
- return { paymentRequired: parsed.data, sublyRequirements };
1371
+ return { paymentRequired: parsed.data, solanaExactRequirements };
1175
1372
  }
1176
- function requestBodyHashFor(body) {
1177
- if (body === null || body === void 0 || body.length === 0) {
1178
- return EMPTY_BODY_HASH;
1373
+ function decodeStandardPaymentRequiredHeader(headerValue) {
1374
+ let decoded;
1375
+ try {
1376
+ decoded = decodeX402Header(headerValue);
1377
+ } catch (error) {
1378
+ throw new StandardX402ChallengeError(
1379
+ error instanceof X402HeaderError ? error.reason : "invalid_header",
1380
+ "Cannot decode the payment-required header"
1381
+ );
1179
1382
  }
1180
- return sha256TaggedHex(
1181
- typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
1182
- );
1383
+ return parseStandardChallenge(decoded);
1384
+ }
1385
+ function selectPayableSolanaRequirement(requirements, options) {
1386
+ const network = options?.network ?? SOLANA_MAINNET_NETWORK;
1387
+ const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
1388
+ const requirement = requirements.find(
1389
+ (candidate) => candidate.network === network && candidate.asset === usdcMint
1390
+ ) ?? null;
1391
+ if (requirement === null) {
1392
+ throw new StandardX402ChallengeError(
1393
+ "no_payable_requirement",
1394
+ `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
1395
+ );
1396
+ }
1397
+ return {
1398
+ requirement,
1399
+ amountRawUsdc: BigInt(requirement.amount),
1400
+ payTo: requirement.payTo,
1401
+ feePayer: requirement.extra?.feePayer ?? null
1402
+ };
1183
1403
  }
1184
1404
 
1185
- // src/client/paid-fetch.ts
1186
- var PaidFetchError = class extends Error {
1405
+ // ../../src/client/standard-x402-payer.ts
1406
+ var StandardX402PayError = class extends Error {
1187
1407
  constructor(reason, message, detail = null) {
1188
1408
  super(message);
1189
1409
  this.reason = reason;
1190
1410
  this.detail = detail;
1191
- this.name = "PaidFetchError";
1411
+ this.name = "StandardX402PayError";
1192
1412
  }
1193
1413
  reason;
1194
1414
  detail;
1195
1415
  };
1196
- var DEFAULT_PENDING_TTL_MS = 11e4;
1197
- var DEFAULT_MAX_TRACKED_URLS = 1e3;
1198
- var DEFAULT_MAX_BODY_CHARS = 2e4;
1199
- function formatRawUsdcAmount(raw) {
1200
- const value = BigInt(raw);
1201
- const negative = value < 0n;
1202
- const abs = negative ? -value : value;
1203
- const whole = abs / 1000000n;
1204
- const frac = (abs % 1000000n).toString().padStart(6, "0");
1205
- return `${negative ? "-" : ""}${whole}.${frac}`;
1206
- }
1207
- var PaidFetchService = class {
1208
- signatureBuilder;
1209
- fetchImpl;
1210
- fetchBudget;
1211
- paymentStatusFor;
1416
+ var StandardX402Payer = class {
1417
+ realizer;
1418
+ x402Fetch;
1419
+ probeFetch;
1212
1420
  defaultMaxAmountRawUsdc;
1213
- pendingTtlMs;
1214
- maxTrackedUrls;
1215
- maxBodyChars;
1216
- nowMs;
1217
- stateStore;
1218
- pending = /* @__PURE__ */ new Map();
1219
- inFlight = /* @__PURE__ */ new Map();
1421
+ network;
1422
+ usdcMint;
1220
1423
  constructor(config) {
1221
- this.signatureBuilder = config.signatureBuilder;
1222
- this.fetchImpl = config.fetchImpl ?? fetch;
1223
- this.fetchBudget = config.fetchBudget ?? (async () => null);
1224
- this.paymentStatusFor = config.paymentStatusFor ?? (async () => "indeterminate");
1424
+ this.realizer = config.realizer;
1425
+ this.x402Fetch = config.x402Fetch;
1426
+ this.probeFetch = config.probeFetch ?? fetch;
1225
1427
  this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
1226
- this.pendingTtlMs = config.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
1227
- this.maxTrackedUrls = config.maxTrackedUrls ?? DEFAULT_MAX_TRACKED_URLS;
1228
- this.maxBodyChars = config.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS;
1229
- this.nowMs = config.nowMs ?? (() => Date.now());
1230
- this.stateStore = config.stateStore ?? null;
1231
- if (this.stateStore !== null) {
1232
- for (const record of this.stateStore.load()) {
1233
- const { url: url2, ...entry } = record;
1234
- this.pending.set(url2, entry);
1235
- }
1236
- }
1237
- }
1238
- /**
1239
- * GET the URL, paying a 402 challenge when needed. Concurrent calls for the
1240
- * same URL share one flow and one result.
1241
- */
1242
- paidFetch(params) {
1243
- const existing = this.inFlight.get(params.url);
1244
- if (existing !== void 0) {
1245
- return existing;
1246
- }
1247
- const flow = this.run(params).finally(() => {
1248
- this.inFlight.delete(params.url);
1249
- });
1250
- this.inFlight.set(params.url, flow);
1251
- return flow;
1252
- }
1253
- async run(params) {
1254
- const { url: url2 } = params;
1255
- const pending = this.pending.get(url2);
1256
- if (pending !== void 0) {
1257
- const expired = this.nowMs() - pending.challengeAtMs > this.pendingTtlMs;
1258
- if (pending.unresolved || expired) {
1259
- if (params.forceNewPayment === true) {
1260
- this.untrack(url2);
1261
- } else {
1262
- const outcome = await this.paymentStatusFor(pending.paymentId);
1263
- if (outcome === "not_settled") {
1264
- this.untrack(url2);
1265
- } else if (outcome === "settled") {
1266
- throw new PaidFetchError(
1267
- "payment_already_settled",
1268
- `the previous payment for this URL settled (paymentId=${pending.paymentId}) but the content delivery was lost, and the signature can no longer be retried. Calling again with forceNewPayment=true will PAY A SECOND TIME for the same resource.`,
1269
- { paymentId: pending.paymentId }
1270
- );
1271
- } else {
1272
- throw new PaidFetchError(
1273
- "payment_outcome_unknown",
1274
- `a previously signed payment for this URL (paymentId=${pending.paymentId}) has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call this tool with forceNewPayment=true.`,
1275
- { paymentId: pending.paymentId }
1276
- );
1277
- }
1278
- }
1279
- } else {
1280
- return this.deliver(url2, pending, {
1281
- retried: true,
1282
- budgetBefore: null
1283
- });
1284
- }
1285
- }
1286
- const challengeAtMs = this.nowMs();
1287
- const first = await this.fetchImpl(url2);
1288
- const firstText = await first.text();
1289
- if (first.status !== 402) {
1290
- return {
1291
- paid: false,
1292
- status: first.status,
1293
- body: this.truncated(firstText)
1294
- };
1295
- }
1296
- const challengeHeader = first.headers.get(PAYMENT_REQUIRED_HEADER);
1297
- if (challengeHeader === null) {
1298
- throw new PaidFetchError(
1299
- "invalid_challenge",
1300
- "402 response is missing the PAYMENT-REQUIRED header"
1301
- );
1302
- }
1303
- const requirement = decodePaymentRequiredHeader(challengeHeader).sublyRequirements[0];
1304
- if (requirement === void 0) {
1305
- throw new PaidFetchError(
1306
- "invalid_challenge",
1307
- "402 challenge contains no subly-yield-exact requirement"
1308
- );
1428
+ this.network = config.network ?? SOLANA_MAINNET_NETWORK;
1429
+ this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
1430
+ }
1431
+ async pay(input) {
1432
+ const init = {
1433
+ method: input.method ?? "GET",
1434
+ ...input.body === void 0 ? {} : { body: input.body },
1435
+ ...input.headers === void 0 ? {} : { headers: input.headers }
1436
+ };
1437
+ const probe = await this.probeFetch(input.url, init);
1438
+ if (probe.status !== 402) {
1439
+ return { paid: false, status: probe.status, body: await probe.text() };
1309
1440
  }
1310
- const amountRawUsdc = BigInt(requirement.amountRawUsdc);
1311
- const maxAmountRawUsdc = params.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1312
- if (amountRawUsdc > maxAmountRawUsdc) {
1313
- throw new PaidFetchError(
1441
+ const selected = await this.selectRequirement(probe);
1442
+ const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
1443
+ if (selected.amountRawUsdc > cap) {
1444
+ throw new StandardX402PayError(
1314
1445
  "amount_exceeds_client_cap",
1315
- `the challenge demands ${formatRawUsdcAmount(amountRawUsdc)} USDC, above this tool call's cap of ${formatRawUsdcAmount(maxAmountRawUsdc)} USDC; nothing was paid. Raise maxAmountRawUsdc only if this price is expected.`,
1316
- {
1317
- amountRawUsdc: requirement.amountRawUsdc,
1318
- maxAmountRawUsdc: maxAmountRawUsdc.toString(),
1319
- payTo: requirement.payTo
1320
- }
1446
+ `the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
1447
+ { amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
1321
1448
  );
1322
1449
  }
1323
- const budgetBefore = await this.fetchBudget();
1324
- const { headerValue, paymentId } = await this.signatureBuilder.buildPaymentSignatureHeader({
1325
- paymentRequiredHeader: challengeHeader,
1326
- httpMethod: "GET",
1327
- url: url2
1328
- });
1329
- const entry = {
1330
- headerValue,
1331
- paymentId,
1332
- amountUsdc: formatRawUsdcAmount(amountRawUsdc),
1333
- payTo: requirement.payTo,
1334
- challengeAtMs,
1335
- unresolved: false
1336
- };
1337
- this.track(url2, entry);
1338
- return this.deliver(url2, entry, { retried: false, budgetBefore });
1339
- }
1340
- /**
1341
- * Sends the signed PAYMENT-SIGNATURE retry. The tracked entry is removed
1342
- * only on confirmed delivery (200). A fresh 402 proves the signature can no
1343
- * longer settle, so the entry is kept as an unresolved marker; every other
1344
- * failure keeps it retryable so the next call reuses the same signature.
1345
- */
1346
- async deliver(url2, entry, params) {
1347
- let second;
1348
- let secondText;
1450
+ let realized;
1349
1451
  try {
1350
- second = await this.fetchImpl(url2, {
1351
- headers: { [PAYMENT_SIGNATURE_HEADER]: entry.headerValue }
1452
+ realized = await this.realizer.ensureUsdcAvailable({
1453
+ amountRawUsdc: selected.amountRawUsdc
1352
1454
  });
1353
- secondText = await second.text();
1354
1455
  } catch (error) {
1355
- throw new PaidFetchError(
1356
- "delivery_failed_payment_pending",
1357
- `delivery request failed in flight (${error instanceof Error ? error.message : String(error)}); the payment (paymentId=${entry.paymentId}) is already signed and may settle. Call this tool again with the same URL to retry delivery with the same payment signature \u2014 do NOT treat this as unpaid.`,
1358
- { paymentId: entry.paymentId }
1456
+ throw new StandardX402PayError(
1457
+ "realize_failed",
1458
+ `could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
1459
+ error
1359
1460
  );
1360
1461
  }
1361
- if (second.status === 200) {
1362
- this.untrack(url2);
1363
- const transaction = receiptTransaction(second.headers);
1364
- return {
1365
- paid: true,
1366
- status: second.status,
1367
- body: this.truncated(secondText),
1368
- ...params.retried ? { retriedPendingPayment: true } : {},
1369
- payment: {
1370
- amountUsdc: entry.amountUsdc,
1371
- payTo: entry.payTo,
1372
- paymentId: entry.paymentId,
1373
- transaction,
1374
- solscanUrl: transaction === null ? null : `https://solscan.io/tx/${transaction}`,
1375
- budgetBefore: params.budgetBefore,
1376
- budgetAfter: await this.fetchBudget()
1377
- }
1378
- };
1379
- }
1380
- if (second.status === 402) {
1381
- entry.unresolved = true;
1382
- this.persist();
1383
- throw new PaidFetchError(
1384
- "payment_outcome_unknown",
1385
- `the seller no longer accepts the signed payment (paymentId=${entry.paymentId}); it may or may not have settled. Verify the payment before purchasing again; to pay again anyway, call this tool with forceNewPayment=true.`,
1386
- { paymentId: entry.paymentId }
1462
+ const response = await this.x402Fetch(input.url, init);
1463
+ const bodyText = await response.text();
1464
+ if (response.status !== 200) {
1465
+ throw new StandardX402PayError(
1466
+ "delivery_failed",
1467
+ `the x402 payment did not deliver (status ${response.status})`,
1468
+ { status: response.status, body: bodyText }
1387
1469
  );
1388
1470
  }
1389
- throw new PaidFetchError(
1390
- "delivery_failed_payment_pending",
1391
- `paid delivery failed with ${second.status} (paymentId=${entry.paymentId}); the payment may already have settled. Call this tool again with the same URL to retry delivery with the same payment signature \u2014 do NOT treat this as unpaid.`,
1392
- {
1393
- paymentId: entry.paymentId,
1394
- status: second.status,
1395
- body: this.truncated(secondText)
1471
+ return {
1472
+ paid: true,
1473
+ status: response.status,
1474
+ body: bodyText,
1475
+ payment: {
1476
+ amountRawUsdc: selected.amountRawUsdc.toString(),
1477
+ payTo: selected.payTo,
1478
+ feePayer: selected.feePayer,
1479
+ realizedRawUsdc: realized.realizedRawUsdc.toString(),
1480
+ realizeTxSignature: realized.txSignature
1396
1481
  }
1397
- );
1482
+ };
1398
1483
  }
1399
- track(url2, entry) {
1400
- if (!this.pending.has(url2) && this.pending.size >= this.maxTrackedUrls) {
1401
- const oldest = this.pending.keys().next();
1402
- if (!oldest.done) {
1403
- this.pending.delete(oldest.value);
1484
+ /** Reads the challenge from the header (preferred) or the JSON body. */
1485
+ async selectRequirement(probe) {
1486
+ const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
1487
+ let requirements;
1488
+ try {
1489
+ if (header !== null) {
1490
+ requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
1491
+ } else {
1492
+ requirements = parseStandardChallenge(
1493
+ await probe.json()
1494
+ ).solanaExactRequirements;
1404
1495
  }
1496
+ } catch (error) {
1497
+ throw new StandardX402PayError(
1498
+ error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
1499
+ "could not parse the x402 402 challenge",
1500
+ error
1501
+ );
1405
1502
  }
1406
- this.pending.set(url2, entry);
1407
- this.persist();
1408
- }
1409
- untrack(url2) {
1410
- this.pending.delete(url2);
1411
- this.persist();
1412
- }
1413
- persist() {
1414
- if (this.stateStore === null) {
1415
- return;
1503
+ try {
1504
+ return selectPayableSolanaRequirement(requirements, {
1505
+ network: this.network,
1506
+ usdcMint: this.usdcMint
1507
+ });
1508
+ } catch (error) {
1509
+ throw new StandardX402PayError(
1510
+ "no_payable_requirement",
1511
+ error instanceof Error ? error.message : String(error),
1512
+ error
1513
+ );
1416
1514
  }
1417
- this.stateStore.save(
1418
- [...this.pending.entries()].map(([url2, entry]) => ({ url: url2, ...entry }))
1419
- );
1420
- }
1421
- truncated(text) {
1422
- return text.length > this.maxBodyChars ? `${text.slice(0, this.maxBodyChars)}
1423
- ... (truncated)` : text;
1424
1515
  }
1425
1516
  };
1426
- function receiptTransaction(headers) {
1427
- try {
1428
- const receiptHeader = headers.get(PAYMENT_RESPONSE_HEADER);
1429
- if (receiptHeader === null) {
1430
- return null;
1431
- }
1432
- const receipt = decodeX402Header(receiptHeader);
1433
- return typeof receipt.transaction === "string" && receipt.transaction.length > 0 ? receipt.transaction : null;
1434
- } catch {
1435
- return null;
1436
- }
1517
+
1518
+ // ../../src/client/relayer-payer.ts
1519
+ function createRelayerX402Payer(config) {
1520
+ const realizer = new RelayerYieldRealizer({
1521
+ facilitatorBaseUrl: config.facilitatorBaseUrl,
1522
+ signer: config.signer,
1523
+ rpc: config.rpc,
1524
+ forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
1525
+ });
1526
+ return new StandardX402Payer({
1527
+ realizer,
1528
+ x402Fetch: config.x402Fetch,
1529
+ defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc
1530
+ });
1437
1531
  }
1438
1532
 
1439
- // src/solana/keys.ts
1533
+ // ../../src/solana/keys.ts
1440
1534
  import { readFileSync } from "node:fs";
1441
1535
  import bs586 from "bs58";
1442
1536
  import {
@@ -1460,304 +1554,110 @@ async function loadKeyPairSigner(params) {
1460
1554
  }
1461
1555
  throw new Error(`${label} keypair is not configured`);
1462
1556
  }
1557
+ function loadSecretKeyBytes(params) {
1558
+ const { base58Secret, jsonFilePath, label } = params;
1559
+ if (base58Secret !== void 0 && base58Secret.length > 0) {
1560
+ const bytes = bs586.decode(base58Secret);
1561
+ if (bytes.length !== 64) {
1562
+ throw new Error(`${label} base58 secret must decode to 64 bytes`);
1563
+ }
1564
+ return bytes;
1565
+ }
1566
+ if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
1567
+ const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
1568
+ if (!Array.isArray(raw) || raw.length !== 64) {
1569
+ throw new Error(`${label} keypair file must be a 64-byte JSON array`);
1570
+ }
1571
+ return Uint8Array.from(raw);
1572
+ }
1573
+ throw new Error(`${label} keypair is not configured`);
1574
+ }
1463
1575
 
1464
- // src/solana/rpc.ts
1576
+ // ../../src/solana/rpc.ts
1465
1577
  import { createSolanaRpc } from "@solana/kit";
1466
1578
  function createRpc(url2) {
1467
1579
  return createSolanaRpc(url2);
1468
1580
  }
1469
1581
 
1470
- // src/x402/client.ts
1471
- var X402ClientError = class extends Error {
1472
- reason;
1473
- detail;
1474
- constructor(reason, message, detail) {
1475
- super(message);
1476
- this.name = "X402ClientError";
1477
- this.reason = reason;
1478
- this.detail = detail;
1479
- }
1480
- };
1481
- var SublyX402Client = class {
1482
- config;
1483
- signer;
1484
- lookupTablesFor;
1485
- fetchImpl;
1486
- constructor(config) {
1487
- this.config = {
1488
- facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
1489
- network: config.network ?? SOLANA_MAINNET_NETWORK
1490
- };
1491
- this.signer = config.signer;
1492
- this.lookupTablesFor = config.lookupTablesFor ?? null;
1493
- this.fetchImpl = config.fetchImpl ?? fetch;
1494
- }
1495
- /**
1496
- * Builds the PAYMENT-SIGNATURE header for a 402 challenge. The request
1497
- * method, URL, and body must be exactly the request being retried; they are
1498
- * bound into the payment and verified again by the seller and facilitator.
1499
- */
1500
- async buildPaymentSignatureHeader(input) {
1501
- const requirement = this.selectRequirement(input.paymentRequiredHeader);
1502
- if (requirement.resource !== input.url) {
1503
- throw new X402ClientError(
1504
- "resource_mismatch",
1505
- "PAYMENT-REQUIRED resource does not match the request URL"
1506
- );
1507
- }
1508
- const requestBodyHash = requestBodyHashFor(input.body);
1509
- const prepared = await this.preparePayment({
1510
- requirement,
1511
- httpMethod: input.httpMethod.toUpperCase(),
1512
- requestBodyHash
1513
- });
1514
- const lookupTables = this.lookupTablesFor === null ? void 0 : await this.lookupTablesFor(prepared.intentJson.serializedTransaction);
1515
- const signed = await this.signer.signPayment({
1516
- intent: prepared.intentJson.signingIntent,
1517
- serializedTransaction: prepared.intentJson.serializedTransaction,
1518
- lookupTables
1519
- });
1520
- const payload = {
1521
- x402Version: X402_VERSION,
1522
- scheme: requirement.scheme,
1523
- network: requirement.network,
1524
- payload: {
1525
- paymentId: prepared.paymentId,
1526
- requestBindingHash: prepared.requestBindingHash,
1527
- preparedMessageHash: prepared.preparedMessageHash,
1528
- serializedTransaction: signed.serializedTransaction,
1529
- agentSignature: signed.agentSignature,
1530
- temporarySettlementSignature: prepared.temporarySettlementSignature
1531
- }
1532
- };
1533
- return {
1534
- headerValue: encodeX402Header(payload),
1535
- paymentId: prepared.paymentId
1536
- };
1537
- }
1538
- /**
1539
- * Convenience wrapper: performs the request, and on a 402 with a Subly
1540
- * requirement pays from vault yield and retries once.
1541
- */
1542
- async fetchWithPayment(url2, init) {
1543
- const first = await this.fetchImpl(url2, init);
1544
- if (first.status !== 402) {
1545
- return first;
1546
- }
1547
- const challenge = first.headers.get(PAYMENT_REQUIRED_HEADER);
1548
- if (challenge === null) {
1549
- return first;
1550
- }
1551
- const { headerValue } = await this.buildPaymentSignatureHeader({
1552
- paymentRequiredHeader: challenge,
1553
- httpMethod: init?.method ?? "GET",
1554
- url: url2,
1555
- body: init?.body ?? null
1556
- });
1557
- return this.fetchImpl(url2, {
1558
- ...init,
1559
- headers: {
1560
- ...init?.headers,
1561
- [PAYMENT_SIGNATURE_HEADER]: headerValue
1582
+ // src/svm-x402-fetch.ts
1583
+ import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
1584
+ import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
1585
+ import { ExactSvmScheme, toClientSvmSigner } from "@x402/svm";
1586
+ async function createSvmX402Fetch(params) {
1587
+ const signer2 = toClientSvmSigner(
1588
+ await createKeyPairSignerFromBytes2(params.agentSecretKey)
1589
+ );
1590
+ const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
1591
+ schemes: [
1592
+ {
1593
+ network: "solana:*",
1594
+ client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
1562
1595
  }
1563
- });
1564
- }
1565
- selectRequirement(paymentRequiredHeader) {
1566
- let sublyRequirements;
1567
- try {
1568
- sublyRequirements = decodePaymentRequiredHeader(
1569
- paymentRequiredHeader
1570
- ).sublyRequirements;
1571
- } catch (error) {
1572
- throw new X402ClientError(
1573
- error instanceof X402HeaderError ? error.reason : "invalid_challenge",
1574
- "Cannot decode the PAYMENT-REQUIRED header"
1575
- );
1576
- }
1577
- const selected = sublyRequirements.find(
1578
- (candidate) => candidate.network === this.config.network && candidate.extra.vault === SUBLY_VAULT.address && candidate.extra.shareMint === SUBLY_VAULT.shareMint && candidate.asset === SUBLY_VAULT.usdcMint
1579
- ) ?? null;
1580
- if (selected === null) {
1581
- throw new X402ClientError(
1582
- "no_supported_requirement",
1583
- "The 402 challenge contains no supported subly-yield-exact requirement"
1584
- );
1585
- }
1586
- return selected;
1587
- }
1588
- async preparePayment(input) {
1589
- const { requirement } = input;
1590
- const wallet = this.signer.walletAddress;
1591
- const prepareUrl = `${this.config.facilitatorBaseUrl}/v1/payments/prepare`;
1592
- const prepareBody = JSON.stringify({
1593
- wallet,
1594
- scheme: requirement.scheme,
1595
- network: requirement.network,
1596
- vault: requirement.extra.vault,
1597
- shareMint: requirement.extra.shareMint,
1598
- asset: requirement.asset,
1599
- seller: requirement.extra.seller,
1600
- sellerRequestId: requirement.extra.sellerRequestId,
1601
- httpMethod: input.httpMethod,
1602
- canonicalResourceUrl: requirement.resource,
1603
- requestBodyHash: input.requestBodyHash,
1604
- amountRawUsdc: requirement.amountRawUsdc,
1605
- payTo: requirement.payTo,
1606
- sellerUsdcAta: requirement.extra.sellerUsdcAta,
1607
- dustRecipientUsdcAta: deriveAssociatedTokenAddress({
1608
- owner: wallet,
1609
- mint: SUBLY_VAULT.usdcMint
1610
- })
1611
- });
1612
- const response = await this.fetchImpl(prepareUrl, {
1613
- method: "POST",
1614
- headers: {
1615
- ...await walletAuthHeaders({
1616
- signer: this.signer,
1617
- method: "POST",
1618
- url: prepareUrl,
1619
- body: prepareBody
1620
- }),
1621
- "content-type": "application/json"
1622
- },
1623
- body: prepareBody
1624
- });
1625
- const body = await response.json();
1626
- if (response.status !== 200) {
1627
- const error = body.error;
1628
- throw new X402ClientError(
1629
- typeof error?.code === "string" ? error.code : "prepare_failed",
1630
- "Payment preparation failed at the facilitator",
1631
- body
1632
- );
1633
- }
1634
- const prepared = body;
1635
- if (typeof prepared.paymentId !== "string" || typeof prepared.preparedMessageHash !== "string" || typeof prepared.intentJson?.serializedTransaction !== "string" || typeof prepared.intentJson?.signingIntent !== "object") {
1636
- throw new X402ClientError(
1637
- "invalid_prepare_response",
1638
- "Facilitator prepare response is missing required fields"
1639
- );
1640
- }
1641
- return prepared;
1642
- }
1643
- };
1596
+ ]
1597
+ });
1598
+ return (url2, init) => wrapped(url2, init);
1599
+ }
1644
1600
 
1645
- // demo/shared.ts
1601
+ // src/pay.ts
1646
1602
  function fail(message) {
1647
- console.error(message);
1603
+ process.stderr.write(`${message}
1604
+ `);
1648
1605
  process.exit(1);
1649
1606
  }
1650
-
1651
- // demo/pay.ts
1652
1607
  var url = process.argv[2];
1653
1608
  if (url === void 0 || !/^https?:\/\//.test(url)) {
1654
- fail("Usage: npm run demo:pay -- <url> [maxAmountRawUsdc]");
1609
+ fail("Usage: pay fetch <url> [maxAmountRawUsdc]");
1655
1610
  }
1656
1611
  var maxAmountArg = process.argv[3];
1657
1612
  var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
1658
1613
  var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
1659
1614
  var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
1660
- var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? "demo/env/mcp-pending-payments.json";
1661
1615
  var keyPairSigner = await loadKeyPairSigner({
1662
1616
  base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1663
1617
  jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1664
1618
  label: "SUBLY_DEMO_AGENT_KEYPAIR"
1665
1619
  });
1620
+ var agentSecretKey = loadSecretKeyBytes({
1621
+ base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
1622
+ jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
1623
+ label: "SUBLY_DEMO_AGENT_KEYPAIR"
1624
+ });
1666
1625
  var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
1667
1626
  var rpc = createRpc(rpcUrl);
1668
- async function fetchBudget() {
1669
- try {
1670
- const budgetUrl = `${facilitatorBaseUrl}/v1/wallets/${signer.walletAddress}/budget`;
1671
- const response = await fetch(budgetUrl, {
1672
- headers: await walletAuthHeaders({ signer, method: "GET", url: budgetUrl })
1673
- });
1674
- if (response.status !== 200) {
1675
- return null;
1676
- }
1677
- const body = await response.json();
1678
- return body.budget === void 0 ? null : {
1679
- positionValueUsdc: formatRawUsdcAmount(body.budget.positionValueRawUsdc),
1680
- spendableYieldUsdc: formatRawUsdcAmount(
1681
- body.budget.spendableYieldRawUsdc
1682
- )
1683
- };
1684
- } catch {
1685
- return null;
1686
- }
1687
- }
1688
- async function paymentStatusFor(paymentId) {
1689
- try {
1690
- const statusUrl = `${facilitatorBaseUrl}/v1/payments/${paymentId}`;
1691
- const response = await fetch(statusUrl, {
1692
- headers: await walletAuthHeaders({ signer, method: "GET", url: statusUrl })
1693
- });
1694
- if (response.status !== 200) {
1695
- return "indeterminate";
1696
- }
1697
- const body = await response.json();
1698
- if (body.status === "settled") {
1699
- return "settled";
1700
- }
1701
- if (body.status === "expired" || body.status === "failed" || body.status === "failed_not_submitted") {
1702
- return "not_settled";
1703
- }
1704
- return "indeterminate";
1705
- } catch {
1706
- return "indeterminate";
1707
- }
1708
- }
1709
- function fileStateStore(path) {
1710
- return {
1711
- load() {
1712
- try {
1713
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
1714
- return Array.isArray(parsed) ? parsed : [];
1715
- } catch {
1716
- return [];
1717
- }
1718
- },
1719
- save(records) {
1720
- try {
1721
- writeFileSync(path, JSON.stringify(records));
1722
- } catch (error) {
1723
- console.error(
1724
- `[subly-pay] failed to persist pending payments: ${error instanceof Error ? error.message : String(error)}`
1725
- );
1726
- }
1727
- }
1728
- };
1729
- }
1730
- var service = new PaidFetchService({
1731
- signatureBuilder: new SublyX402Client({
1732
- facilitatorBaseUrl,
1733
- signer,
1734
- lookupTablesFor: (serializedTransaction) => fetchLookupTablesForTransaction(rpc, serializedTransaction)
1735
- }),
1736
- defaultMaxAmountRawUsdc,
1737
- fetchBudget,
1738
- paymentStatusFor,
1739
- stateStore: fileStateStore(pendingStatePath)
1627
+ var payer = createRelayerX402Payer({
1628
+ facilitatorBaseUrl,
1629
+ signer,
1630
+ rpc,
1631
+ x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
1632
+ defaultMaxAmountRawUsdc
1740
1633
  });
1741
- console.error(`[subly-pay] agent ${signer.walletAddress} -> ${facilitatorBaseUrl}`);
1634
+ var method = process.env.SUBLY_PAY_METHOD;
1635
+ var body = process.env.SUBLY_PAY_BODY;
1636
+ console.error(`[pay] agent ${signer.walletAddress} -> ${url}`);
1742
1637
  try {
1743
1638
  await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
1744
1639
  } catch (error) {
1745
1640
  console.error(
1746
- `[subly-pay] onboarding skipped: ${error instanceof Error ? error.message : String(error)}`
1641
+ `[pay] onboarding skipped: ${error instanceof Error ? error.message : String(error)}`
1747
1642
  );
1748
1643
  }
1749
1644
  try {
1750
- const result = await service.paidFetch({
1645
+ const result = await payer.pay({
1751
1646
  url,
1647
+ ...method === void 0 ? {} : { method },
1648
+ ...body === void 0 ? {} : { body, headers: { "content-type": "application/json" } },
1752
1649
  ...maxAmountArg === void 0 ? {} : { maxAmountRawUsdc: BigInt(maxAmountArg) }
1753
1650
  });
1754
1651
  process.stdout.write(`${JSON.stringify(result, null, 2)}
1755
1652
  `);
1653
+ if (!result.paid) {
1654
+ process.exit(1);
1655
+ }
1756
1656
  } catch (error) {
1757
- if (error instanceof PaidFetchError) {
1657
+ if (error instanceof StandardX402PayError) {
1758
1658
  process.stdout.write(
1759
1659
  `${JSON.stringify(
1760
- { paid: false, refused: true, reason: error.reason, message: error.message, detail: error.detail },
1660
+ { paid: false, reason: error.reason, message: error.message },
1761
1661
  null,
1762
1662
  2
1763
1663
  )}
@@ -1765,5 +1665,5 @@ try {
1765
1665
  );
1766
1666
  process.exit(1);
1767
1667
  }
1768
- fail(`[subly-pay] ${error instanceof Error ? error.message : String(error)}`);
1668
+ fail(`[pay] ${error instanceof Error ? error.message : String(error)}`);
1769
1669
  }