@subly_fi/pay 0.3.0 → 0.4.1
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 +14 -7
- package/dist/deposit.js +245 -54
- package/dist/mcp-server.js +813 -212
- package/dist/pay.js +548 -130
- package/dist/withdraw.js +280 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,10 @@ locally with your own Solana key; Subly never holds it.
|
|
|
7
7
|
|
|
8
8
|
Ships one `pay` dispatcher bin with subcommands, all runnable with `npx` (no clone):
|
|
9
9
|
|
|
10
|
-
- `pay mcp` — an MCP server (Claude Code, Cursor, any MCP client)
|
|
10
|
+
- `pay mcp` — an MCP server (Claude Code, Cursor, any MCP client) exposing
|
|
11
|
+
the full lifecycle as tools: `deposit_to_subly_vault`,
|
|
12
|
+
`get_subly_yield_budget`, `fetch_with_subly_payment`,
|
|
13
|
+
`withdraw_from_subly_vault`
|
|
11
14
|
- `pay fetch <url>` — one-shot: pay for a URL, print the receipt (used by the
|
|
12
15
|
OpenClaw skill)
|
|
13
16
|
- `pay deposit <amountRawUsdc>` / `pay withdraw <amountRawUsdc>` — vault
|
|
@@ -23,11 +26,11 @@ export SUBLY_DEMO_AGENT_KEYPAIR_PATH=~/.subly/agent.json
|
|
|
23
26
|
```
|
|
24
27
|
|
|
25
28
|
Send USDC (Solana mainnet) to the printed address — no SOL needed, fees are
|
|
26
|
-
sponsored — then deposit (vault minimum 1 USDC
|
|
27
|
-
wallet):
|
|
29
|
+
sponsored — then deposit (vault minimum is just over 1 USDC: share rounding
|
|
30
|
+
refuses exactly 1.000000; deposit self-registers the wallet):
|
|
28
31
|
|
|
29
32
|
```bash
|
|
30
|
-
npx -y @subly_fi/pay deposit
|
|
33
|
+
npx -y @subly_fi/pay deposit 1010000 # 1.01 USDC
|
|
31
34
|
```
|
|
32
35
|
|
|
33
36
|
## Use it
|
|
@@ -45,10 +48,14 @@ npx -y @subly_fi/pay fetch https://seller.example.com/api/premium
|
|
|
45
48
|
| Var | Required | Default |
|
|
46
49
|
|---|---|---|
|
|
47
50
|
| `SUBLY_DEMO_AGENT_KEYPAIR_PATH` | yes (or `SUBLY_DEMO_AGENT_KEYPAIR` base58) | — |
|
|
48
|
-
| `
|
|
51
|
+
| `SUBLY_RELAYER_URL` | no | `https://api.demo.sublyfi.com` |
|
|
49
52
|
| `SOLANA_RPC_URL` | no | public mainnet RPC |
|
|
50
53
|
| `SUBLY_MCP_MAX_AMOUNT_RAW_USDC` | no | `10000` (0.01 USDC) per-payment cap |
|
|
51
54
|
|
|
52
55
|
Requests authenticate with a signature from your wallet key — there is no API
|
|
53
|
-
token.
|
|
54
|
-
|
|
56
|
+
token. `SUBLY_FACILITATOR_URL` is still accepted as a legacy fallback for
|
|
57
|
+
`SUBLY_RELAYER_URL`. Spending is bounded twice: the client cap, and the
|
|
58
|
+
relayer's server-side guard that refuses to realize anything beyond the
|
|
59
|
+
spendable yield — the deposited principal is never touched by a payment.
|
|
60
|
+
(A plain `pay withdraw` is the exit path and may of course move principal
|
|
61
|
+
back to your wallet.)
|
package/dist/deposit.js
CHANGED
|
@@ -48,10 +48,10 @@ function decodeSerializedTransaction(serializedBase64) {
|
|
|
48
48
|
}
|
|
49
49
|
async function addSignaturesToSerializedTransaction(params) {
|
|
50
50
|
const decoded = decodeSerializedTransaction(params.serializedBase64);
|
|
51
|
-
const
|
|
51
|
+
const signed = await partiallySignTransaction(params.signers, decoded);
|
|
52
52
|
return {
|
|
53
|
-
serializedBase64: getBase64EncodedWireTransaction(
|
|
54
|
-
transaction:
|
|
53
|
+
serializedBase64: getBase64EncodedWireTransaction(signed),
|
|
54
|
+
transaction: signed
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
function signatureBase58ForSigner(transaction, signer2) {
|
|
@@ -993,7 +993,7 @@ var OnboardingError = class extends Error {
|
|
|
993
993
|
};
|
|
994
994
|
async function ensureWalletOnboarded(params) {
|
|
995
995
|
const fetchImpl = params.fetchImpl ?? fetch;
|
|
996
|
-
const baseUrl = params.
|
|
996
|
+
const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
|
|
997
997
|
const post = async (step, path, body) => {
|
|
998
998
|
const url = `${baseUrl}${path}`;
|
|
999
999
|
const serialized = JSON.stringify(body);
|
|
@@ -1076,6 +1076,222 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
|
1076
1076
|
return result;
|
|
1077
1077
|
}
|
|
1078
1078
|
|
|
1079
|
+
// ../../src/client/vault-flows.ts
|
|
1080
|
+
var VaultFlowClientError = class extends Error {
|
|
1081
|
+
constructor(step, message, detail = null) {
|
|
1082
|
+
super(message);
|
|
1083
|
+
this.step = step;
|
|
1084
|
+
this.detail = detail;
|
|
1085
|
+
this.name = "VaultFlowClientError";
|
|
1086
|
+
}
|
|
1087
|
+
step;
|
|
1088
|
+
detail;
|
|
1089
|
+
};
|
|
1090
|
+
var VaultFlowClient = class {
|
|
1091
|
+
baseUrl;
|
|
1092
|
+
signer;
|
|
1093
|
+
fetchImpl;
|
|
1094
|
+
lookupTablesFor;
|
|
1095
|
+
pollTimeoutMs;
|
|
1096
|
+
pollIntervalMs;
|
|
1097
|
+
constructor(config) {
|
|
1098
|
+
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
1099
|
+
this.signer = config.signer;
|
|
1100
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1101
|
+
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
|
|
1102
|
+
this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
|
|
1103
|
+
this.pollIntervalMs = config.pollIntervalMs ?? 2500;
|
|
1104
|
+
}
|
|
1105
|
+
/** Moves USDC from the agent wallet into the vault (fee sponsored). */
|
|
1106
|
+
async deposit(input) {
|
|
1107
|
+
const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1108
|
+
wallet: this.signer.walletAddress,
|
|
1109
|
+
amountRawUsdc: input.amountRawUsdc.toString()
|
|
1110
|
+
});
|
|
1111
|
+
const signed = await this.signer.signDeposit({
|
|
1112
|
+
intent: prepared.signingIntent,
|
|
1113
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1114
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1115
|
+
});
|
|
1116
|
+
let outcome = await this.postJson("submit", "/v1/deposits/submit", {
|
|
1117
|
+
depositId: prepared.depositId,
|
|
1118
|
+
serializedTransaction: signed.serializedTransaction,
|
|
1119
|
+
agentSignature: signed.agentSignature
|
|
1120
|
+
});
|
|
1121
|
+
if (outcome.status === "submitted") {
|
|
1122
|
+
outcome = await this.pollUntilTerminal(
|
|
1123
|
+
`/v1/deposits/${prepared.depositId}`,
|
|
1124
|
+
outcome
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
depositId: prepared.depositId,
|
|
1129
|
+
status: outcome.status,
|
|
1130
|
+
txSignature: outcome.txSignature ?? null,
|
|
1131
|
+
actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
|
|
1132
|
+
sharesMintedRaw: outcome.sharesMintedRaw ?? null,
|
|
1133
|
+
errorCode: outcome.errorCode ?? null
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Moves USDC from the vault back to the agent wallet's USDC ATA (fee
|
|
1138
|
+
* sponsored). A plain withdrawal is the exit path and MAY spend principal;
|
|
1139
|
+
* with purpose "yield_realize" the relayer refuses anything beyond the
|
|
1140
|
+
* spendable yield (the payment path, via RelayerYieldRealizer).
|
|
1141
|
+
*/
|
|
1142
|
+
async withdraw(input) {
|
|
1143
|
+
const prepared = await this.postJson(
|
|
1144
|
+
"prepare",
|
|
1145
|
+
"/v1/withdrawals/prepare",
|
|
1146
|
+
{
|
|
1147
|
+
wallet: this.signer.walletAddress,
|
|
1148
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1149
|
+
...input.purpose === void 0 ? {} : { purpose: input.purpose }
|
|
1150
|
+
}
|
|
1151
|
+
);
|
|
1152
|
+
const signed = await this.signer.signWithdrawal({
|
|
1153
|
+
intent: prepared.signingIntent,
|
|
1154
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1155
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1156
|
+
});
|
|
1157
|
+
let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
|
|
1158
|
+
withdrawalId: prepared.withdrawalId,
|
|
1159
|
+
serializedTransaction: signed.serializedTransaction,
|
|
1160
|
+
agentSignature: signed.agentSignature
|
|
1161
|
+
});
|
|
1162
|
+
if (outcome.status === "submitted") {
|
|
1163
|
+
outcome = await this.pollUntilTerminal(
|
|
1164
|
+
`/v1/withdrawals/${prepared.withdrawalId}`,
|
|
1165
|
+
outcome
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
return {
|
|
1169
|
+
withdrawalId: prepared.withdrawalId,
|
|
1170
|
+
status: outcome.status,
|
|
1171
|
+
txSignature: outcome.txSignature ?? null,
|
|
1172
|
+
destinationUsdcAta: prepared.destinationUsdcAta,
|
|
1173
|
+
actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
|
|
1174
|
+
actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
|
|
1175
|
+
errorCode: outcome.errorCode ?? null
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Reads the yield budget. Syncs the relayer's ledger from chain first (so
|
|
1180
|
+
* yield accrued since the last sync shows up); the sync is best-effort and
|
|
1181
|
+
* on failure the last-synced view is returned.
|
|
1182
|
+
*/
|
|
1183
|
+
async getBudget(options = {}) {
|
|
1184
|
+
if (options.refreshFromChain !== false) {
|
|
1185
|
+
try {
|
|
1186
|
+
await this.postJson(
|
|
1187
|
+
"sync",
|
|
1188
|
+
`/v1/wallets/${this.signer.walletAddress}/sync`,
|
|
1189
|
+
{ source: "chain" }
|
|
1190
|
+
);
|
|
1191
|
+
} catch {
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
1195
|
+
const response = await this.fetchImpl(url, {
|
|
1196
|
+
headers: await walletAuthHeaders({
|
|
1197
|
+
signer: this.signer,
|
|
1198
|
+
method: "GET",
|
|
1199
|
+
url
|
|
1200
|
+
})
|
|
1201
|
+
});
|
|
1202
|
+
const text = await response.text();
|
|
1203
|
+
if (response.status !== 200) {
|
|
1204
|
+
throw new VaultFlowClientError(
|
|
1205
|
+
"budget",
|
|
1206
|
+
`budget endpoint returned ${response.status}: ${text}`
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
let parsed;
|
|
1210
|
+
try {
|
|
1211
|
+
parsed = JSON.parse(text);
|
|
1212
|
+
} catch {
|
|
1213
|
+
throw new VaultFlowClientError(
|
|
1214
|
+
"budget",
|
|
1215
|
+
"budget endpoint returned 200 with a non-JSON body",
|
|
1216
|
+
text
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
const body = parsed;
|
|
1220
|
+
return {
|
|
1221
|
+
wallet: this.signer.walletAddress,
|
|
1222
|
+
principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
|
|
1223
|
+
positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
|
|
1224
|
+
grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
|
|
1225
|
+
spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Polls the reconciling GET endpoint until the intent leaves "submitted"
|
|
1230
|
+
* (each read looks the tx up on-chain) or the timeout elapses.
|
|
1231
|
+
*/
|
|
1232
|
+
async pollUntilTerminal(path, last) {
|
|
1233
|
+
const deadline = Date.now() + this.pollTimeoutMs;
|
|
1234
|
+
let latest = last;
|
|
1235
|
+
while (Date.now() < deadline) {
|
|
1236
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
1237
|
+
const url = `${this.baseUrl}${path}`;
|
|
1238
|
+
const response = await this.fetchImpl(url, {
|
|
1239
|
+
headers: await walletAuthHeaders({
|
|
1240
|
+
signer: this.signer,
|
|
1241
|
+
method: "GET",
|
|
1242
|
+
url
|
|
1243
|
+
})
|
|
1244
|
+
});
|
|
1245
|
+
if (response.status !== 200) {
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
try {
|
|
1249
|
+
latest = await response.json();
|
|
1250
|
+
} catch {
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (latest.status !== "submitted") {
|
|
1254
|
+
return latest;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return latest;
|
|
1258
|
+
}
|
|
1259
|
+
async postJson(step, path, body) {
|
|
1260
|
+
const url = `${this.baseUrl}${path}`;
|
|
1261
|
+
const serialized = JSON.stringify(body);
|
|
1262
|
+
const response = await this.fetchImpl(url, {
|
|
1263
|
+
method: "POST",
|
|
1264
|
+
headers: {
|
|
1265
|
+
...await walletAuthHeaders({
|
|
1266
|
+
signer: this.signer,
|
|
1267
|
+
method: "POST",
|
|
1268
|
+
url,
|
|
1269
|
+
body: serialized
|
|
1270
|
+
}),
|
|
1271
|
+
"content-type": "application/json"
|
|
1272
|
+
},
|
|
1273
|
+
body: serialized
|
|
1274
|
+
});
|
|
1275
|
+
const text = await response.text();
|
|
1276
|
+
if (response.status !== 200) {
|
|
1277
|
+
throw new VaultFlowClientError(
|
|
1278
|
+
step,
|
|
1279
|
+
`${path} failed with ${response.status}: ${text}`,
|
|
1280
|
+
text
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
try {
|
|
1284
|
+
return JSON.parse(text);
|
|
1285
|
+
} catch {
|
|
1286
|
+
throw new VaultFlowClientError(
|
|
1287
|
+
step,
|
|
1288
|
+
`${path} returned 200 with a non-JSON body`,
|
|
1289
|
+
text
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1079
1295
|
// ../../src/solana/keys.ts
|
|
1080
1296
|
import { readFileSync } from "node:fs";
|
|
1081
1297
|
import bs586 from "bs58";
|
|
@@ -1120,7 +1336,7 @@ function formatRawUsdc(raw) {
|
|
|
1120
1336
|
}
|
|
1121
1337
|
|
|
1122
1338
|
// ../../demo/deposit.ts
|
|
1123
|
-
var
|
|
1339
|
+
var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
|
|
1124
1340
|
var amountRawUsdc = process.argv[2];
|
|
1125
1341
|
if (amountRawUsdc === void 0 || !/^[1-9]\d*$/.test(amountRawUsdc)) {
|
|
1126
1342
|
fail(
|
|
@@ -1136,69 +1352,44 @@ var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
|
1136
1352
|
var rpc = createRpc(
|
|
1137
1353
|
process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com"
|
|
1138
1354
|
);
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
headers: {
|
|
1145
|
-
...await walletAuthHeaders({
|
|
1146
|
-
signer,
|
|
1147
|
-
method: "POST",
|
|
1148
|
-
url,
|
|
1149
|
-
body: serialized
|
|
1150
|
-
}),
|
|
1151
|
-
"content-type": "application/json"
|
|
1152
|
-
},
|
|
1153
|
-
body: serialized
|
|
1154
|
-
});
|
|
1155
|
-
const text = await response.text();
|
|
1156
|
-
if (response.status !== 200) {
|
|
1157
|
-
fail(`[deposit] ${path} failed with ${response.status}: ${text}`);
|
|
1158
|
-
}
|
|
1159
|
-
return JSON.parse(text);
|
|
1160
|
-
}
|
|
1355
|
+
var vaultFlows = new VaultFlowClient({
|
|
1356
|
+
relayerBaseUrl,
|
|
1357
|
+
signer,
|
|
1358
|
+
rpc
|
|
1359
|
+
});
|
|
1161
1360
|
console.log(`[deposit] agent wallet: ${signer.walletAddress}`);
|
|
1162
|
-
console.log(`[deposit]
|
|
1361
|
+
console.log(`[deposit] relayer: ${relayerBaseUrl}`);
|
|
1163
1362
|
console.log("\n[deposit] step 0: ensure the wallet is registered (self-serve)");
|
|
1164
|
-
await ensureWalletOnboarded({
|
|
1363
|
+
await ensureWalletOnboarded({ relayerBaseUrl, signer });
|
|
1165
1364
|
console.log("[deposit] wallet registered and synced");
|
|
1166
1365
|
console.log(
|
|
1167
1366
|
`
|
|
1168
|
-
[deposit] step 1:
|
|
1367
|
+
[deposit] step 1: deposit ${formatRawUsdc(amountRawUsdc)} USDC (prepare -> validate intent + sign locally -> sponsor submits)`
|
|
1169
1368
|
);
|
|
1170
|
-
var
|
|
1171
|
-
|
|
1172
|
-
amountRawUsdc
|
|
1173
|
-
})
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
serializedTransaction: prepared.serializedTransaction,
|
|
1181
|
-
lookupTables: await fetchLookupTablesForTransaction(
|
|
1182
|
-
rpc,
|
|
1183
|
-
prepared.serializedTransaction
|
|
1184
|
-
)
|
|
1185
|
-
});
|
|
1186
|
-
console.log("[deposit] agent signature attached");
|
|
1187
|
-
console.log("\n[deposit] step 3: submit (sponsor co-signs and broadcasts)");
|
|
1188
|
-
var submitted = await postJson("/v1/deposits/submit", {
|
|
1189
|
-
depositId: prepared.depositId,
|
|
1190
|
-
serializedTransaction: signed.serializedTransaction,
|
|
1191
|
-
agentSignature: signed.agentSignature
|
|
1192
|
-
});
|
|
1369
|
+
var submitted;
|
|
1370
|
+
try {
|
|
1371
|
+
submitted = await vaultFlows.deposit({ amountRawUsdc: BigInt(amountRawUsdc) });
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
if (error instanceof VaultFlowClientError) {
|
|
1374
|
+
fail(`[deposit] ${error.step} failed: ${error.message}`);
|
|
1375
|
+
}
|
|
1376
|
+
throw error;
|
|
1377
|
+
}
|
|
1378
|
+
console.log(`[deposit] depositId: ${submitted.depositId}`);
|
|
1193
1379
|
console.log(`[deposit] status: ${submitted.status}`);
|
|
1194
1380
|
if (submitted.txSignature !== null) {
|
|
1195
1381
|
console.log(
|
|
1196
1382
|
`[deposit] transaction: https://solscan.io/tx/${submitted.txSignature}`
|
|
1197
1383
|
);
|
|
1198
1384
|
}
|
|
1385
|
+
if (submitted.status === "submitted") {
|
|
1386
|
+
fail(
|
|
1387
|
+
`[deposit] broadcast but not yet confirmed \u2014 it may still land. Do NOT resubmit; check GET /v1/deposits/${submitted.depositId} (or the tx link above) first`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1199
1390
|
if (submitted.status !== "confirmed") {
|
|
1200
1391
|
fail(
|
|
1201
|
-
`[deposit] not confirmed (errorCode=${submitted.errorCode}); check GET /v1/deposits/${
|
|
1392
|
+
`[deposit] not confirmed (errorCode=${submitted.errorCode}); check GET /v1/deposits/${submitted.depositId} and the relayer logs`
|
|
1202
1393
|
);
|
|
1203
1394
|
}
|
|
1204
1395
|
console.log(
|