@subly_fi/pay 0.4.1 → 0.5.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
@@ -1083,14 +1083,18 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
1083
1083
 
1084
1084
  // ../../src/client/vault-flows.ts
1085
1085
  var VaultFlowClientError = class extends Error {
1086
- constructor(step, message, detail = null) {
1086
+ constructor(step, message, detail = null, code = null, errorDetails = null) {
1087
1087
  super(message);
1088
1088
  this.step = step;
1089
1089
  this.detail = detail;
1090
+ this.code = code;
1091
+ this.errorDetails = errorDetails;
1090
1092
  this.name = "VaultFlowClientError";
1091
1093
  }
1092
1094
  step;
1093
1095
  detail;
1096
+ code;
1097
+ errorDetails;
1094
1098
  };
1095
1099
  var VaultFlowClient = class {
1096
1100
  baseUrl;
@@ -1107,12 +1111,37 @@ var VaultFlowClient = class {
1107
1111
  this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1108
1112
  this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1109
1113
  }
1110
- /** Moves USDC from the agent wallet into the vault (fee sponsored). */
1114
+ /**
1115
+ * Moves USDC from the agent wallet into the vault (fee sponsored). Under
1116
+ * depositPolicy "owner_approval_required" the relayer refuses to prepare
1117
+ * without an owner approval; when the caller passes none, an already
1118
+ * APPROVED deposit approval for this exact amount (e.g. the mandate's
1119
+ * initialDeposit — "one Face ID covers mandate + first deposit") is looked
1120
+ * up and used automatically before surfacing deposit_approval_required.
1121
+ */
1111
1122
  async deposit(input) {
1112
- const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1113
- wallet: this.signer.walletAddress,
1114
- amountRawUsdc: input.amountRawUsdc.toString()
1115
- });
1123
+ let approvalId = input.approvalId;
1124
+ let prepared;
1125
+ try {
1126
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1127
+ wallet: this.signer.walletAddress,
1128
+ amountRawUsdc: input.amountRawUsdc.toString(),
1129
+ ...approvalId === void 0 ? {} : { approvalId }
1130
+ });
1131
+ } catch (error) {
1132
+ if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
1133
+ throw error;
1134
+ }
1135
+ approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
1136
+ if (approvalId === void 0) {
1137
+ throw error;
1138
+ }
1139
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1140
+ wallet: this.signer.walletAddress,
1141
+ amountRawUsdc: input.amountRawUsdc.toString(),
1142
+ approvalId
1143
+ });
1144
+ }
1116
1145
  const signed = await this.signer.signDeposit({
1117
1146
  intent: prepared.signingIntent,
1118
1147
  serializedTransaction: prepared.serializedTransaction,
@@ -1151,7 +1180,9 @@ var VaultFlowClient = class {
1151
1180
  {
1152
1181
  wallet: this.signer.walletAddress,
1153
1182
  amountRawUsdc: input.amountRawUsdc.toString(),
1154
- ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1183
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1184
+ ...input.payment === void 0 ? {} : { payment: input.payment },
1185
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1155
1186
  }
1156
1187
  );
1157
1188
  const signed = await this.signer.signWithdrawal({
@@ -1230,6 +1261,70 @@ var VaultFlowClient = class {
1230
1261
  spendableYieldRawUsdc: body2.budget?.spendableYieldRawUsdc ?? "0"
1231
1262
  };
1232
1263
  }
1264
+ /** Best-effort audit link: reports the x402 payment tx a realize funded. */
1265
+ async reportPayment(input) {
1266
+ await this.postJson("submit", "/v1/payments/report", {
1267
+ wallet: this.signer.walletAddress,
1268
+ withdrawalId: input.withdrawalId,
1269
+ paymentTxSignature: input.paymentTxSignature
1270
+ });
1271
+ }
1272
+ /** Wallet's approvals as the relayer sees them (optionally by status). */
1273
+ async listApprovals(status) {
1274
+ const body2 = await this.getJson(
1275
+ `/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" : `?status=${encodeURIComponent(status)}`}`
1276
+ );
1277
+ return body2.approvals ?? [];
1278
+ }
1279
+ /**
1280
+ * Creates the owner-onboarding setup link (wallet-auth pins the agreed
1281
+ * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1282
+ */
1283
+ async createSetupSession(input) {
1284
+ return await this.postJson(
1285
+ "prepare",
1286
+ `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1287
+ {
1288
+ ...input.policy === void 0 ? {} : { policy: input.policy },
1289
+ ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1290
+ ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1291
+ ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1292
+ }
1293
+ );
1294
+ }
1295
+ /** Polls a setup session (public capability URL — no auth needed). */
1296
+ async getSetupSession(sessionId) {
1297
+ const url2 = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
1298
+ const response = await this.fetchImpl(url2);
1299
+ const text = await response.text();
1300
+ if (response.status !== 200) {
1301
+ const parsed = parseRelayerError(text);
1302
+ throw new VaultFlowClientError(
1303
+ "read",
1304
+ parsed.message ?? `setup session read failed with ${response.status}`,
1305
+ text,
1306
+ parsed.code,
1307
+ parsed.details
1308
+ );
1309
+ }
1310
+ return JSON.parse(text);
1311
+ }
1312
+ /**
1313
+ * Finds an APPROVED, unconsumed deposit approval bound to exactly this
1314
+ * amount — the shape the mandate's initialDeposit approval has.
1315
+ */
1316
+ async findApprovedDepositApproval(amountRawUsdc) {
1317
+ try {
1318
+ const approvals = await this.listApprovals("approved");
1319
+ const match = approvals.find((approval) => {
1320
+ const binding = approval.binding;
1321
+ return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc.toString();
1322
+ });
1323
+ return match?.approvalId;
1324
+ } catch {
1325
+ return void 0;
1326
+ }
1327
+ }
1233
1328
  /**
1234
1329
  * Polls the reconciling GET endpoint until the intent leaves "submitted"
1235
1330
  * (each read looks the tx up on-chain) or the timeout elapses.
@@ -1279,10 +1374,13 @@ var VaultFlowClient = class {
1279
1374
  });
1280
1375
  const text = await response.text();
1281
1376
  if (response.status !== 200) {
1377
+ const parsed = parseRelayerError(text);
1282
1378
  throw new VaultFlowClientError(
1283
1379
  step,
1284
- `${path} failed with ${response.status}: ${text}`,
1285
- text
1380
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1381
+ text,
1382
+ parsed.code,
1383
+ parsed.details
1286
1384
  );
1287
1385
  }
1288
1386
  try {
@@ -1295,7 +1393,49 @@ var VaultFlowClient = class {
1295
1393
  );
1296
1394
  }
1297
1395
  }
1396
+ async getJson(path) {
1397
+ const url2 = `${this.baseUrl}${path}`;
1398
+ const response = await this.fetchImpl(url2, {
1399
+ headers: await walletAuthHeaders({
1400
+ signer: this.signer,
1401
+ method: "GET",
1402
+ url: url2
1403
+ })
1404
+ });
1405
+ const text = await response.text();
1406
+ if (response.status !== 200) {
1407
+ const parsed = parseRelayerError(text);
1408
+ throw new VaultFlowClientError(
1409
+ "read",
1410
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1411
+ text,
1412
+ parsed.code,
1413
+ parsed.details
1414
+ );
1415
+ }
1416
+ try {
1417
+ return JSON.parse(text);
1418
+ } catch {
1419
+ throw new VaultFlowClientError(
1420
+ "read",
1421
+ `${path} returned 200 with a non-JSON body`,
1422
+ text
1423
+ );
1424
+ }
1425
+ }
1298
1426
  };
1427
+ function parseRelayerError(text) {
1428
+ try {
1429
+ const parsed = JSON.parse(text);
1430
+ return {
1431
+ code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
1432
+ message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
1433
+ details: parsed.error?.details ?? null
1434
+ };
1435
+ } catch {
1436
+ return { code: null, message: null, details: null };
1437
+ }
1438
+ }
1299
1439
 
1300
1440
  // ../../src/client/relayer-yield-realizer.ts
1301
1441
  var REALIZE_OVERHEAD_RAW_USDC = 2500n;
@@ -1329,7 +1469,11 @@ var RelayerYieldRealizer = class {
1329
1469
  amountRawUsdc: shortfallRawUsdc,
1330
1470
  // The relayer refuses to prepare this withdrawal beyond the spendable
1331
1471
  // yield — the principal-protection guard the client cannot bypass.
1332
- purpose: "yield_realize"
1472
+ purpose: "yield_realize",
1473
+ // Declares what is being paid so the relayer's spending-mandate layer
1474
+ // can enforce caps/payee and keep the mandate → payment audit chain.
1475
+ ...input.payment === void 0 ? {} : { payment: input.payment },
1476
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1333
1477
  });
1334
1478
  } catch (error) {
1335
1479
  throw this.mapWithdrawError(error);
@@ -1343,9 +1487,18 @@ var RelayerYieldRealizer = class {
1343
1487
  }
1344
1488
  return {
1345
1489
  realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
1346
- txSignature: outcome.txSignature
1490
+ txSignature: outcome.txSignature,
1491
+ withdrawalId: outcome.withdrawalId
1347
1492
  };
1348
1493
  }
1494
+ /**
1495
+ * Best-effort report-back of the x402 payment tx this realize funded —
1496
+ * closes the relayer's mandate → realize → payment audit chain. Callers
1497
+ * must never let a failure here affect the payment result.
1498
+ */
1499
+ async reportPayment(input) {
1500
+ await this.vaultFlows.reportPayment(input);
1501
+ }
1349
1502
  /**
1350
1503
  * Refuses to realize more than the ledger's spendable yield (principal).
1351
1504
  * getBudget syncs the relayer's ledger from chain first (best-effort), so a
@@ -1380,7 +1533,14 @@ var RelayerYieldRealizer = class {
1380
1533
  error
1381
1534
  );
1382
1535
  }
1383
- const serverCode = errorCodeFrom(error.detail);
1536
+ const serverCode = error.code ?? errorCodeFrom(error.detail);
1537
+ if (serverCode === "approval_required") {
1538
+ return new RelayerRealizeError(
1539
+ "approval_required",
1540
+ "this payment exceeds the owner-approval threshold; nothing was realized or paid. Ask the owner to approve, then retry with the approvalId",
1541
+ error.errorDetails ?? error.detail
1542
+ );
1543
+ }
1384
1544
  if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
1385
1545
  return new RelayerRealizeError(
1386
1546
  "insufficient_yield",
@@ -1407,6 +1567,12 @@ function errorCodeFrom(detail) {
1407
1567
  }
1408
1568
  }
1409
1569
 
1570
+ // ../../src/lib/canonical-json.ts
1571
+ import { createHash as createHash4 } from "node:crypto";
1572
+ function sha256HexOf(data) {
1573
+ return createHash4("sha256").update(data, "utf8").digest("hex");
1574
+ }
1575
+
1410
1576
  // ../../src/x402/headers.ts
1411
1577
  import { z } from "zod";
1412
1578
  var PAYMENT_REQUIRED_HEADER = "payment-required";
@@ -1547,20 +1713,36 @@ function decodeStandardPaymentRequiredHeader(headerValue) {
1547
1713
  function selectPayableSolanaRequirement(requirements, options) {
1548
1714
  const network = options?.network ?? SOLANA_MAINNET_NETWORK;
1549
1715
  const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
1550
- const requirement = requirements.find(
1716
+ const matchingRequirements = requirements.filter(
1551
1717
  (candidate) => candidate.network === network && candidate.asset === usdcMint
1552
- ) ?? null;
1553
- if (requirement === null) {
1718
+ );
1719
+ if (matchingRequirements.length === 0) {
1554
1720
  throw new StandardX402ChallengeError(
1555
1721
  "no_payable_requirement",
1556
1722
  `The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
1557
1723
  );
1558
1724
  }
1725
+ const requirement = matchingRequirements.find(
1726
+ (candidate) => candidate.extra?.feePayer !== void 0
1727
+ ) ?? null;
1728
+ if (requirement === null) {
1729
+ throw new StandardX402ChallengeError(
1730
+ "missing_svm_fee_payer",
1731
+ "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
1732
+ );
1733
+ }
1734
+ const feePayer = requirement.extra?.feePayer;
1735
+ if (feePayer === void 0) {
1736
+ throw new StandardX402ChallengeError(
1737
+ "missing_svm_fee_payer",
1738
+ "The Solana exact requirement does not include extra.feePayer, which the official @x402/svm client requires for gas sponsorship"
1739
+ );
1740
+ }
1559
1741
  return {
1560
1742
  requirement,
1561
1743
  amountRawUsdc: BigInt(requirement.amount),
1562
1744
  payTo: requirement.payTo,
1563
- feePayer: requirement.extra?.feePayer ?? null
1745
+ feePayer
1564
1746
  };
1565
1747
  }
1566
1748
  function standardRequirementMatchesSelected(candidate, selected) {
@@ -1683,9 +1865,23 @@ var StandardX402Payer = class {
1683
1865
  let realized;
1684
1866
  try {
1685
1867
  realized = await this.realizer.ensureUsdcAvailable({
1686
- amountRawUsdc: selected.amountRawUsdc
1868
+ amountRawUsdc: selected.amountRawUsdc,
1869
+ payment: {
1870
+ payTo: selected.payTo,
1871
+ amountRawUsdc: selected.amountRawUsdc.toString(),
1872
+ resourceUrlHash: sha256HexOf(input.url),
1873
+ method: method2
1874
+ },
1875
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1687
1876
  });
1688
1877
  } catch (error) {
1878
+ if (error.code === "approval_required") {
1879
+ throw new StandardX402PayError(
1880
+ "approval_required",
1881
+ "this payment exceeds the owner-approval threshold; NOTHING was paid. Ask the owner to open the approveUrl, then retry the same call with the approvalId",
1882
+ error.detail ?? null
1883
+ );
1884
+ }
1689
1885
  throw new StandardX402PayError(
1690
1886
  "realize_failed",
1691
1887
  `could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
@@ -1741,6 +1937,19 @@ var StandardX402Payer = class {
1741
1937
  );
1742
1938
  }
1743
1939
  this.clearDelivered(pendingKey);
1940
+ const paymentTxSignature = extractSettledPaymentTxSignature(response);
1941
+ if (paymentTxSignature !== null && typeof realized.withdrawalId === "string" && this.realizer.reportPayment !== void 0) {
1942
+ try {
1943
+ await this.realizer.reportPayment({
1944
+ withdrawalId: realized.withdrawalId,
1945
+ paymentTxSignature
1946
+ });
1947
+ } catch (error) {
1948
+ console.error(
1949
+ `[subly-x402] payment report-back failed (audit only, payment ok): ${error instanceof Error ? error.message : String(error)}`
1950
+ );
1951
+ }
1952
+ }
1744
1953
  return {
1745
1954
  paid: true,
1746
1955
  status: response.status,
@@ -1750,7 +1959,8 @@ var StandardX402Payer = class {
1750
1959
  payTo: selected.payTo,
1751
1960
  feePayer: selected.feePayer,
1752
1961
  realizedRawUsdc: realized.realizedRawUsdc.toString(),
1753
- realizeTxSignature: realized.txSignature
1962
+ realizeTxSignature: realized.txSignature,
1963
+ paymentTxSignature
1754
1964
  }
1755
1965
  };
1756
1966
  }
@@ -1864,6 +2074,26 @@ var StandardX402Payer = class {
1864
2074
  function pendingPaymentKey(input) {
1865
2075
  return `${input.method}:${input.url}:${input.requestBodyHash}`;
1866
2076
  }
2077
+ function extractSettledPaymentTxSignature(response) {
2078
+ const header = response.headers.get("x-payment-response");
2079
+ if (header === null || header.length === 0) {
2080
+ return null;
2081
+ }
2082
+ try {
2083
+ const decoded = JSON.parse(
2084
+ Buffer.from(header, "base64").toString("utf8")
2085
+ );
2086
+ if (typeof decoded.transaction === "string" && decoded.transaction.length > 0) {
2087
+ return decoded.transaction;
2088
+ }
2089
+ if (typeof decoded.txHash === "string" && decoded.txHash.length > 0) {
2090
+ return decoded.txHash;
2091
+ }
2092
+ return null;
2093
+ } catch {
2094
+ return null;
2095
+ }
2096
+ }
1867
2097
 
1868
2098
  // ../../src/client/relayer-payer.ts
1869
2099
  function createRelayerX402Payer(config) {
package/dist/withdraw.js CHANGED
@@ -1021,14 +1021,18 @@ async function walletAuthHeaders(params) {
1021
1021
 
1022
1022
  // ../../src/client/vault-flows.ts
1023
1023
  var VaultFlowClientError = class extends Error {
1024
- constructor(step, message, detail = null) {
1024
+ constructor(step, message, detail = null, code = null, errorDetails = null) {
1025
1025
  super(message);
1026
1026
  this.step = step;
1027
1027
  this.detail = detail;
1028
+ this.code = code;
1029
+ this.errorDetails = errorDetails;
1028
1030
  this.name = "VaultFlowClientError";
1029
1031
  }
1030
1032
  step;
1031
1033
  detail;
1034
+ code;
1035
+ errorDetails;
1032
1036
  };
1033
1037
  var VaultFlowClient = class {
1034
1038
  baseUrl;
@@ -1045,12 +1049,37 @@ var VaultFlowClient = class {
1045
1049
  this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
1046
1050
  this.pollIntervalMs = config.pollIntervalMs ?? 2500;
1047
1051
  }
1048
- /** Moves USDC from the agent wallet into the vault (fee sponsored). */
1052
+ /**
1053
+ * Moves USDC from the agent wallet into the vault (fee sponsored). Under
1054
+ * depositPolicy "owner_approval_required" the relayer refuses to prepare
1055
+ * without an owner approval; when the caller passes none, an already
1056
+ * APPROVED deposit approval for this exact amount (e.g. the mandate's
1057
+ * initialDeposit — "one Face ID covers mandate + first deposit") is looked
1058
+ * up and used automatically before surfacing deposit_approval_required.
1059
+ */
1049
1060
  async deposit(input) {
1050
- const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1051
- wallet: this.signer.walletAddress,
1052
- amountRawUsdc: input.amountRawUsdc.toString()
1053
- });
1061
+ let approvalId = input.approvalId;
1062
+ let prepared;
1063
+ try {
1064
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1065
+ wallet: this.signer.walletAddress,
1066
+ amountRawUsdc: input.amountRawUsdc.toString(),
1067
+ ...approvalId === void 0 ? {} : { approvalId }
1068
+ });
1069
+ } catch (error) {
1070
+ if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
1071
+ throw error;
1072
+ }
1073
+ approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
1074
+ if (approvalId === void 0) {
1075
+ throw error;
1076
+ }
1077
+ prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
1078
+ wallet: this.signer.walletAddress,
1079
+ amountRawUsdc: input.amountRawUsdc.toString(),
1080
+ approvalId
1081
+ });
1082
+ }
1054
1083
  const signed = await this.signer.signDeposit({
1055
1084
  intent: prepared.signingIntent,
1056
1085
  serializedTransaction: prepared.serializedTransaction,
@@ -1089,7 +1118,9 @@ var VaultFlowClient = class {
1089
1118
  {
1090
1119
  wallet: this.signer.walletAddress,
1091
1120
  amountRawUsdc: input.amountRawUsdc.toString(),
1092
- ...input.purpose === void 0 ? {} : { purpose: input.purpose }
1121
+ ...input.purpose === void 0 ? {} : { purpose: input.purpose },
1122
+ ...input.payment === void 0 ? {} : { payment: input.payment },
1123
+ ...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
1093
1124
  }
1094
1125
  );
1095
1126
  const signed = await this.signer.signWithdrawal({
@@ -1168,6 +1199,70 @@ var VaultFlowClient = class {
1168
1199
  spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
1169
1200
  };
1170
1201
  }
1202
+ /** Best-effort audit link: reports the x402 payment tx a realize funded. */
1203
+ async reportPayment(input) {
1204
+ await this.postJson("submit", "/v1/payments/report", {
1205
+ wallet: this.signer.walletAddress,
1206
+ withdrawalId: input.withdrawalId,
1207
+ paymentTxSignature: input.paymentTxSignature
1208
+ });
1209
+ }
1210
+ /** Wallet's approvals as the relayer sees them (optionally by status). */
1211
+ async listApprovals(status) {
1212
+ const body = await this.getJson(
1213
+ `/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" : `?status=${encodeURIComponent(status)}`}`
1214
+ );
1215
+ return body.approvals ?? [];
1216
+ }
1217
+ /**
1218
+ * Creates the owner-onboarding setup link (wallet-auth pins the agreed
1219
+ * policy + initial deposit). Paste `setupUrl` into the chat verbatim.
1220
+ */
1221
+ async createSetupSession(input) {
1222
+ return await this.postJson(
1223
+ "prepare",
1224
+ `/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
1225
+ {
1226
+ ...input.policy === void 0 ? {} : { policy: input.policy },
1227
+ ...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
1228
+ ...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
1229
+ ...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
1230
+ }
1231
+ );
1232
+ }
1233
+ /** Polls a setup session (public capability URL — no auth needed). */
1234
+ async getSetupSession(sessionId) {
1235
+ const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
1236
+ const response = await this.fetchImpl(url);
1237
+ const text = await response.text();
1238
+ if (response.status !== 200) {
1239
+ const parsed = parseRelayerError(text);
1240
+ throw new VaultFlowClientError(
1241
+ "read",
1242
+ parsed.message ?? `setup session read failed with ${response.status}`,
1243
+ text,
1244
+ parsed.code,
1245
+ parsed.details
1246
+ );
1247
+ }
1248
+ return JSON.parse(text);
1249
+ }
1250
+ /**
1251
+ * Finds an APPROVED, unconsumed deposit approval bound to exactly this
1252
+ * amount — the shape the mandate's initialDeposit approval has.
1253
+ */
1254
+ async findApprovedDepositApproval(amountRawUsdc2) {
1255
+ try {
1256
+ const approvals = await this.listApprovals("approved");
1257
+ const match = approvals.find((approval) => {
1258
+ const binding = approval.binding;
1259
+ return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc2.toString();
1260
+ });
1261
+ return match?.approvalId;
1262
+ } catch {
1263
+ return void 0;
1264
+ }
1265
+ }
1171
1266
  /**
1172
1267
  * Polls the reconciling GET endpoint until the intent leaves "submitted"
1173
1268
  * (each read looks the tx up on-chain) or the timeout elapses.
@@ -1217,10 +1312,13 @@ var VaultFlowClient = class {
1217
1312
  });
1218
1313
  const text = await response.text();
1219
1314
  if (response.status !== 200) {
1315
+ const parsed = parseRelayerError(text);
1220
1316
  throw new VaultFlowClientError(
1221
1317
  step,
1222
- `${path} failed with ${response.status}: ${text}`,
1223
- text
1318
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1319
+ text,
1320
+ parsed.code,
1321
+ parsed.details
1224
1322
  );
1225
1323
  }
1226
1324
  try {
@@ -1233,7 +1331,49 @@ var VaultFlowClient = class {
1233
1331
  );
1234
1332
  }
1235
1333
  }
1334
+ async getJson(path) {
1335
+ const url = `${this.baseUrl}${path}`;
1336
+ const response = await this.fetchImpl(url, {
1337
+ headers: await walletAuthHeaders({
1338
+ signer: this.signer,
1339
+ method: "GET",
1340
+ url
1341
+ })
1342
+ });
1343
+ const text = await response.text();
1344
+ if (response.status !== 200) {
1345
+ const parsed = parseRelayerError(text);
1346
+ throw new VaultFlowClientError(
1347
+ "read",
1348
+ parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
1349
+ text,
1350
+ parsed.code,
1351
+ parsed.details
1352
+ );
1353
+ }
1354
+ try {
1355
+ return JSON.parse(text);
1356
+ } catch {
1357
+ throw new VaultFlowClientError(
1358
+ "read",
1359
+ `${path} returned 200 with a non-JSON body`,
1360
+ text
1361
+ );
1362
+ }
1363
+ }
1236
1364
  };
1365
+ function parseRelayerError(text) {
1366
+ try {
1367
+ const parsed = JSON.parse(text);
1368
+ return {
1369
+ code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
1370
+ message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
1371
+ details: parsed.error?.details ?? null
1372
+ };
1373
+ } catch {
1374
+ return { code: null, message: null, details: null };
1375
+ }
1376
+ }
1237
1377
 
1238
1378
  // ../../src/solana/keys.ts
1239
1379
  import { readFileSync } from "node:fs";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@subly_fi/pay",
3
- "version": "0.4.1",
4
- "description": "Subly client: pay for ANY standard x402 (HTTP 402) paid API from Kamino vault yield — the seller needs no Subly integration. Ships an MCP server and a one-shot pay/deposit CLI; non-custodial (signs locally with your own key).",
3
+ "version": "0.5.0",
4
+ "description": "Subly client: pay compatible standard x402 Solana USDC exact APIs from Kamino vault yield — the seller needs no Subly integration. Ships an MCP server and a one-shot pay/deposit CLI; non-custodial (signs locally with your own key).",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "engines": {