@subly_fi/pay 0.4.1 → 0.6.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 approvalId2 = 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
+ ...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
1130
+ });
1131
+ } catch (error) {
1132
+ if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId2 !== void 0) {
1133
+ throw error;
1134
+ }
1135
+ approvalId2 = await this.findApprovedDepositApproval(input.amountRawUsdc);
1136
+ if (approvalId2 === 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: approvalId2
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) {
@@ -2021,9 +2251,20 @@ function fail(message) {
2021
2251
  }
2022
2252
  var url = process.argv[2];
2023
2253
  if (url === void 0 || !/^https?:\/\//.test(url)) {
2024
- fail("Usage: pay fetch <url> [maxAmountRawUsdc]");
2254
+ fail("Usage: pay fetch <url> [maxAmountRawUsdc] [apr_<approvalId>]");
2255
+ }
2256
+ var maxAmountArg;
2257
+ var approvalId;
2258
+ for (const arg of process.argv.slice(3)) {
2259
+ if (/^apr_[0-9a-f]+$/i.test(arg)) {
2260
+ approvalId = arg;
2261
+ } else if (/^\d+$/.test(arg)) {
2262
+ maxAmountArg = arg;
2263
+ } else {
2264
+ fail(`unrecognized argument: ${arg}
2265
+ Usage: pay fetch <url> [maxAmountRawUsdc] [apr_<approvalId>]`);
2266
+ }
2025
2267
  }
2026
- var maxAmountArg = process.argv[3];
2027
2268
  var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
2028
2269
  var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
2029
2270
  var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
@@ -2064,6 +2305,7 @@ try {
2064
2305
  ...method === void 0 ? {} : { method },
2065
2306
  ...body === void 0 ? {} : { body, headers: { "content-type": "application/json" } },
2066
2307
  ...maxAmountArg === void 0 ? {} : { maxAmountRawUsdc: BigInt(maxAmountArg) },
2308
+ ...approvalId === void 0 ? {} : { approvalId },
2067
2309
  ...process.env.SUBLY_PAY_FORCE_NEW_PAYMENT === "1" ? { forceNewPayment: true } : {}
2068
2310
  });
2069
2311
  process.stdout.write(`${JSON.stringify(result, null, 2)}
@@ -2075,7 +2317,12 @@ try {
2075
2317
  if (error instanceof StandardX402PayError) {
2076
2318
  process.stdout.write(
2077
2319
  `${JSON.stringify(
2078
- { paid: false, reason: error.reason, message: error.message },
2320
+ {
2321
+ paid: false,
2322
+ reason: error.reason,
2323
+ message: error.message,
2324
+ detail: error.detail ?? null
2325
+ },
2079
2326
  null,
2080
2327
  2
2081
2328
  )}