@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/README.md +18 -6
- package/dist/cli.js +4 -2
- package/dist/deposit.js +192 -11
- package/dist/mcp-server.js +428 -35
- package/dist/pay.js +268 -21
- package/dist/setup-link.js +1494 -0
- package/dist/setup-status.js +27 -0
- package/dist/withdraw.js +175 -11
- package/package.json +2 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/setup-status.ts
|
|
2
|
+
function fail(message) {
|
|
3
|
+
process.stderr.write(`${message}
|
|
4
|
+
`);
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}
|
|
7
|
+
var arg = process.argv[2];
|
|
8
|
+
if (arg === void 0) {
|
|
9
|
+
fail("Usage: pay setup-status <st_sessionId | setupUrl>");
|
|
10
|
+
}
|
|
11
|
+
var match = arg.match(/st_[0-9a-f]+/i);
|
|
12
|
+
if (match === null) {
|
|
13
|
+
fail(`no session id (st_...) found in: ${arg}`);
|
|
14
|
+
}
|
|
15
|
+
var sessionId = match[0];
|
|
16
|
+
var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
|
|
17
|
+
var response = await fetch(
|
|
18
|
+
`${relayerBaseUrl}/v1/setup-sessions/${encodeURIComponent(sessionId)}`
|
|
19
|
+
);
|
|
20
|
+
var text = await response.text();
|
|
21
|
+
if (response.status !== 200) {
|
|
22
|
+
process.stdout.write(`${text}
|
|
23
|
+
`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
process.stdout.write(`${text}
|
|
27
|
+
`);
|
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
|
-
/**
|
|
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 approvalId2 = 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
|
+
...approvalId2 === void 0 ? {} : { approvalId: approvalId2 }
|
|
1068
|
+
});
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
if (!(error instanceof VaultFlowClientError) || error.code !== "deposit_approval_required" || approvalId2 !== void 0) {
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
1073
|
+
approvalId2 = await this.findApprovedDepositApproval(input.amountRawUsdc);
|
|
1074
|
+
if (approvalId2 === 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: approvalId2
|
|
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";
|
|
@@ -1283,9 +1423,13 @@ var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITA
|
|
|
1283
1423
|
var amountRawUsdc = process.argv[2];
|
|
1284
1424
|
if (amountRawUsdc === void 0 || !/^[1-9]\d*$/.test(amountRawUsdc)) {
|
|
1285
1425
|
fail(
|
|
1286
|
-
"Usage:
|
|
1426
|
+
"Usage: pay withdraw <amountRawUsdc> [apr_<approvalId>] (positive integer, e.g. 1000000 = 1 USDC)"
|
|
1287
1427
|
);
|
|
1288
1428
|
}
|
|
1429
|
+
var approvalId = process.argv[3];
|
|
1430
|
+
if (approvalId !== void 0 && !/^apr_[0-9a-f]+$/i.test(approvalId)) {
|
|
1431
|
+
fail(`unrecognized argument: ${approvalId} (expected apr_<approvalId>)`);
|
|
1432
|
+
}
|
|
1289
1433
|
var keyPairSigner = await loadKeyPairSigner({
|
|
1290
1434
|
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1291
1435
|
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
@@ -1309,10 +1453,30 @@ console.log(
|
|
|
1309
1453
|
var submitted;
|
|
1310
1454
|
try {
|
|
1311
1455
|
submitted = await vaultFlows.withdraw({
|
|
1312
|
-
amountRawUsdc: BigInt(amountRawUsdc)
|
|
1456
|
+
amountRawUsdc: BigInt(amountRawUsdc),
|
|
1457
|
+
...approvalId === void 0 ? {} : { approvalId }
|
|
1313
1458
|
});
|
|
1314
1459
|
} catch (error) {
|
|
1315
1460
|
if (error instanceof VaultFlowClientError) {
|
|
1461
|
+
if (error.code === "withdrawal_approval_required") {
|
|
1462
|
+
const details = error.errorDetails ?? {};
|
|
1463
|
+
process.stdout.write(
|
|
1464
|
+
`${JSON.stringify(
|
|
1465
|
+
{
|
|
1466
|
+
ok: false,
|
|
1467
|
+
approvalRequired: true,
|
|
1468
|
+
approvalId: details.approvalId ?? null,
|
|
1469
|
+
approveUrl: details.approveUrl ?? null,
|
|
1470
|
+
expiresAtMs: details.expiresAtMs ?? null,
|
|
1471
|
+
message: `This withdrawal needs the owner's approval. Paste approveUrl to the user; once they approve, retry: pay withdraw ${amountRawUsdc} ${details.approvalId ?? "<approvalId>"}`
|
|
1472
|
+
},
|
|
1473
|
+
null,
|
|
1474
|
+
2
|
|
1475
|
+
)}
|
|
1476
|
+
`
|
|
1477
|
+
);
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
}
|
|
1316
1480
|
fail(`[withdraw] ${error.step} failed: ${error.message}`);
|
|
1317
1481
|
}
|
|
1318
1482
|
throw error;
|
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.6.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": {
|