@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/README.md +10 -2
- package/dist/deposit.js +149 -9
- package/dist/mcp-server.js +428 -35
- package/dist/pay.js +248 -18
- package/dist/withdraw.js +149 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,12 +5,20 @@ Subly client for [x402](https://x402.org)-style HTTP payments funded by
|
|
|
5
5
|
deposited USDC, and the principal is never spent. Non-custodial: it signs
|
|
6
6
|
locally with your own Solana key; Subly never holds it.
|
|
7
7
|
|
|
8
|
+
Current payments target standard x402 sellers that offer a Solana USDC `exact`
|
|
9
|
+
rail with facilitator `extra.feePayer` support.
|
|
10
|
+
|
|
8
11
|
Ships one `pay` dispatcher bin with subcommands, all runnable with `npx` (no clone):
|
|
9
12
|
|
|
10
13
|
- `pay mcp` — an MCP server (Claude Code, Cursor, any MCP client) exposing
|
|
11
|
-
the full lifecycle as tools: `
|
|
14
|
+
the full lifecycle as tools: `create_subly_setup_link` /
|
|
15
|
+
`check_subly_setup` (owner onboarding: the human approves the spending
|
|
16
|
+
mandate + first deposit with one Face ID), `deposit_to_subly_vault`,
|
|
12
17
|
`get_subly_yield_budget`, `fetch_with_subly_payment`,
|
|
13
|
-
`withdraw_from_subly_vault
|
|
18
|
+
`withdraw_from_subly_vault`. Payments above the owner's approval
|
|
19
|
+
threshold, deposits, and (when the mandate opts in) withdrawals return an
|
|
20
|
+
`approveUrl` to paste into chat; retry with the `approvalId` once the
|
|
21
|
+
human approved.
|
|
14
22
|
- `pay fetch <url>` — one-shot: pay for a URL, print the receipt (used by the
|
|
15
23
|
OpenClaw skill)
|
|
16
24
|
- `pay deposit <amountRawUsdc>` / `pay withdraw <amountRawUsdc>` — vault
|
package/dist/deposit.js
CHANGED
|
@@ -1078,14 +1078,18 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
|
1078
1078
|
|
|
1079
1079
|
// ../../src/client/vault-flows.ts
|
|
1080
1080
|
var VaultFlowClientError = class extends Error {
|
|
1081
|
-
constructor(step, message, detail = null) {
|
|
1081
|
+
constructor(step, message, detail = null, code = null, errorDetails = null) {
|
|
1082
1082
|
super(message);
|
|
1083
1083
|
this.step = step;
|
|
1084
1084
|
this.detail = detail;
|
|
1085
|
+
this.code = code;
|
|
1086
|
+
this.errorDetails = errorDetails;
|
|
1085
1087
|
this.name = "VaultFlowClientError";
|
|
1086
1088
|
}
|
|
1087
1089
|
step;
|
|
1088
1090
|
detail;
|
|
1091
|
+
code;
|
|
1092
|
+
errorDetails;
|
|
1089
1093
|
};
|
|
1090
1094
|
var VaultFlowClient = class {
|
|
1091
1095
|
baseUrl;
|
|
@@ -1102,12 +1106,37 @@ var VaultFlowClient = class {
|
|
|
1102
1106
|
this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
|
|
1103
1107
|
this.pollIntervalMs = config.pollIntervalMs ?? 2500;
|
|
1104
1108
|
}
|
|
1105
|
-
/**
|
|
1109
|
+
/**
|
|
1110
|
+
* Moves USDC from the agent wallet into the vault (fee sponsored). Under
|
|
1111
|
+
* depositPolicy "owner_approval_required" the relayer refuses to prepare
|
|
1112
|
+
* without an owner approval; when the caller passes none, an already
|
|
1113
|
+
* APPROVED deposit approval for this exact amount (e.g. the mandate's
|
|
1114
|
+
* initialDeposit — "one Face ID covers mandate + first deposit") is looked
|
|
1115
|
+
* up and used automatically before surfacing deposit_approval_required.
|
|
1116
|
+
*/
|
|
1106
1117
|
async deposit(input) {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1118
|
+
let approvalId = input.approvalId;
|
|
1119
|
+
let prepared;
|
|
1120
|
+
try {
|
|
1121
|
+
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1122
|
+
wallet: this.signer.walletAddress,
|
|
1123
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1124
|
+
...approvalId === void 0 ? {} : { approvalId }
|
|
1125
|
+
});
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId !== void 0) {
|
|
1128
|
+
throw error;
|
|
1129
|
+
}
|
|
1130
|
+
approvalId = await this.findApprovedDepositApproval(input.amountRawUsdc);
|
|
1131
|
+
if (approvalId === void 0) {
|
|
1132
|
+
throw error;
|
|
1133
|
+
}
|
|
1134
|
+
prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1135
|
+
wallet: this.signer.walletAddress,
|
|
1136
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1137
|
+
approvalId
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1111
1140
|
const signed = await this.signer.signDeposit({
|
|
1112
1141
|
intent: prepared.signingIntent,
|
|
1113
1142
|
serializedTransaction: prepared.serializedTransaction,
|
|
@@ -1146,7 +1175,9 @@ var VaultFlowClient = class {
|
|
|
1146
1175
|
{
|
|
1147
1176
|
wallet: this.signer.walletAddress,
|
|
1148
1177
|
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1149
|
-
...input.purpose === void 0 ? {} : { purpose: input.purpose }
|
|
1178
|
+
...input.purpose === void 0 ? {} : { purpose: input.purpose },
|
|
1179
|
+
...input.payment === void 0 ? {} : { payment: input.payment },
|
|
1180
|
+
...input.approvalId === void 0 ? {} : { approvalId: input.approvalId }
|
|
1150
1181
|
}
|
|
1151
1182
|
);
|
|
1152
1183
|
const signed = await this.signer.signWithdrawal({
|
|
@@ -1225,6 +1256,70 @@ var VaultFlowClient = class {
|
|
|
1225
1256
|
spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
|
|
1226
1257
|
};
|
|
1227
1258
|
}
|
|
1259
|
+
/** Best-effort audit link: reports the x402 payment tx a realize funded. */
|
|
1260
|
+
async reportPayment(input) {
|
|
1261
|
+
await this.postJson("submit", "/v1/payments/report", {
|
|
1262
|
+
wallet: this.signer.walletAddress,
|
|
1263
|
+
withdrawalId: input.withdrawalId,
|
|
1264
|
+
paymentTxSignature: input.paymentTxSignature
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
/** Wallet's approvals as the relayer sees them (optionally by status). */
|
|
1268
|
+
async listApprovals(status) {
|
|
1269
|
+
const body = await this.getJson(
|
|
1270
|
+
`/v1/wallets/${this.signer.walletAddress}/approvals${status === void 0 ? "" : `?status=${encodeURIComponent(status)}`}`
|
|
1271
|
+
);
|
|
1272
|
+
return body.approvals ?? [];
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Creates the owner-onboarding setup link (wallet-auth pins the agreed
|
|
1276
|
+
* policy + initial deposit). Paste `setupUrl` into the chat verbatim.
|
|
1277
|
+
*/
|
|
1278
|
+
async createSetupSession(input) {
|
|
1279
|
+
return await this.postJson(
|
|
1280
|
+
"prepare",
|
|
1281
|
+
`/v1/wallets/${this.signer.walletAddress}/setup-sessions`,
|
|
1282
|
+
{
|
|
1283
|
+
...input.policy === void 0 ? {} : { policy: input.policy },
|
|
1284
|
+
...input.enforcementMode === void 0 ? {} : { enforcementMode: input.enforcementMode },
|
|
1285
|
+
...input.mandateTtlDays === void 0 ? {} : { mandateTtlDays: input.mandateTtlDays },
|
|
1286
|
+
...input.initialDepositRawUsdc === void 0 ? {} : { initialDepositRawUsdc: input.initialDepositRawUsdc }
|
|
1287
|
+
}
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
/** Polls a setup session (public capability URL — no auth needed). */
|
|
1291
|
+
async getSetupSession(sessionId) {
|
|
1292
|
+
const url = `${this.baseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`;
|
|
1293
|
+
const response = await this.fetchImpl(url);
|
|
1294
|
+
const text = await response.text();
|
|
1295
|
+
if (response.status !== 200) {
|
|
1296
|
+
const parsed = parseRelayerError(text);
|
|
1297
|
+
throw new VaultFlowClientError(
|
|
1298
|
+
"read",
|
|
1299
|
+
parsed.message ?? `setup session read failed with ${response.status}`,
|
|
1300
|
+
text,
|
|
1301
|
+
parsed.code,
|
|
1302
|
+
parsed.details
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
return JSON.parse(text);
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Finds an APPROVED, unconsumed deposit approval bound to exactly this
|
|
1309
|
+
* amount — the shape the mandate's initialDeposit approval has.
|
|
1310
|
+
*/
|
|
1311
|
+
async findApprovedDepositApproval(amountRawUsdc2) {
|
|
1312
|
+
try {
|
|
1313
|
+
const approvals = await this.listApprovals("approved");
|
|
1314
|
+
const match = approvals.find((approval) => {
|
|
1315
|
+
const binding = approval.binding;
|
|
1316
|
+
return binding?.kind === "deposit" && binding.amountRawUsdc === amountRawUsdc2.toString();
|
|
1317
|
+
});
|
|
1318
|
+
return match?.approvalId;
|
|
1319
|
+
} catch {
|
|
1320
|
+
return void 0;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1228
1323
|
/**
|
|
1229
1324
|
* Polls the reconciling GET endpoint until the intent leaves "submitted"
|
|
1230
1325
|
* (each read looks the tx up on-chain) or the timeout elapses.
|
|
@@ -1274,10 +1369,13 @@ var VaultFlowClient = class {
|
|
|
1274
1369
|
});
|
|
1275
1370
|
const text = await response.text();
|
|
1276
1371
|
if (response.status !== 200) {
|
|
1372
|
+
const parsed = parseRelayerError(text);
|
|
1277
1373
|
throw new VaultFlowClientError(
|
|
1278
1374
|
step,
|
|
1279
|
-
`${path} failed with ${response.status}: ${text}`,
|
|
1280
|
-
text
|
|
1375
|
+
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
1376
|
+
text,
|
|
1377
|
+
parsed.code,
|
|
1378
|
+
parsed.details
|
|
1281
1379
|
);
|
|
1282
1380
|
}
|
|
1283
1381
|
try {
|
|
@@ -1290,7 +1388,49 @@ var VaultFlowClient = class {
|
|
|
1290
1388
|
);
|
|
1291
1389
|
}
|
|
1292
1390
|
}
|
|
1391
|
+
async getJson(path) {
|
|
1392
|
+
const url = `${this.baseUrl}${path}`;
|
|
1393
|
+
const response = await this.fetchImpl(url, {
|
|
1394
|
+
headers: await walletAuthHeaders({
|
|
1395
|
+
signer: this.signer,
|
|
1396
|
+
method: "GET",
|
|
1397
|
+
url
|
|
1398
|
+
})
|
|
1399
|
+
});
|
|
1400
|
+
const text = await response.text();
|
|
1401
|
+
if (response.status !== 200) {
|
|
1402
|
+
const parsed = parseRelayerError(text);
|
|
1403
|
+
throw new VaultFlowClientError(
|
|
1404
|
+
"read",
|
|
1405
|
+
parsed.message === null ? `${path} failed with ${response.status}: ${text}` : `${path} failed (${parsed.code ?? response.status}): ${parsed.message}`,
|
|
1406
|
+
text,
|
|
1407
|
+
parsed.code,
|
|
1408
|
+
parsed.details
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
try {
|
|
1412
|
+
return JSON.parse(text);
|
|
1413
|
+
} catch {
|
|
1414
|
+
throw new VaultFlowClientError(
|
|
1415
|
+
"read",
|
|
1416
|
+
`${path} returned 200 with a non-JSON body`,
|
|
1417
|
+
text
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1293
1421
|
};
|
|
1422
|
+
function parseRelayerError(text) {
|
|
1423
|
+
try {
|
|
1424
|
+
const parsed = JSON.parse(text);
|
|
1425
|
+
return {
|
|
1426
|
+
code: typeof parsed.error?.code === "string" ? parsed.error.code : null,
|
|
1427
|
+
message: typeof parsed.error?.message === "string" ? parsed.error.message : null,
|
|
1428
|
+
details: parsed.error?.details ?? null
|
|
1429
|
+
};
|
|
1430
|
+
} catch {
|
|
1431
|
+
return { code: null, message: null, details: null };
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1294
1434
|
|
|
1295
1435
|
// ../../src/solana/keys.ts
|
|
1296
1436
|
import { readFileSync } from "node:fs";
|