@subly_fi/pay 0.4.0 → 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/README.md +13 -5
- package/dist/deposit.js +153 -13
- package/dist/mcp-server.js +439 -46
- package/dist/pay.js +254 -24
- package/dist/withdraw.js +151 -11
- package/package.json +2 -2
package/dist/pay.js
CHANGED
|
@@ -998,7 +998,7 @@ var OnboardingError = class extends Error {
|
|
|
998
998
|
};
|
|
999
999
|
async function ensureWalletOnboarded(params) {
|
|
1000
1000
|
const fetchImpl = params.fetchImpl ?? fetch;
|
|
1001
|
-
const baseUrl = params.
|
|
1001
|
+
const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
|
|
1002
1002
|
const post = async (step, path, body2) => {
|
|
1003
1003
|
const url2 = `${baseUrl}${path}`;
|
|
1004
1004
|
const serialized = JSON.stringify(body2);
|
|
@@ -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;
|
|
@@ -1100,19 +1104,44 @@ var VaultFlowClient = class {
|
|
|
1100
1104
|
pollTimeoutMs;
|
|
1101
1105
|
pollIntervalMs;
|
|
1102
1106
|
constructor(config) {
|
|
1103
|
-
this.baseUrl = config.
|
|
1107
|
+
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
1104
1108
|
this.signer = config.signer;
|
|
1105
1109
|
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1106
1110
|
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
|
|
1107
1111
|
this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
|
|
1108
1112
|
this.pollIntervalMs = config.pollIntervalMs ?? 2500;
|
|
1109
1113
|
}
|
|
1110
|
-
/**
|
|
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
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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;
|
|
@@ -1313,7 +1453,7 @@ var RelayerYieldRealizer = class {
|
|
|
1313
1453
|
vaultFlows;
|
|
1314
1454
|
constructor(config) {
|
|
1315
1455
|
this.vaultFlows = new VaultFlowClient({
|
|
1316
|
-
|
|
1456
|
+
relayerBaseUrl: config.relayerBaseUrl,
|
|
1317
1457
|
signer: config.signer,
|
|
1318
1458
|
rpc: config.rpc,
|
|
1319
1459
|
...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
|
|
@@ -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
|
|
1716
|
+
const matchingRequirements = requirements.filter(
|
|
1551
1717
|
(candidate) => candidate.network === network && candidate.asset === usdcMint
|
|
1552
|
-
)
|
|
1553
|
-
if (
|
|
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
|
|
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,11 +2074,31 @@ 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) {
|
|
1870
2100
|
const realizer = new RelayerYieldRealizer({
|
|
1871
|
-
|
|
2101
|
+
relayerBaseUrl: config.relayerBaseUrl,
|
|
1872
2102
|
signer: config.signer,
|
|
1873
2103
|
rpc: config.rpc
|
|
1874
2104
|
});
|
|
@@ -2041,7 +2271,7 @@ var agentSecretKey = loadSecretKeyBytes({
|
|
|
2041
2271
|
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
2042
2272
|
var rpc = createRpc(rpcUrl);
|
|
2043
2273
|
var payer = createRelayerX402Payer({
|
|
2044
|
-
|
|
2274
|
+
relayerBaseUrl,
|
|
2045
2275
|
signer,
|
|
2046
2276
|
rpc,
|
|
2047
2277
|
x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
|
|
@@ -2052,7 +2282,7 @@ var method = process.env.SUBLY_PAY_METHOD;
|
|
|
2052
2282
|
var body = process.env.SUBLY_PAY_BODY;
|
|
2053
2283
|
console.error(`[pay] agent ${signer.walletAddress} -> ${url}`);
|
|
2054
2284
|
try {
|
|
2055
|
-
await ensureWalletOnboarded({
|
|
2285
|
+
await ensureWalletOnboarded({ relayerBaseUrl, signer });
|
|
2056
2286
|
} catch (error) {
|
|
2057
2287
|
console.error(
|
|
2058
2288
|
`[pay] onboarding skipped: ${error instanceof Error ? error.message : String(error)}`
|
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;
|
|
@@ -1038,19 +1042,44 @@ var VaultFlowClient = class {
|
|
|
1038
1042
|
pollTimeoutMs;
|
|
1039
1043
|
pollIntervalMs;
|
|
1040
1044
|
constructor(config) {
|
|
1041
|
-
this.baseUrl = config.
|
|
1045
|
+
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
1042
1046
|
this.signer = config.signer;
|
|
1043
1047
|
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1044
1048
|
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
|
|
1045
1049
|
this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
|
|
1046
1050
|
this.pollIntervalMs = config.pollIntervalMs ?? 2500;
|
|
1047
1051
|
}
|
|
1048
|
-
/**
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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";
|
|
@@ -1296,7 +1436,7 @@ var rpc = createRpc(
|
|
|
1296
1436
|
process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
|
|
1297
1437
|
);
|
|
1298
1438
|
var vaultFlows = new VaultFlowClient({
|
|
1299
|
-
|
|
1439
|
+
relayerBaseUrl,
|
|
1300
1440
|
signer,
|
|
1301
1441
|
rpc
|
|
1302
1442
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@subly_fi/pay",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Subly client: pay
|
|
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": {
|