@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/dist/mcp-server.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// src/mcp-server.ts
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join as join2 } from "node:path";
|
|
4
|
+
|
|
1
5
|
// ../../src/client/agent-wallet-signer.ts
|
|
2
6
|
import { signBytes } from "@solana/kit";
|
|
3
7
|
import bs584 from "bs58";
|
|
@@ -19,6 +23,7 @@ import {
|
|
|
19
23
|
|
|
20
24
|
// ../../src/lib/hash.ts
|
|
21
25
|
import { createHash } from "node:crypto";
|
|
26
|
+
var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
22
27
|
function sha256TaggedHex(data) {
|
|
23
28
|
return `sha256-${createHash("sha256").update(data).digest("hex")}`;
|
|
24
29
|
}
|
|
@@ -115,12 +120,12 @@ function deriveAssociatedTokenAddress(params) {
|
|
|
115
120
|
"associatedTokenProgramId"
|
|
116
121
|
);
|
|
117
122
|
for (let bump = 255; bump >= 0; bump -= 1) {
|
|
118
|
-
const
|
|
123
|
+
const address2 = createProgramAddress(
|
|
119
124
|
[owner, tokenProgramId, mint, Uint8Array.of(bump)],
|
|
120
125
|
associatedTokenProgramId
|
|
121
126
|
);
|
|
122
|
-
if (
|
|
123
|
-
return bs582.encode(
|
|
127
|
+
if (address2 !== null) {
|
|
128
|
+
return bs582.encode(address2);
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
throw new Error("Unable to derive associated token account address");
|
|
@@ -1001,7 +1006,7 @@ var OnboardingError = class extends Error {
|
|
|
1001
1006
|
};
|
|
1002
1007
|
async function ensureWalletOnboarded(params) {
|
|
1003
1008
|
const fetchImpl = params.fetchImpl ?? fetch;
|
|
1004
|
-
const baseUrl = params.
|
|
1009
|
+
const baseUrl = params.relayerBaseUrl.replace(/\/$/, "");
|
|
1005
1010
|
const post = async (step, path, body) => {
|
|
1006
1011
|
const url = `${baseUrl}${path}`;
|
|
1007
1012
|
const serialized = JSON.stringify(body);
|
|
@@ -1110,6 +1115,14 @@ function decodeX402Header(headerValue) {
|
|
|
1110
1115
|
);
|
|
1111
1116
|
}
|
|
1112
1117
|
}
|
|
1118
|
+
function requestBodyHashFor(body) {
|
|
1119
|
+
if (body === null || body === void 0 || body.length === 0) {
|
|
1120
|
+
return EMPTY_BODY_HASH;
|
|
1121
|
+
}
|
|
1122
|
+
return sha256TaggedHex(
|
|
1123
|
+
typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body)
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1113
1126
|
|
|
1114
1127
|
// ../../src/client/paid-fetch.ts
|
|
1115
1128
|
function formatRawUsdcAmount(raw) {
|
|
@@ -1202,6 +1215,24 @@ function selectPayableSolanaRequirement(requirements, options) {
|
|
|
1202
1215
|
feePayer: requirement.extra?.feePayer ?? null
|
|
1203
1216
|
};
|
|
1204
1217
|
}
|
|
1218
|
+
function standardRequirementMatchesSelected(candidate, selected) {
|
|
1219
|
+
const parsed = standardExactRequirementSchema.safeParse(candidate);
|
|
1220
|
+
return parsed.success && stableJson(parsed.data) === stableJson(selected.requirement);
|
|
1221
|
+
}
|
|
1222
|
+
function stableJson(value) {
|
|
1223
|
+
return JSON.stringify(sortJson(value));
|
|
1224
|
+
}
|
|
1225
|
+
function sortJson(value) {
|
|
1226
|
+
if (Array.isArray(value)) {
|
|
1227
|
+
return value.map(sortJson);
|
|
1228
|
+
}
|
|
1229
|
+
if (value !== null && typeof value === "object") {
|
|
1230
|
+
return Object.fromEntries(
|
|
1231
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
return value;
|
|
1235
|
+
}
|
|
1205
1236
|
|
|
1206
1237
|
// ../../src/client/standard-x402-payer.ts
|
|
1207
1238
|
var StandardX402PayError = class extends Error {
|
|
@@ -1221,6 +1252,10 @@ var StandardX402Payer = class {
|
|
|
1221
1252
|
defaultMaxAmountRawUsdc;
|
|
1222
1253
|
network;
|
|
1223
1254
|
usdcMint;
|
|
1255
|
+
stateStore;
|
|
1256
|
+
pending = /* @__PURE__ */ new Map();
|
|
1257
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
1258
|
+
nowMs;
|
|
1224
1259
|
constructor(config) {
|
|
1225
1260
|
this.realizer = config.realizer;
|
|
1226
1261
|
this.x402Fetch = config.x402Fetch;
|
|
@@ -1228,10 +1263,59 @@ var StandardX402Payer = class {
|
|
|
1228
1263
|
this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
|
|
1229
1264
|
this.network = config.network ?? SOLANA_MAINNET_NETWORK;
|
|
1230
1265
|
this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1266
|
+
this.stateStore = config.stateStore ?? null;
|
|
1267
|
+
this.nowMs = config.nowMs ?? (() => Date.now());
|
|
1268
|
+
if (this.stateStore !== null) {
|
|
1269
|
+
for (const record of this.stateStore.load()) {
|
|
1270
|
+
this.pending.set(record.key, record);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1231
1273
|
}
|
|
1232
|
-
|
|
1274
|
+
pay(input) {
|
|
1275
|
+
const method = (input.method ?? "GET").toUpperCase();
|
|
1276
|
+
const requestBodyHash = requestBodyHashFor(input.body ?? null);
|
|
1277
|
+
const pendingKey = pendingPaymentKey({
|
|
1278
|
+
url: input.url,
|
|
1279
|
+
method,
|
|
1280
|
+
requestBodyHash
|
|
1281
|
+
});
|
|
1282
|
+
const existingFlow = this.inFlight.get(pendingKey);
|
|
1283
|
+
if (existingFlow !== void 0) {
|
|
1284
|
+
return existingFlow;
|
|
1285
|
+
}
|
|
1286
|
+
const flow = this.run(input, {
|
|
1287
|
+
method,
|
|
1288
|
+
requestBodyHash,
|
|
1289
|
+
pendingKey
|
|
1290
|
+
}).finally(() => {
|
|
1291
|
+
this.inFlight.delete(pendingKey);
|
|
1292
|
+
});
|
|
1293
|
+
this.inFlight.set(pendingKey, flow);
|
|
1294
|
+
return flow;
|
|
1295
|
+
}
|
|
1296
|
+
async run(input, computed) {
|
|
1297
|
+
const { method, requestBodyHash, pendingKey } = computed;
|
|
1298
|
+
const existingPending = this.pending.get(pendingKey);
|
|
1299
|
+
if (existingPending !== void 0 && input.forceNewPayment !== true) {
|
|
1300
|
+
throw new StandardX402PayError(
|
|
1301
|
+
"payment_outcome_unknown",
|
|
1302
|
+
"a previous external x402 payment for this request has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call with forceNewPayment=true.",
|
|
1303
|
+
existingPending
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
if (existingPending !== void 0 && input.forceNewPayment === true) {
|
|
1307
|
+
try {
|
|
1308
|
+
this.untrack(pendingKey);
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
throw new StandardX402PayError(
|
|
1311
|
+
"state_persist_failed",
|
|
1312
|
+
"could not clear the previous pending x402 marker before forcing a new payment",
|
|
1313
|
+
error
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1233
1317
|
const init = {
|
|
1234
|
-
method
|
|
1318
|
+
method,
|
|
1235
1319
|
...input.body === void 0 ? {} : { body: input.body },
|
|
1236
1320
|
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
1237
1321
|
};
|
|
@@ -1260,15 +1344,55 @@ var StandardX402Payer = class {
|
|
|
1260
1344
|
error
|
|
1261
1345
|
);
|
|
1262
1346
|
}
|
|
1263
|
-
const
|
|
1347
|
+
const pendingRecord = {
|
|
1348
|
+
key: pendingKey,
|
|
1349
|
+
url: input.url,
|
|
1350
|
+
method,
|
|
1351
|
+
requestBodyHash,
|
|
1352
|
+
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1353
|
+
payTo: selected.payTo,
|
|
1354
|
+
feePayer: selected.feePayer,
|
|
1355
|
+
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1356
|
+
realizeTxSignature: realized.txSignature,
|
|
1357
|
+
status: "realized",
|
|
1358
|
+
createdAtMs: this.nowMs(),
|
|
1359
|
+
updatedAtMs: this.nowMs()
|
|
1360
|
+
};
|
|
1361
|
+
try {
|
|
1362
|
+
this.track(pendingRecord);
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
throw new StandardX402PayError(
|
|
1365
|
+
"state_persist_failed",
|
|
1366
|
+
"could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
|
|
1367
|
+
{ error, pendingPayment: pendingRecord }
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
let response;
|
|
1371
|
+
try {
|
|
1372
|
+
response = await this.x402Fetch(input.url, init, selected);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1375
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1376
|
+
});
|
|
1377
|
+
throw new StandardX402PayError(
|
|
1378
|
+
"payment_outcome_unknown",
|
|
1379
|
+
`the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
|
|
1380
|
+
{ error, persistError }
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1264
1383
|
const bodyText = await response.text();
|
|
1265
1384
|
if (response.status !== 200) {
|
|
1385
|
+
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1386
|
+
status: response.status,
|
|
1387
|
+
body: bodyText
|
|
1388
|
+
});
|
|
1266
1389
|
throw new StandardX402PayError(
|
|
1267
|
-
"
|
|
1268
|
-
`the x402 payment
|
|
1269
|
-
{ status: response.status, body: bodyText }
|
|
1390
|
+
"payment_outcome_unknown",
|
|
1391
|
+
`the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
|
|
1392
|
+
{ status: response.status, body: bodyText, persistError }
|
|
1270
1393
|
);
|
|
1271
1394
|
}
|
|
1395
|
+
this.clearDelivered(pendingKey);
|
|
1272
1396
|
return {
|
|
1273
1397
|
paid: true,
|
|
1274
1398
|
status: response.status,
|
|
@@ -1314,26 +1438,421 @@ var StandardX402Payer = class {
|
|
|
1314
1438
|
);
|
|
1315
1439
|
}
|
|
1316
1440
|
}
|
|
1441
|
+
track(record) {
|
|
1442
|
+
const previous = this.pending.get(record.key);
|
|
1443
|
+
this.pending.set(record.key, record);
|
|
1444
|
+
try {
|
|
1445
|
+
this.persist();
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
if (previous === void 0) {
|
|
1448
|
+
this.pending.delete(record.key);
|
|
1449
|
+
} else {
|
|
1450
|
+
this.pending.set(record.key, previous);
|
|
1451
|
+
}
|
|
1452
|
+
throw error;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
markUnknown(key, detail) {
|
|
1456
|
+
const current = this.pending.get(key);
|
|
1457
|
+
if (current === void 0) {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
const next = {
|
|
1461
|
+
...current,
|
|
1462
|
+
status: "external_outcome_unknown",
|
|
1463
|
+
updatedAtMs: this.nowMs(),
|
|
1464
|
+
detail
|
|
1465
|
+
};
|
|
1466
|
+
this.pending.set(key, next);
|
|
1467
|
+
try {
|
|
1468
|
+
this.persist();
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
this.pending.set(key, current);
|
|
1471
|
+
throw error;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
tryMarkUnknown(key, detail) {
|
|
1475
|
+
try {
|
|
1476
|
+
this.markUnknown(key, detail);
|
|
1477
|
+
return null;
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
return error;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
untrack(key) {
|
|
1483
|
+
const previous = this.pending.get(key);
|
|
1484
|
+
const existed = previous !== void 0;
|
|
1485
|
+
this.pending.delete(key);
|
|
1486
|
+
try {
|
|
1487
|
+
this.persist();
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
if (existed) {
|
|
1490
|
+
this.pending.set(key, previous);
|
|
1491
|
+
}
|
|
1492
|
+
throw error;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
clearDelivered(key) {
|
|
1496
|
+
const previous = this.pending.get(key);
|
|
1497
|
+
this.pending.delete(key);
|
|
1498
|
+
try {
|
|
1499
|
+
this.persist();
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
if (previous !== void 0) {
|
|
1502
|
+
this.pending.set(key, previous);
|
|
1503
|
+
}
|
|
1504
|
+
console.error(
|
|
1505
|
+
`[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
persist() {
|
|
1510
|
+
if (this.stateStore === null) {
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
this.stateStore.save([...this.pending.values()]);
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
function pendingPaymentKey(input) {
|
|
1517
|
+
return `${input.method}:${input.url}:${input.requestBodyHash}`;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// ../../src/client/lookup-tables.ts
|
|
1521
|
+
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
1522
|
+
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
1523
|
+
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
1524
|
+
const wire = Buffer.from(serializedTransaction, "base64");
|
|
1525
|
+
let offset = 0;
|
|
1526
|
+
let signatureCount = 0;
|
|
1527
|
+
let shift = 0;
|
|
1528
|
+
while (offset < wire.length) {
|
|
1529
|
+
const byte = wire[offset];
|
|
1530
|
+
signatureCount |= (byte & 127) << shift;
|
|
1531
|
+
offset += 1;
|
|
1532
|
+
if ((byte & 128) === 0) {
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
shift += 7;
|
|
1536
|
+
}
|
|
1537
|
+
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
1538
|
+
const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
|
|
1539
|
+
const lookups = compiled.addressTableLookups ?? [];
|
|
1540
|
+
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
1541
|
+
}
|
|
1542
|
+
async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
1543
|
+
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
1544
|
+
if (addresses.length === 0) {
|
|
1545
|
+
return {};
|
|
1546
|
+
}
|
|
1547
|
+
const tables = await fetchAllMaybeAddressLookupTable(
|
|
1548
|
+
rpc2,
|
|
1549
|
+
addresses.map((value) => address(value))
|
|
1550
|
+
);
|
|
1551
|
+
const result = {};
|
|
1552
|
+
for (const table of tables) {
|
|
1553
|
+
if (table.exists) {
|
|
1554
|
+
result[table.address] = table.data.addresses.map(String);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return result;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// ../../src/client/vault-flows.ts
|
|
1561
|
+
var VaultFlowClientError = class extends Error {
|
|
1562
|
+
constructor(step, message, detail = null) {
|
|
1563
|
+
super(message);
|
|
1564
|
+
this.step = step;
|
|
1565
|
+
this.detail = detail;
|
|
1566
|
+
this.name = "VaultFlowClientError";
|
|
1567
|
+
}
|
|
1568
|
+
step;
|
|
1569
|
+
detail;
|
|
1570
|
+
};
|
|
1571
|
+
var VaultFlowClient = class {
|
|
1572
|
+
baseUrl;
|
|
1573
|
+
signer;
|
|
1574
|
+
fetchImpl;
|
|
1575
|
+
lookupTablesFor;
|
|
1576
|
+
pollTimeoutMs;
|
|
1577
|
+
pollIntervalMs;
|
|
1578
|
+
constructor(config) {
|
|
1579
|
+
this.baseUrl = config.relayerBaseUrl.replace(/\/$/, "");
|
|
1580
|
+
this.signer = config.signer;
|
|
1581
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1582
|
+
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
|
|
1583
|
+
this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
|
|
1584
|
+
this.pollIntervalMs = config.pollIntervalMs ?? 2500;
|
|
1585
|
+
}
|
|
1586
|
+
/** Moves USDC from the agent wallet into the vault (fee sponsored). */
|
|
1587
|
+
async deposit(input) {
|
|
1588
|
+
const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1589
|
+
wallet: this.signer.walletAddress,
|
|
1590
|
+
amountRawUsdc: input.amountRawUsdc.toString()
|
|
1591
|
+
});
|
|
1592
|
+
const signed = await this.signer.signDeposit({
|
|
1593
|
+
intent: prepared.signingIntent,
|
|
1594
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1595
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1596
|
+
});
|
|
1597
|
+
let outcome = await this.postJson("submit", "/v1/deposits/submit", {
|
|
1598
|
+
depositId: prepared.depositId,
|
|
1599
|
+
serializedTransaction: signed.serializedTransaction,
|
|
1600
|
+
agentSignature: signed.agentSignature
|
|
1601
|
+
});
|
|
1602
|
+
if (outcome.status === "submitted") {
|
|
1603
|
+
outcome = await this.pollUntilTerminal(
|
|
1604
|
+
`/v1/deposits/${prepared.depositId}`,
|
|
1605
|
+
outcome
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
return {
|
|
1609
|
+
depositId: prepared.depositId,
|
|
1610
|
+
status: outcome.status,
|
|
1611
|
+
txSignature: outcome.txSignature ?? null,
|
|
1612
|
+
actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
|
|
1613
|
+
sharesMintedRaw: outcome.sharesMintedRaw ?? null,
|
|
1614
|
+
errorCode: outcome.errorCode ?? null
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Moves USDC from the vault back to the agent wallet's USDC ATA (fee
|
|
1619
|
+
* sponsored). A plain withdrawal is the exit path and MAY spend principal;
|
|
1620
|
+
* with purpose "yield_realize" the relayer refuses anything beyond the
|
|
1621
|
+
* spendable yield (the payment path, via RelayerYieldRealizer).
|
|
1622
|
+
*/
|
|
1623
|
+
async withdraw(input) {
|
|
1624
|
+
const prepared = await this.postJson(
|
|
1625
|
+
"prepare",
|
|
1626
|
+
"/v1/withdrawals/prepare",
|
|
1627
|
+
{
|
|
1628
|
+
wallet: this.signer.walletAddress,
|
|
1629
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1630
|
+
...input.purpose === void 0 ? {} : { purpose: input.purpose }
|
|
1631
|
+
}
|
|
1632
|
+
);
|
|
1633
|
+
const signed = await this.signer.signWithdrawal({
|
|
1634
|
+
intent: prepared.signingIntent,
|
|
1635
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1636
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1637
|
+
});
|
|
1638
|
+
let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
|
|
1639
|
+
withdrawalId: prepared.withdrawalId,
|
|
1640
|
+
serializedTransaction: signed.serializedTransaction,
|
|
1641
|
+
agentSignature: signed.agentSignature
|
|
1642
|
+
});
|
|
1643
|
+
if (outcome.status === "submitted") {
|
|
1644
|
+
outcome = await this.pollUntilTerminal(
|
|
1645
|
+
`/v1/withdrawals/${prepared.withdrawalId}`,
|
|
1646
|
+
outcome
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
return {
|
|
1650
|
+
withdrawalId: prepared.withdrawalId,
|
|
1651
|
+
status: outcome.status,
|
|
1652
|
+
txSignature: outcome.txSignature ?? null,
|
|
1653
|
+
destinationUsdcAta: prepared.destinationUsdcAta,
|
|
1654
|
+
actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
|
|
1655
|
+
actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
|
|
1656
|
+
errorCode: outcome.errorCode ?? null
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Reads the yield budget. Syncs the relayer's ledger from chain first (so
|
|
1661
|
+
* yield accrued since the last sync shows up); the sync is best-effort and
|
|
1662
|
+
* on failure the last-synced view is returned.
|
|
1663
|
+
*/
|
|
1664
|
+
async getBudget(options = {}) {
|
|
1665
|
+
if (options.refreshFromChain !== false) {
|
|
1666
|
+
try {
|
|
1667
|
+
await this.postJson(
|
|
1668
|
+
"sync",
|
|
1669
|
+
`/v1/wallets/${this.signer.walletAddress}/sync`,
|
|
1670
|
+
{ source: "chain" }
|
|
1671
|
+
);
|
|
1672
|
+
} catch {
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
const url = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
1676
|
+
const response = await this.fetchImpl(url, {
|
|
1677
|
+
headers: await walletAuthHeaders({
|
|
1678
|
+
signer: this.signer,
|
|
1679
|
+
method: "GET",
|
|
1680
|
+
url
|
|
1681
|
+
})
|
|
1682
|
+
});
|
|
1683
|
+
const text = await response.text();
|
|
1684
|
+
if (response.status !== 200) {
|
|
1685
|
+
throw new VaultFlowClientError(
|
|
1686
|
+
"budget",
|
|
1687
|
+
`budget endpoint returned ${response.status}: ${text}`
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
let parsed;
|
|
1691
|
+
try {
|
|
1692
|
+
parsed = JSON.parse(text);
|
|
1693
|
+
} catch {
|
|
1694
|
+
throw new VaultFlowClientError(
|
|
1695
|
+
"budget",
|
|
1696
|
+
"budget endpoint returned 200 with a non-JSON body",
|
|
1697
|
+
text
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
const body = parsed;
|
|
1701
|
+
return {
|
|
1702
|
+
wallet: this.signer.walletAddress,
|
|
1703
|
+
principalBasisRawUsdc: body.position?.principalBasisRawUsdc ?? "0",
|
|
1704
|
+
positionValueRawUsdc: body.budget?.positionValueRawUsdc ?? "0",
|
|
1705
|
+
grossYieldRawUsdc: body.budget?.grossYieldRawUsdc ?? "0",
|
|
1706
|
+
spendableYieldRawUsdc: body.budget?.spendableYieldRawUsdc ?? "0"
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Polls the reconciling GET endpoint until the intent leaves "submitted"
|
|
1711
|
+
* (each read looks the tx up on-chain) or the timeout elapses.
|
|
1712
|
+
*/
|
|
1713
|
+
async pollUntilTerminal(path, last) {
|
|
1714
|
+
const deadline = Date.now() + this.pollTimeoutMs;
|
|
1715
|
+
let latest = last;
|
|
1716
|
+
while (Date.now() < deadline) {
|
|
1717
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
1718
|
+
const url = `${this.baseUrl}${path}`;
|
|
1719
|
+
const response = await this.fetchImpl(url, {
|
|
1720
|
+
headers: await walletAuthHeaders({
|
|
1721
|
+
signer: this.signer,
|
|
1722
|
+
method: "GET",
|
|
1723
|
+
url
|
|
1724
|
+
})
|
|
1725
|
+
});
|
|
1726
|
+
if (response.status !== 200) {
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
try {
|
|
1730
|
+
latest = await response.json();
|
|
1731
|
+
} catch {
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
if (latest.status !== "submitted") {
|
|
1735
|
+
return latest;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return latest;
|
|
1739
|
+
}
|
|
1740
|
+
async postJson(step, path, body) {
|
|
1741
|
+
const url = `${this.baseUrl}${path}`;
|
|
1742
|
+
const serialized = JSON.stringify(body);
|
|
1743
|
+
const response = await this.fetchImpl(url, {
|
|
1744
|
+
method: "POST",
|
|
1745
|
+
headers: {
|
|
1746
|
+
...await walletAuthHeaders({
|
|
1747
|
+
signer: this.signer,
|
|
1748
|
+
method: "POST",
|
|
1749
|
+
url,
|
|
1750
|
+
body: serialized
|
|
1751
|
+
}),
|
|
1752
|
+
"content-type": "application/json"
|
|
1753
|
+
},
|
|
1754
|
+
body: serialized
|
|
1755
|
+
});
|
|
1756
|
+
const text = await response.text();
|
|
1757
|
+
if (response.status !== 200) {
|
|
1758
|
+
throw new VaultFlowClientError(
|
|
1759
|
+
step,
|
|
1760
|
+
`${path} failed with ${response.status}: ${text}`,
|
|
1761
|
+
text
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
try {
|
|
1765
|
+
return JSON.parse(text);
|
|
1766
|
+
} catch {
|
|
1767
|
+
throw new VaultFlowClientError(
|
|
1768
|
+
step,
|
|
1769
|
+
`${path} returned 200 with a non-JSON body`,
|
|
1770
|
+
text
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1317
1774
|
};
|
|
1318
1775
|
|
|
1319
1776
|
// ../../src/client/mcp-payment-server.ts
|
|
1320
1777
|
var TOOL_NAME = "fetch_with_subly_payment";
|
|
1778
|
+
var DEPOSIT_TOOL_NAME = "deposit_to_subly_vault";
|
|
1779
|
+
var WITHDRAW_TOOL_NAME = "withdraw_from_subly_vault";
|
|
1780
|
+
var BUDGET_TOOL_NAME = "get_subly_yield_budget";
|
|
1321
1781
|
var SERVER_INSTRUCTIONS = `Subly lets an agent pay for ANY standard x402 (HTTP 402) paid API from its wallet's Kamino vault YIELD \u2014 the deposited principal is never spent, and the seller needs no Subly integration.
|
|
1322
1782
|
|
|
1323
|
-
|
|
1324
|
-
1. A Solana keypair for the agent wallet. Subly does NOT create wallets; make one with \`solana-keygen new -o agent.json\` (or export a keypair from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it. The private key never leaves that file; this server only signs locally with it.
|
|
1325
|
-
2. Funded that wallet with USDC on Solana mainnet (no SOL needed \u2014 realize fees are sponsored) and deposited into the vault (see the project's deposit command). The vault minimum deposit is 1 USDC.
|
|
1326
|
-
3. Waited for yield to accrue; a payment needs the seller's price of spendable yield.
|
|
1783
|
+
One-time setup: the operator needs a Solana keypair for the agent wallet. Subly does NOT create wallets; make one with \`solana-keygen new -o agent.json\` (or export a keypair from an existing wallet) and point SUBLY_DEMO_AGENT_KEYPAIR_PATH at it. The private key never leaves that file; this server only signs locally with it. Then fund the wallet with USDC on Solana mainnet \u2014 no SOL is ever needed, all vault transaction fees are sponsored.
|
|
1327
1784
|
|
|
1328
|
-
|
|
1785
|
+
From there the agent can do everything with these tools:
|
|
1786
|
+
1. deposit_to_subly_vault(amountRawUsdc) puts wallet USDC into the vault (minimum just over 1 USDC, e.g. 1010000 raw) so it starts earning yield.
|
|
1787
|
+
2. get_subly_yield_budget() shows the principal, position value, and the spendable yield a payment can use right now.
|
|
1788
|
+
3. fetch_with_subly_payment(url) GETs or POSTs a paid resource from any x402 seller (e.g. Nansen): it realizes just enough yield to the agent's USDC ATA and pays the seller's standard x402 challenge, returning the body plus the payment details. If it returns insufficient_yield, that is expected \u2014 yield accrues over time; wait, do not loop.
|
|
1789
|
+
4. withdraw_from_subly_vault(amountRawUsdc) exits: moves vault funds (principal included) back to the agent wallet's USDC account.`;
|
|
1329
1790
|
async function runMcpPaymentServer(config) {
|
|
1330
|
-
const { payer: payer2, signer: signer2,
|
|
1791
|
+
const { payer: payer2, signer: signer2, relayerBaseUrl: relayerBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
|
|
1792
|
+
const vaultFlows = config.vaultFlows ?? null;
|
|
1331
1793
|
const server = new Server(
|
|
1332
1794
|
{ name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
|
|
1333
1795
|
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
1334
1796
|
);
|
|
1797
|
+
const vaultTools = vaultFlows === null ? [] : [
|
|
1798
|
+
{
|
|
1799
|
+
name: DEPOSIT_TOOL_NAME,
|
|
1800
|
+
description: "Deposit USDC from the agent wallet into the Subly/Kamino vault so it starts earning the yield that funds x402 payments. The transaction fee is sponsored \u2014 the agent wallet needs USDC only, never SOL. The vault minimum is just over 1 USDC: share rounding refuses exactly 1000000 raw, so deposit e.g. 1010000 (1.01 USDC) or more. The deposited amount becomes protected principal: payments can only ever spend the yield on top of it.",
|
|
1801
|
+
inputSchema: {
|
|
1802
|
+
type: "object",
|
|
1803
|
+
properties: {
|
|
1804
|
+
amountRawUsdc: {
|
|
1805
|
+
type: "string",
|
|
1806
|
+
description: 'Amount to deposit in raw USDC units (6 decimals, e.g. "1010000" = 1.01 USDC). Must exceed the 1 USDC vault minimum by a small rounding margin.'
|
|
1807
|
+
}
|
|
1808
|
+
},
|
|
1809
|
+
required: ["amountRawUsdc"]
|
|
1810
|
+
},
|
|
1811
|
+
annotations: {
|
|
1812
|
+
title: "Deposit into the Subly vault",
|
|
1813
|
+
readOnlyHint: false,
|
|
1814
|
+
destructiveHint: false,
|
|
1815
|
+
idempotentHint: false,
|
|
1816
|
+
openWorldHint: false
|
|
1817
|
+
}
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
name: WITHDRAW_TOOL_NAME,
|
|
1821
|
+
description: "Withdraw USDC from the Subly/Kamino vault back to the agent wallet's USDC account (fee sponsored, no SOL needed). This is the exit path and may spend PRINCIPAL \u2014 it reduces the deposit that earns yield. Limited to the vault's instant liquidity.",
|
|
1822
|
+
inputSchema: {
|
|
1823
|
+
type: "object",
|
|
1824
|
+
properties: {
|
|
1825
|
+
amountRawUsdc: {
|
|
1826
|
+
type: "string",
|
|
1827
|
+
description: 'Amount to withdraw in raw USDC units (6 decimals, e.g. "1000000" = 1 USDC).'
|
|
1828
|
+
}
|
|
1829
|
+
},
|
|
1830
|
+
required: ["amountRawUsdc"]
|
|
1831
|
+
},
|
|
1832
|
+
annotations: {
|
|
1833
|
+
title: "Withdraw from the Subly vault",
|
|
1834
|
+
readOnlyHint: false,
|
|
1835
|
+
destructiveHint: true,
|
|
1836
|
+
idempotentHint: false,
|
|
1837
|
+
openWorldHint: false
|
|
1838
|
+
}
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
name: BUDGET_TOOL_NAME,
|
|
1842
|
+
description: "Show the agent wallet's Subly vault budget: protected principal, current position value, and the spendable yield available for x402 payments right now. Syncs the position from chain first, so newly accrued yield is included.",
|
|
1843
|
+
inputSchema: { type: "object", properties: {} },
|
|
1844
|
+
annotations: {
|
|
1845
|
+
title: "Get Subly yield budget",
|
|
1846
|
+
readOnlyHint: true,
|
|
1847
|
+
destructiveHint: false,
|
|
1848
|
+
idempotentHint: true,
|
|
1849
|
+
openWorldHint: false
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
];
|
|
1335
1853
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1336
1854
|
tools: [
|
|
1855
|
+
...vaultTools,
|
|
1337
1856
|
{
|
|
1338
1857
|
name: TOOL_NAME,
|
|
1339
1858
|
description: `Fetch a URL (GET or POST), automatically paying a standard x402 (HTTP 402) challenge from any x402-compatible seller (Nansen, etc.) out of the agent wallet's Kamino vault yield. Subly realizes just enough yield to the agent's USDC ATA (sponsored) and pays the seller's Solana USDC \`exact\` challenge; the seller needs no Subly integration. Returns the response body and, when a payment was made, the payment details (amount, payee, realize tx). Challenges above maxAmountRawUsdc (default ${defaultMaxAmountRawUsdc2} raw = ${formatRawUsdcAmount(
|
|
@@ -1362,6 +1881,10 @@ async function runMcpPaymentServer(config) {
|
|
|
1362
1881
|
maxAmountRawUsdc: {
|
|
1363
1882
|
type: "string",
|
|
1364
1883
|
description: 'Refuse (without paying) any challenge above this amount in raw USDC units (6 decimals, e.g. "10000" = 0.01 USDC). Defaults to the server-side cap.'
|
|
1884
|
+
},
|
|
1885
|
+
forceNewPayment: {
|
|
1886
|
+
type: "boolean",
|
|
1887
|
+
description: "Pay again even if a previous external x402 attempt for the same URL/method/body has an unknown outcome. This may pay twice for the same resource."
|
|
1365
1888
|
}
|
|
1366
1889
|
},
|
|
1367
1890
|
required: ["url"]
|
|
@@ -1376,7 +1899,118 @@ async function runMcpPaymentServer(config) {
|
|
|
1376
1899
|
}
|
|
1377
1900
|
]
|
|
1378
1901
|
}));
|
|
1902
|
+
const textResult = (value, isError = false) => ({
|
|
1903
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
1904
|
+
...isError ? { isError: true } : {}
|
|
1905
|
+
});
|
|
1906
|
+
const vaultFlowFailure = (error) => {
|
|
1907
|
+
if (error instanceof VaultFlowClientError) {
|
|
1908
|
+
return textResult(
|
|
1909
|
+
{ ok: false, step: error.step, message: error.message },
|
|
1910
|
+
true
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
return {
|
|
1914
|
+
content: [
|
|
1915
|
+
{
|
|
1916
|
+
type: "text",
|
|
1917
|
+
text: error instanceof Error ? error.message : String(error)
|
|
1918
|
+
}
|
|
1919
|
+
],
|
|
1920
|
+
isError: true
|
|
1921
|
+
};
|
|
1922
|
+
};
|
|
1923
|
+
const parseRawAmount = (value) => {
|
|
1924
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1927
|
+
try {
|
|
1928
|
+
const amount = BigInt(value);
|
|
1929
|
+
return amount > 0n ? amount : null;
|
|
1930
|
+
} catch {
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
const vaultFlowOutcome = (outcome, amountField) => {
|
|
1935
|
+
const solscanUrl = outcome.txSignature === null ? null : `https://solscan.io/tx/${outcome.txSignature}`;
|
|
1936
|
+
if (outcome.status === "submitted") {
|
|
1937
|
+
return textResult(
|
|
1938
|
+
{
|
|
1939
|
+
...outcome,
|
|
1940
|
+
...amountField,
|
|
1941
|
+
solscanUrl,
|
|
1942
|
+
stillConfirming: true,
|
|
1943
|
+
warning: "the transaction was broadcast but had not confirmed before the poll timeout. Do NOT submit this deposit/withdrawal again \u2014 it may still confirm and moving the funds twice is not what the user asked for. Check get_subly_yield_budget in a minute, or the solscanUrl."
|
|
1944
|
+
},
|
|
1945
|
+
true
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
return textResult(
|
|
1949
|
+
{ ...outcome, ...amountField, solscanUrl },
|
|
1950
|
+
outcome.status !== "confirmed"
|
|
1951
|
+
);
|
|
1952
|
+
};
|
|
1379
1953
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1954
|
+
const vaultToolNames = [
|
|
1955
|
+
BUDGET_TOOL_NAME,
|
|
1956
|
+
DEPOSIT_TOOL_NAME,
|
|
1957
|
+
WITHDRAW_TOOL_NAME
|
|
1958
|
+
];
|
|
1959
|
+
if (vaultFlows !== null && vaultToolNames.includes(request.params.name)) {
|
|
1960
|
+
try {
|
|
1961
|
+
await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer2 });
|
|
1962
|
+
} catch {
|
|
1963
|
+
}
|
|
1964
|
+
if (request.params.name === BUDGET_TOOL_NAME) {
|
|
1965
|
+
try {
|
|
1966
|
+
const budget = await vaultFlows.getBudget();
|
|
1967
|
+
return textResult({
|
|
1968
|
+
...budget,
|
|
1969
|
+
principalUsdc: formatRawUsdcAmount(
|
|
1970
|
+
BigInt(budget.principalBasisRawUsdc)
|
|
1971
|
+
),
|
|
1972
|
+
positionValueUsdc: formatRawUsdcAmount(
|
|
1973
|
+
BigInt(budget.positionValueRawUsdc)
|
|
1974
|
+
),
|
|
1975
|
+
spendableYieldUsdc: formatRawUsdcAmount(
|
|
1976
|
+
BigInt(budget.spendableYieldRawUsdc)
|
|
1977
|
+
)
|
|
1978
|
+
});
|
|
1979
|
+
} catch (error) {
|
|
1980
|
+
return vaultFlowFailure(error);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
const amountRawUsdc = parseRawAmount(
|
|
1984
|
+
(request.params.arguments ?? {}).amountRawUsdc
|
|
1985
|
+
);
|
|
1986
|
+
if (amountRawUsdc === null) {
|
|
1987
|
+
return textResult(
|
|
1988
|
+
{
|
|
1989
|
+
ok: false,
|
|
1990
|
+
message: 'amountRawUsdc must be a positive integer raw USDC amount (6 decimals, e.g. "1000000" = 1 USDC)'
|
|
1991
|
+
},
|
|
1992
|
+
true
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
try {
|
|
1996
|
+
if (request.params.name === DEPOSIT_TOOL_NAME) {
|
|
1997
|
+
const outcome2 = await vaultFlows.deposit({ amountRawUsdc });
|
|
1998
|
+
return vaultFlowOutcome(outcome2, {
|
|
1999
|
+
depositedUsdc: formatRawUsdcAmount(
|
|
2000
|
+
BigInt(outcome2.actualDepositRawUsdc ?? "0")
|
|
2001
|
+
)
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
const outcome = await vaultFlows.withdraw({ amountRawUsdc });
|
|
2005
|
+
return vaultFlowOutcome(outcome, {
|
|
2006
|
+
withdrawnUsdc: formatRawUsdcAmount(
|
|
2007
|
+
BigInt(outcome.actualWithdrawRawUsdc ?? "0")
|
|
2008
|
+
)
|
|
2009
|
+
});
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
return vaultFlowFailure(error);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
1380
2014
|
if (request.params.name !== TOOL_NAME) {
|
|
1381
2015
|
return {
|
|
1382
2016
|
content: [{ type: "text", text: `unknown tool: ${request.params.name}` }],
|
|
@@ -1413,6 +2047,7 @@ async function runMcpPaymentServer(config) {
|
|
|
1413
2047
|
}
|
|
1414
2048
|
const method = typeof args.method === "string" ? args.method : void 0;
|
|
1415
2049
|
const body = typeof args.body === "string" ? args.body : void 0;
|
|
2050
|
+
const forceNewPayment = args.forceNewPayment === true;
|
|
1416
2051
|
const headers = args.headers !== null && typeof args.headers === "object" && !Array.isArray(args.headers) ? Object.fromEntries(
|
|
1417
2052
|
Object.entries(args.headers).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])
|
|
1418
2053
|
) : void 0;
|
|
@@ -1423,7 +2058,8 @@ async function runMcpPaymentServer(config) {
|
|
|
1423
2058
|
...method === void 0 ? {} : { method },
|
|
1424
2059
|
...body === void 0 ? {} : { body },
|
|
1425
2060
|
...mergedHeaders === void 0 ? {} : { headers: mergedHeaders },
|
|
1426
|
-
...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc }
|
|
2061
|
+
...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
|
|
2062
|
+
...forceNewPayment ? { forceNewPayment } : {}
|
|
1427
2063
|
});
|
|
1428
2064
|
return {
|
|
1429
2065
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
@@ -1462,7 +2098,7 @@ async function runMcpPaymentServer(config) {
|
|
|
1462
2098
|
}
|
|
1463
2099
|
});
|
|
1464
2100
|
try {
|
|
1465
|
-
await ensureWalletOnboarded({
|
|
2101
|
+
await ensureWalletOnboarded({ relayerBaseUrl: relayerBaseUrl2, signer: signer2 });
|
|
1466
2102
|
console.error("[subly-mcp] wallet registered and synced at the relayer");
|
|
1467
2103
|
} catch (error) {
|
|
1468
2104
|
console.error(
|
|
@@ -1472,54 +2108,12 @@ async function runMcpPaymentServer(config) {
|
|
|
1472
2108
|
const transport = new StdioServerTransport();
|
|
1473
2109
|
await server.connect(transport);
|
|
1474
2110
|
console.error(
|
|
1475
|
-
`[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${
|
|
1476
|
-
);
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
// ../../src/client/relayer-yield-realizer.ts
|
|
1480
|
-
import { address as address2 } from "@solana/kit";
|
|
1481
|
-
|
|
1482
|
-
// ../../src/client/lookup-tables.ts
|
|
1483
|
-
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
1484
|
-
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
1485
|
-
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
1486
|
-
const wire = Buffer.from(serializedTransaction, "base64");
|
|
1487
|
-
let offset = 0;
|
|
1488
|
-
let signatureCount = 0;
|
|
1489
|
-
let shift = 0;
|
|
1490
|
-
while (offset < wire.length) {
|
|
1491
|
-
const byte = wire[offset];
|
|
1492
|
-
signatureCount |= (byte & 127) << shift;
|
|
1493
|
-
offset += 1;
|
|
1494
|
-
if ((byte & 128) === 0) {
|
|
1495
|
-
break;
|
|
1496
|
-
}
|
|
1497
|
-
shift += 7;
|
|
1498
|
-
}
|
|
1499
|
-
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
1500
|
-
const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
|
|
1501
|
-
const lookups = compiled.addressTableLookups ?? [];
|
|
1502
|
-
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
1503
|
-
}
|
|
1504
|
-
async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
1505
|
-
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
1506
|
-
if (addresses.length === 0) {
|
|
1507
|
-
return {};
|
|
1508
|
-
}
|
|
1509
|
-
const tables = await fetchAllMaybeAddressLookupTable(
|
|
1510
|
-
rpc2,
|
|
1511
|
-
addresses.map((value) => address(value))
|
|
2111
|
+
`[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${relayerBaseUrl2}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc2)} USDC`
|
|
1512
2112
|
);
|
|
1513
|
-
const result = {};
|
|
1514
|
-
for (const table of tables) {
|
|
1515
|
-
if (table.exists) {
|
|
1516
|
-
result[table.address] = table.data.addresses.map(String);
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
return result;
|
|
1520
2113
|
}
|
|
1521
2114
|
|
|
1522
2115
|
// ../../src/client/relayer-yield-realizer.ts
|
|
2116
|
+
var REALIZE_OVERHEAD_RAW_USDC = 2500n;
|
|
1523
2117
|
var RelayerRealizeError = class extends Error {
|
|
1524
2118
|
constructor(code, message, detail = null) {
|
|
1525
2119
|
super(message);
|
|
@@ -1531,79 +2125,52 @@ var RelayerRealizeError = class extends Error {
|
|
|
1531
2125
|
detail;
|
|
1532
2126
|
};
|
|
1533
2127
|
var RelayerYieldRealizer = class {
|
|
1534
|
-
|
|
1535
|
-
signer;
|
|
1536
|
-
rpc;
|
|
1537
|
-
fetchImpl;
|
|
1538
|
-
lookupTablesFor;
|
|
2128
|
+
vaultFlows;
|
|
1539
2129
|
constructor(config) {
|
|
1540
|
-
this.
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1548
|
-
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(this.rpc, serializedTransaction));
|
|
2130
|
+
this.vaultFlows = new VaultFlowClient({
|
|
2131
|
+
relayerBaseUrl: config.relayerBaseUrl,
|
|
2132
|
+
signer: config.signer,
|
|
2133
|
+
rpc: config.rpc,
|
|
2134
|
+
...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
|
|
2135
|
+
...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
|
|
2136
|
+
});
|
|
1549
2137
|
}
|
|
1550
2138
|
async ensureUsdcAvailable(input) {
|
|
1551
|
-
const
|
|
1552
|
-
owner: this.signer.walletAddress,
|
|
1553
|
-
mint: this.config.usdcMint
|
|
1554
|
-
});
|
|
1555
|
-
let shortfallRawUsdc;
|
|
1556
|
-
if (this.config.forceRealizeFullAmount) {
|
|
1557
|
-
shortfallRawUsdc = input.amountRawUsdc;
|
|
1558
|
-
} else {
|
|
1559
|
-
const currentBalance = await this.readTokenBalance(agentAta);
|
|
1560
|
-
if (currentBalance >= input.amountRawUsdc) {
|
|
1561
|
-
return { realizedRawUsdc: 0n, txSignature: null };
|
|
1562
|
-
}
|
|
1563
|
-
shortfallRawUsdc = input.amountRawUsdc - currentBalance;
|
|
1564
|
-
}
|
|
2139
|
+
const shortfallRawUsdc = input.amountRawUsdc;
|
|
1565
2140
|
await this.assertSpendableYield(shortfallRawUsdc);
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
})
|
|
1575
|
-
|
|
1576
|
-
withdrawalId: prepared.withdrawalId,
|
|
1577
|
-
serializedTransaction: signed.serializedTransaction,
|
|
1578
|
-
agentSignature: signed.agentSignature
|
|
1579
|
-
});
|
|
1580
|
-
if (settled.status === "submitted") {
|
|
1581
|
-
settled = await this.pollUntilSettled(prepared.withdrawalId);
|
|
2141
|
+
let outcome;
|
|
2142
|
+
try {
|
|
2143
|
+
outcome = await this.vaultFlows.withdraw({
|
|
2144
|
+
amountRawUsdc: shortfallRawUsdc,
|
|
2145
|
+
// The relayer refuses to prepare this withdrawal beyond the spendable
|
|
2146
|
+
// yield — the principal-protection guard the client cannot bypass.
|
|
2147
|
+
purpose: "yield_realize"
|
|
2148
|
+
});
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
throw this.mapWithdrawError(error);
|
|
1582
2151
|
}
|
|
1583
|
-
if (
|
|
2152
|
+
if (outcome.status !== "confirmed" || outcome.txSignature === null) {
|
|
1584
2153
|
throw new RelayerRealizeError(
|
|
1585
2154
|
"realize_not_confirmed",
|
|
1586
|
-
`yield realize withdrawal did not confirm (status=${
|
|
1587
|
-
|
|
2155
|
+
`yield realize withdrawal did not confirm (status=${outcome.status})`,
|
|
2156
|
+
outcome
|
|
1588
2157
|
);
|
|
1589
2158
|
}
|
|
1590
2159
|
return {
|
|
1591
|
-
realizedRawUsdc: BigInt(
|
|
1592
|
-
txSignature:
|
|
2160
|
+
realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
|
|
2161
|
+
txSignature: outcome.txSignature
|
|
1593
2162
|
};
|
|
1594
2163
|
}
|
|
1595
|
-
/**
|
|
2164
|
+
/**
|
|
2165
|
+
* Refuses to realize more than the ledger's spendable yield (principal).
|
|
2166
|
+
* getBudget syncs the relayer's ledger from chain first (best-effort), so a
|
|
2167
|
+
* long-running client sees yield as it accrues instead of a frozen view.
|
|
2168
|
+
*/
|
|
1596
2169
|
async assertSpendableYield(shortfallRawUsdc) {
|
|
1597
|
-
|
|
1598
|
-
let response;
|
|
2170
|
+
let spendable;
|
|
1599
2171
|
try {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
signer: this.signer,
|
|
1603
|
-
method: "GET",
|
|
1604
|
-
url
|
|
1605
|
-
})
|
|
1606
|
-
});
|
|
2172
|
+
const budget = await this.vaultFlows.getBudget();
|
|
2173
|
+
spendable = BigInt(budget.spendableYieldRawUsdc);
|
|
1607
2174
|
} catch (error) {
|
|
1608
2175
|
throw new RelayerRealizeError(
|
|
1609
2176
|
"budget_unavailable",
|
|
@@ -1611,105 +2178,118 @@ var RelayerYieldRealizer = class {
|
|
|
1611
2178
|
error
|
|
1612
2179
|
);
|
|
1613
2180
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
"budget_unavailable",
|
|
1617
|
-
`budget endpoint returned ${response.status}`
|
|
1618
|
-
);
|
|
1619
|
-
}
|
|
1620
|
-
const body = await response.json();
|
|
1621
|
-
const spendable = BigInt(body.budget?.spendableYieldRawUsdc ?? "0");
|
|
1622
|
-
if (spendable < shortfallRawUsdc) {
|
|
2181
|
+
const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
|
|
2182
|
+
if (spendable < requiredRawUsdc) {
|
|
1623
2183
|
throw new RelayerRealizeError(
|
|
1624
2184
|
"insufficient_yield",
|
|
1625
|
-
`spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC; the principal is never spent \u2014 wait for more yield`,
|
|
2185
|
+
`spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC plus the ${REALIZE_OVERHEAD_RAW_USDC} raw fee headroom; the principal is never spent \u2014 wait for more yield`,
|
|
1626
2186
|
{ spendableYieldRawUsdc: spendable.toString() }
|
|
1627
2187
|
);
|
|
1628
2188
|
}
|
|
1629
2189
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
let latest = null;
|
|
1637
|
-
while (Date.now() < deadline) {
|
|
1638
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1639
|
-
const url = `${this.config.facilitatorBaseUrl}/v1/withdrawals/${withdrawalId}`;
|
|
1640
|
-
const response = await this.fetchImpl(url, {
|
|
1641
|
-
headers: await walletAuthHeaders({
|
|
1642
|
-
signer: this.signer,
|
|
1643
|
-
method: "GET",
|
|
1644
|
-
url
|
|
1645
|
-
})
|
|
1646
|
-
});
|
|
1647
|
-
if (response.status !== 200) {
|
|
1648
|
-
continue;
|
|
1649
|
-
}
|
|
1650
|
-
latest = await response.json();
|
|
1651
|
-
if (latest.status !== "submitted") {
|
|
1652
|
-
return latest;
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
return latest ?? {
|
|
1656
|
-
status: "submitted",
|
|
1657
|
-
txSignature: null,
|
|
1658
|
-
actualWithdrawRawUsdc: null
|
|
1659
|
-
};
|
|
1660
|
-
}
|
|
1661
|
-
async postJson(path, body) {
|
|
1662
|
-
const url = `${this.config.facilitatorBaseUrl}${path}`;
|
|
1663
|
-
const serialized = JSON.stringify(body);
|
|
1664
|
-
const response = await this.fetchImpl(url, {
|
|
1665
|
-
method: "POST",
|
|
1666
|
-
headers: {
|
|
1667
|
-
...await walletAuthHeaders({
|
|
1668
|
-
signer: this.signer,
|
|
1669
|
-
method: "POST",
|
|
1670
|
-
url,
|
|
1671
|
-
body: serialized
|
|
1672
|
-
}),
|
|
1673
|
-
"content-type": "application/json"
|
|
1674
|
-
},
|
|
1675
|
-
body: serialized
|
|
1676
|
-
});
|
|
1677
|
-
const text = await response.text();
|
|
1678
|
-
if (response.status !== 200) {
|
|
1679
|
-
throw new RelayerRealizeError(
|
|
1680
|
-
path.endsWith("/submit") ? "submit_failed" : "prepare_failed",
|
|
1681
|
-
`${path} failed with ${response.status}: ${text}`
|
|
2190
|
+
mapWithdrawError(error) {
|
|
2191
|
+
if (!(error instanceof VaultFlowClientError)) {
|
|
2192
|
+
return new RelayerRealizeError(
|
|
2193
|
+
"prepare_failed",
|
|
2194
|
+
`yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2195
|
+
error
|
|
1682
2196
|
);
|
|
1683
2197
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
return 0n;
|
|
2198
|
+
const serverCode = errorCodeFrom(error.detail);
|
|
2199
|
+
if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
|
|
2200
|
+
return new RelayerRealizeError(
|
|
2201
|
+
"insufficient_yield",
|
|
2202
|
+
"the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
|
|
2203
|
+
error.detail
|
|
2204
|
+
);
|
|
1692
2205
|
}
|
|
2206
|
+
return new RelayerRealizeError(
|
|
2207
|
+
error.step === "submit" ? "submit_failed" : "prepare_failed",
|
|
2208
|
+
error.message,
|
|
2209
|
+
error.detail
|
|
2210
|
+
);
|
|
1693
2211
|
}
|
|
1694
2212
|
};
|
|
2213
|
+
function errorCodeFrom(detail) {
|
|
2214
|
+
if (typeof detail !== "string") {
|
|
2215
|
+
return null;
|
|
2216
|
+
}
|
|
2217
|
+
try {
|
|
2218
|
+
const parsed = JSON.parse(detail);
|
|
2219
|
+
return typeof parsed.error?.code === "string" ? parsed.error.code : null;
|
|
2220
|
+
} catch {
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
1695
2224
|
|
|
1696
2225
|
// ../../src/client/relayer-payer.ts
|
|
1697
2226
|
function createRelayerX402Payer(config) {
|
|
1698
2227
|
const realizer = new RelayerYieldRealizer({
|
|
1699
|
-
|
|
2228
|
+
relayerBaseUrl: config.relayerBaseUrl,
|
|
1700
2229
|
signer: config.signer,
|
|
1701
|
-
rpc: config.rpc
|
|
1702
|
-
forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
|
|
2230
|
+
rpc: config.rpc
|
|
1703
2231
|
});
|
|
1704
2232
|
return new StandardX402Payer({
|
|
1705
2233
|
realizer,
|
|
1706
2234
|
x402Fetch: config.x402Fetch,
|
|
1707
|
-
defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc
|
|
2235
|
+
defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc,
|
|
2236
|
+
...config.stateStore === void 0 ? {} : { stateStore: config.stateStore }
|
|
1708
2237
|
});
|
|
1709
2238
|
}
|
|
1710
2239
|
|
|
2240
|
+
// ../../src/client/standard-x402-state-store.ts
|
|
2241
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2242
|
+
import { basename, dirname, join } from "node:path";
|
|
2243
|
+
function fileStandardX402StateStore(path) {
|
|
2244
|
+
return {
|
|
2245
|
+
load() {
|
|
2246
|
+
let text;
|
|
2247
|
+
try {
|
|
2248
|
+
text = readFileSync(path, "utf8");
|
|
2249
|
+
} catch (error) {
|
|
2250
|
+
if (isMissingFileError(error)) {
|
|
2251
|
+
return [];
|
|
2252
|
+
}
|
|
2253
|
+
throw error;
|
|
2254
|
+
}
|
|
2255
|
+
const parsed = JSON.parse(text);
|
|
2256
|
+
if (!Array.isArray(parsed)) {
|
|
2257
|
+
throw new Error(`pending payment state is not an array: ${path}`);
|
|
2258
|
+
}
|
|
2259
|
+
for (const [index, record] of parsed.entries()) {
|
|
2260
|
+
if (!isPendingPaymentRecord(record)) {
|
|
2261
|
+
throw new Error(
|
|
2262
|
+
`pending payment state has an invalid record at index ${index}: ${path}`
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return parsed;
|
|
2267
|
+
},
|
|
2268
|
+
save(records) {
|
|
2269
|
+
const directory = dirname(path);
|
|
2270
|
+
mkdirSync(directory, { recursive: true });
|
|
2271
|
+
const tempPath = join(
|
|
2272
|
+
directory,
|
|
2273
|
+
`.${basename(path)}.${process.pid}.${Date.now()}.tmp`
|
|
2274
|
+
);
|
|
2275
|
+
writeFileSync(tempPath, JSON.stringify(records, null, 2));
|
|
2276
|
+
renameSync(tempPath, path);
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
function isMissingFileError(error) {
|
|
2281
|
+
return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
2282
|
+
}
|
|
2283
|
+
function isPendingPaymentRecord(value) {
|
|
2284
|
+
if (value === null || typeof value !== "object") {
|
|
2285
|
+
return false;
|
|
2286
|
+
}
|
|
2287
|
+
const record = value;
|
|
2288
|
+
return typeof record.key === "string" && typeof record.url === "string" && typeof record.method === "string" && typeof record.requestBodyHash === "string" && typeof record.amountRawUsdc === "string" && typeof record.payTo === "string" && (record.feePayer === null || typeof record.feePayer === "string") && typeof record.realizedRawUsdc === "string" && (record.realizeTxSignature === null || typeof record.realizeTxSignature === "string") && (record.status === "realized" || record.status === "external_outcome_unknown") && typeof record.createdAtMs === "number" && typeof record.updatedAtMs === "number";
|
|
2289
|
+
}
|
|
2290
|
+
|
|
1711
2291
|
// ../../src/solana/keys.ts
|
|
1712
|
-
import { readFileSync } from "node:fs";
|
|
2292
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1713
2293
|
import bs586 from "bs58";
|
|
1714
2294
|
import {
|
|
1715
2295
|
createKeyPairSignerFromBytes
|
|
@@ -1724,7 +2304,7 @@ async function loadKeyPairSigner(params) {
|
|
|
1724
2304
|
return createKeyPairSignerFromBytes(bytes);
|
|
1725
2305
|
}
|
|
1726
2306
|
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1727
|
-
const raw = JSON.parse(
|
|
2307
|
+
const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
|
|
1728
2308
|
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1729
2309
|
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1730
2310
|
}
|
|
@@ -1742,7 +2322,7 @@ function loadSecretKeyBytes(params) {
|
|
|
1742
2322
|
return bytes;
|
|
1743
2323
|
}
|
|
1744
2324
|
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1745
|
-
const raw = JSON.parse(
|
|
2325
|
+
const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
|
|
1746
2326
|
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1747
2327
|
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1748
2328
|
}
|
|
@@ -1765,21 +2345,36 @@ async function createSvmX402Fetch(params) {
|
|
|
1765
2345
|
const signer2 = toClientSvmSigner(
|
|
1766
2346
|
await createKeyPairSignerFromBytes2(params.agentSecretKey)
|
|
1767
2347
|
);
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
2348
|
+
return (url, init, expected) => {
|
|
2349
|
+
const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
|
|
2350
|
+
schemes: [
|
|
2351
|
+
{
|
|
2352
|
+
network: expected.requirement.network,
|
|
2353
|
+
client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
|
|
2354
|
+
}
|
|
2355
|
+
],
|
|
2356
|
+
paymentRequirementsSelector: (_x402Version, requirements) => selectExpectedRequirement(requirements, expected)
|
|
2357
|
+
});
|
|
2358
|
+
return wrapped(url, init);
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
function selectExpectedRequirement(requirements, expected) {
|
|
2362
|
+
const match = requirements.find(
|
|
2363
|
+
(candidate) => standardRequirementMatchesSelected(candidate, expected)
|
|
2364
|
+
);
|
|
2365
|
+
if (match === void 0) {
|
|
2366
|
+
throw new Error(
|
|
2367
|
+
"x402 challenge changed after preflight; refusing to pay an unchecked requirement"
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
return match;
|
|
1777
2371
|
}
|
|
1778
2372
|
|
|
1779
2373
|
// src/mcp-server.ts
|
|
1780
|
-
var
|
|
2374
|
+
var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
|
|
1781
2375
|
var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
|
|
1782
2376
|
var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
|
|
2377
|
+
var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? join2(homedir(), ".subly", "standard-x402-pending.json");
|
|
1783
2378
|
var keyPairSigner = await loadKeyPairSigner({
|
|
1784
2379
|
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1785
2380
|
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
@@ -1793,15 +2388,21 @@ var agentSecretKey = loadSecretKeyBytes({
|
|
|
1793
2388
|
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
1794
2389
|
var rpc = createRpc(rpcUrl);
|
|
1795
2390
|
var payer = createRelayerX402Payer({
|
|
1796
|
-
|
|
2391
|
+
relayerBaseUrl,
|
|
1797
2392
|
signer,
|
|
1798
2393
|
rpc,
|
|
1799
2394
|
x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
|
|
1800
|
-
defaultMaxAmountRawUsdc
|
|
2395
|
+
defaultMaxAmountRawUsdc,
|
|
2396
|
+
stateStore: fileStandardX402StateStore(pendingStatePath)
|
|
1801
2397
|
});
|
|
1802
2398
|
await runMcpPaymentServer({
|
|
1803
2399
|
payer,
|
|
1804
2400
|
signer,
|
|
1805
|
-
|
|
1806
|
-
defaultMaxAmountRawUsdc
|
|
2401
|
+
relayerBaseUrl,
|
|
2402
|
+
defaultMaxAmountRawUsdc,
|
|
2403
|
+
vaultFlows: new VaultFlowClient({
|
|
2404
|
+
relayerBaseUrl,
|
|
2405
|
+
signer,
|
|
2406
|
+
rpc
|
|
2407
|
+
})
|
|
1807
2408
|
});
|