@subly_fi/pay 0.2.0 → 0.4.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 +11 -4
- package/dist/deposit.js +262 -71
- package/dist/mcp-server.js +1076 -411
- package/dist/pay.js +861 -543
- package/dist/withdraw.js +295 -102
- package/package.json +9 -7
package/dist/mcp-server.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
CallToolRequestSchema,
|
|
6
|
-
ListToolsRequestSchema
|
|
7
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
1
|
+
// src/mcp-server.ts
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join as join2 } from "node:path";
|
|
8
4
|
|
|
9
|
-
// src/client/agent-wallet-signer.ts
|
|
5
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
10
6
|
import { signBytes } from "@solana/kit";
|
|
11
7
|
import bs584 from "bs58";
|
|
12
8
|
|
|
13
|
-
// src/solana/tx.ts
|
|
9
|
+
// ../../src/solana/tx.ts
|
|
14
10
|
import bs58 from "bs58";
|
|
15
11
|
import {
|
|
16
12
|
appendTransactionMessageInstructions,
|
|
@@ -25,8 +21,9 @@ import {
|
|
|
25
21
|
setTransactionMessageLifetimeUsingBlockhash
|
|
26
22
|
} from "@solana/kit";
|
|
27
23
|
|
|
28
|
-
// src/lib/hash.ts
|
|
24
|
+
// ../../src/lib/hash.ts
|
|
29
25
|
import { createHash } from "node:crypto";
|
|
26
|
+
var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
30
27
|
function sha256TaggedHex(data) {
|
|
31
28
|
return `sha256-${createHash("sha256").update(data).digest("hex")}`;
|
|
32
29
|
}
|
|
@@ -50,7 +47,7 @@ function hashStableJson(value) {
|
|
|
50
47
|
return sha256TaggedHex(stableStringify(value));
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
// src/solana/tx.ts
|
|
50
|
+
// ../../src/solana/tx.ts
|
|
54
51
|
function decodeSerializedTransaction(serializedBase64) {
|
|
55
52
|
return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
|
|
56
53
|
}
|
|
@@ -70,11 +67,11 @@ function signatureBase58ForSigner(transaction, signer2) {
|
|
|
70
67
|
return bs58.encode(signature);
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
// src/client/transaction-intent-validator.ts
|
|
70
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
74
71
|
import bs583 from "bs58";
|
|
75
72
|
import { getCompiledTransactionMessageDecoder } from "@solana/kit";
|
|
76
73
|
|
|
77
|
-
// src/config/constants.ts
|
|
74
|
+
// ../../src/config/constants.ts
|
|
78
75
|
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
79
76
|
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
80
77
|
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
@@ -90,7 +87,7 @@ var SUBLY_VAULT = {
|
|
|
90
87
|
};
|
|
91
88
|
var USDC_DECIMALS = 6;
|
|
92
89
|
|
|
93
|
-
// src/domain/request-binding.ts
|
|
90
|
+
// ../../src/domain/request-binding.ts
|
|
94
91
|
function computeRequestBindingHash(fields) {
|
|
95
92
|
return hashStableJson({
|
|
96
93
|
sellerRequestId: fields.sellerRequestId,
|
|
@@ -105,7 +102,7 @@ function computeRequestBindingHash(fields) {
|
|
|
105
102
|
});
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
// src/lib/associated-token-account.ts
|
|
105
|
+
// ../../src/lib/associated-token-account.ts
|
|
109
106
|
import { createHash as createHash2 } from "node:crypto";
|
|
110
107
|
import bs582 from "bs58";
|
|
111
108
|
var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
|
|
@@ -123,12 +120,12 @@ function deriveAssociatedTokenAddress(params) {
|
|
|
123
120
|
"associatedTokenProgramId"
|
|
124
121
|
);
|
|
125
122
|
for (let bump = 255; bump >= 0; bump -= 1) {
|
|
126
|
-
const
|
|
123
|
+
const address2 = createProgramAddress(
|
|
127
124
|
[owner, tokenProgramId, mint, Uint8Array.of(bump)],
|
|
128
125
|
associatedTokenProgramId
|
|
129
126
|
);
|
|
130
|
-
if (
|
|
131
|
-
return bs582.encode(
|
|
127
|
+
if (address2 !== null) {
|
|
128
|
+
return bs582.encode(address2);
|
|
132
129
|
}
|
|
133
130
|
}
|
|
134
131
|
throw new Error("Unable to derive associated token account address");
|
|
@@ -197,7 +194,7 @@ function modPow(base, exponent, modulus) {
|
|
|
197
194
|
return result;
|
|
198
195
|
}
|
|
199
196
|
|
|
200
|
-
// src/client/transaction-intent-validator.ts
|
|
197
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
201
198
|
var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
|
|
202
199
|
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
|
|
203
200
|
var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
@@ -883,7 +880,7 @@ function readShortVec(bytes, startOffset) {
|
|
|
883
880
|
return null;
|
|
884
881
|
}
|
|
885
882
|
|
|
886
|
-
// src/client/agent-wallet-signer.ts
|
|
883
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
887
884
|
var LocalKeypairAgentWalletSigner = class {
|
|
888
885
|
validationMode = "structured_intent_transaction";
|
|
889
886
|
keyPairSigner;
|
|
@@ -953,7 +950,15 @@ var LocalKeypairAgentWalletSigner = class {
|
|
|
953
950
|
}
|
|
954
951
|
};
|
|
955
952
|
|
|
956
|
-
// src/
|
|
953
|
+
// ../../src/client/mcp-payment-server.ts
|
|
954
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
955
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
956
|
+
import {
|
|
957
|
+
CallToolRequestSchema,
|
|
958
|
+
ListToolsRequestSchema
|
|
959
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
960
|
+
|
|
961
|
+
// ../../src/api/wallet-auth.ts
|
|
957
962
|
import { createHash as createHash3 } from "node:crypto";
|
|
958
963
|
import bs585 from "bs58";
|
|
959
964
|
import nacl from "tweetnacl";
|
|
@@ -971,7 +976,7 @@ function walletAuthMessage(params) {
|
|
|
971
976
|
);
|
|
972
977
|
}
|
|
973
978
|
|
|
974
|
-
// src/client/wallet-auth-headers.ts
|
|
979
|
+
// ../../src/client/wallet-auth-headers.ts
|
|
975
980
|
async function walletAuthHeaders(params) {
|
|
976
981
|
const signedAtMs = String(Date.now());
|
|
977
982
|
const message = walletAuthMessage({
|
|
@@ -987,7 +992,7 @@ async function walletAuthHeaders(params) {
|
|
|
987
992
|
};
|
|
988
993
|
}
|
|
989
994
|
|
|
990
|
-
// src/client/onboarding.ts
|
|
995
|
+
// ../../src/client/onboarding.ts
|
|
991
996
|
var SELF_SERVE_POLICY_ID = "self-serve";
|
|
992
997
|
var OnboardingError = class extends Error {
|
|
993
998
|
constructor(step, message, detail = null) {
|
|
@@ -1044,187 +1049,7 @@ async function ensureWalletOnboarded(params) {
|
|
|
1044
1049
|
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
|
|
1045
1050
|
}
|
|
1046
1051
|
|
|
1047
|
-
// src/
|
|
1048
|
-
import { createX402Client } from "x402-solana/client";
|
|
1049
|
-
|
|
1050
|
-
// src/client/relayer-yield-realizer.ts
|
|
1051
|
-
import { address as address2 } from "@solana/kit";
|
|
1052
|
-
|
|
1053
|
-
// src/client/lookup-tables.ts
|
|
1054
|
-
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
1055
|
-
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
1056
|
-
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
1057
|
-
const wire = Buffer.from(serializedTransaction, "base64");
|
|
1058
|
-
let offset = 0;
|
|
1059
|
-
let signatureCount = 0;
|
|
1060
|
-
let shift = 0;
|
|
1061
|
-
while (offset < wire.length) {
|
|
1062
|
-
const byte = wire[offset];
|
|
1063
|
-
signatureCount |= (byte & 127) << shift;
|
|
1064
|
-
offset += 1;
|
|
1065
|
-
if ((byte & 128) === 0) {
|
|
1066
|
-
break;
|
|
1067
|
-
}
|
|
1068
|
-
shift += 7;
|
|
1069
|
-
}
|
|
1070
|
-
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
1071
|
-
const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
|
|
1072
|
-
const lookups = compiled.addressTableLookups ?? [];
|
|
1073
|
-
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
1074
|
-
}
|
|
1075
|
-
async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
1076
|
-
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
1077
|
-
if (addresses.length === 0) {
|
|
1078
|
-
return {};
|
|
1079
|
-
}
|
|
1080
|
-
const tables = await fetchAllMaybeAddressLookupTable(
|
|
1081
|
-
rpc2,
|
|
1082
|
-
addresses.map((value) => address(value))
|
|
1083
|
-
);
|
|
1084
|
-
const result = {};
|
|
1085
|
-
for (const table of tables) {
|
|
1086
|
-
if (table.exists) {
|
|
1087
|
-
result[table.address] = table.data.addresses.map(String);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
return result;
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// src/client/relayer-yield-realizer.ts
|
|
1094
|
-
var RelayerRealizeError = class extends Error {
|
|
1095
|
-
constructor(code, message, detail = null) {
|
|
1096
|
-
super(message);
|
|
1097
|
-
this.code = code;
|
|
1098
|
-
this.detail = detail;
|
|
1099
|
-
this.name = "RelayerRealizeError";
|
|
1100
|
-
}
|
|
1101
|
-
code;
|
|
1102
|
-
detail;
|
|
1103
|
-
};
|
|
1104
|
-
var RelayerYieldRealizer = class {
|
|
1105
|
-
config;
|
|
1106
|
-
signer;
|
|
1107
|
-
rpc;
|
|
1108
|
-
fetchImpl;
|
|
1109
|
-
lookupTablesFor;
|
|
1110
|
-
constructor(config) {
|
|
1111
|
-
this.config = {
|
|
1112
|
-
facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
|
|
1113
|
-
usdcMint: config.usdcMint ?? SUBLY_VAULT.usdcMint
|
|
1114
|
-
};
|
|
1115
|
-
this.signer = config.signer;
|
|
1116
|
-
this.rpc = config.rpc;
|
|
1117
|
-
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1118
|
-
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(this.rpc, serializedTransaction));
|
|
1119
|
-
}
|
|
1120
|
-
async ensureUsdcAvailable(input) {
|
|
1121
|
-
const agentAta = deriveAssociatedTokenAddress({
|
|
1122
|
-
owner: this.signer.walletAddress,
|
|
1123
|
-
mint: this.config.usdcMint
|
|
1124
|
-
});
|
|
1125
|
-
const currentBalance = await this.readTokenBalance(agentAta);
|
|
1126
|
-
if (currentBalance >= input.amountRawUsdc) {
|
|
1127
|
-
return { realizedRawUsdc: 0n, txSignature: null };
|
|
1128
|
-
}
|
|
1129
|
-
const shortfallRawUsdc = input.amountRawUsdc - currentBalance;
|
|
1130
|
-
await this.assertSpendableYield(shortfallRawUsdc);
|
|
1131
|
-
const prepared = await this.postJson("/v1/withdrawals/prepare", {
|
|
1132
|
-
wallet: this.signer.walletAddress,
|
|
1133
|
-
amountRawUsdc: shortfallRawUsdc.toString()
|
|
1134
|
-
});
|
|
1135
|
-
const signed = await this.signer.signWithdrawal({
|
|
1136
|
-
intent: prepared.signingIntent,
|
|
1137
|
-
serializedTransaction: prepared.serializedTransaction,
|
|
1138
|
-
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1139
|
-
});
|
|
1140
|
-
const submitted = await this.postJson("/v1/withdrawals/submit", {
|
|
1141
|
-
withdrawalId: prepared.withdrawalId,
|
|
1142
|
-
serializedTransaction: signed.serializedTransaction,
|
|
1143
|
-
agentSignature: signed.agentSignature
|
|
1144
|
-
});
|
|
1145
|
-
if (submitted.status !== "confirmed" || submitted.txSignature === null) {
|
|
1146
|
-
throw new RelayerRealizeError(
|
|
1147
|
-
"realize_not_confirmed",
|
|
1148
|
-
`yield realize withdrawal did not confirm (status=${submitted.status})`,
|
|
1149
|
-
submitted
|
|
1150
|
-
);
|
|
1151
|
-
}
|
|
1152
|
-
return {
|
|
1153
|
-
realizedRawUsdc: BigInt(submitted.actualWithdrawRawUsdc ?? "0"),
|
|
1154
|
-
txSignature: submitted.txSignature
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
|
-
/** Refuses to realize more than the ledger's spendable yield (principal). */
|
|
1158
|
-
async assertSpendableYield(shortfallRawUsdc) {
|
|
1159
|
-
const url = `${this.config.facilitatorBaseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
1160
|
-
let response;
|
|
1161
|
-
try {
|
|
1162
|
-
response = await this.fetchImpl(url, {
|
|
1163
|
-
headers: await walletAuthHeaders({
|
|
1164
|
-
signer: this.signer,
|
|
1165
|
-
method: "GET",
|
|
1166
|
-
url
|
|
1167
|
-
})
|
|
1168
|
-
});
|
|
1169
|
-
} catch (error) {
|
|
1170
|
-
throw new RelayerRealizeError(
|
|
1171
|
-
"budget_unavailable",
|
|
1172
|
-
"could not read the spendable-yield budget",
|
|
1173
|
-
error
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1176
|
-
if (response.status !== 200) {
|
|
1177
|
-
throw new RelayerRealizeError(
|
|
1178
|
-
"budget_unavailable",
|
|
1179
|
-
`budget endpoint returned ${response.status}`
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
const body = await response.json();
|
|
1183
|
-
const spendable = BigInt(body.budget?.spendableYieldRawUsdc ?? "0");
|
|
1184
|
-
if (spendable < shortfallRawUsdc) {
|
|
1185
|
-
throw new RelayerRealizeError(
|
|
1186
|
-
"insufficient_yield",
|
|
1187
|
-
`spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC; the principal is never spent \u2014 wait for more yield`,
|
|
1188
|
-
{ spendableYieldRawUsdc: spendable.toString() }
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
async postJson(path, body) {
|
|
1193
|
-
const url = `${this.config.facilitatorBaseUrl}${path}`;
|
|
1194
|
-
const serialized = JSON.stringify(body);
|
|
1195
|
-
const response = await this.fetchImpl(url, {
|
|
1196
|
-
method: "POST",
|
|
1197
|
-
headers: {
|
|
1198
|
-
...await walletAuthHeaders({
|
|
1199
|
-
signer: this.signer,
|
|
1200
|
-
method: "POST",
|
|
1201
|
-
url,
|
|
1202
|
-
body: serialized
|
|
1203
|
-
}),
|
|
1204
|
-
"content-type": "application/json"
|
|
1205
|
-
},
|
|
1206
|
-
body: serialized
|
|
1207
|
-
});
|
|
1208
|
-
const text = await response.text();
|
|
1209
|
-
if (response.status !== 200) {
|
|
1210
|
-
throw new RelayerRealizeError(
|
|
1211
|
-
path.endsWith("/submit") ? "submit_failed" : "prepare_failed",
|
|
1212
|
-
`${path} failed with ${response.status}: ${text}`
|
|
1213
|
-
);
|
|
1214
|
-
}
|
|
1215
|
-
return JSON.parse(text);
|
|
1216
|
-
}
|
|
1217
|
-
async readTokenBalance(ata) {
|
|
1218
|
-
try {
|
|
1219
|
-
const response = await this.rpc.getTokenAccountBalance(address2(ata), { commitment: "confirmed" }).send();
|
|
1220
|
-
return BigInt(response.value.amount);
|
|
1221
|
-
} catch {
|
|
1222
|
-
return 0n;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
};
|
|
1226
|
-
|
|
1227
|
-
// src/x402/headers.ts
|
|
1052
|
+
// ../../src/x402/headers.ts
|
|
1228
1053
|
import { z } from "zod";
|
|
1229
1054
|
var PAYMENT_REQUIRED_HEADER = "payment-required";
|
|
1230
1055
|
var MAX_HEADER_JSON_BYTES = 16384;
|
|
@@ -1290,8 +1115,26 @@ function decodeX402Header(headerValue) {
|
|
|
1290
1115
|
);
|
|
1291
1116
|
}
|
|
1292
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
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// ../../src/client/paid-fetch.ts
|
|
1128
|
+
function formatRawUsdcAmount(raw) {
|
|
1129
|
+
const value = BigInt(raw);
|
|
1130
|
+
const negative = value < 0n;
|
|
1131
|
+
const abs = negative ? -value : value;
|
|
1132
|
+
const whole = abs / 1000000n;
|
|
1133
|
+
const frac = (abs % 1000000n).toString().padStart(6, "0");
|
|
1134
|
+
return `${negative ? "-" : ""}${whole}.${frac}`;
|
|
1135
|
+
}
|
|
1293
1136
|
|
|
1294
|
-
// src/x402/standard-requirements.ts
|
|
1137
|
+
// ../../src/x402/standard-requirements.ts
|
|
1295
1138
|
import { z as z2 } from "zod";
|
|
1296
1139
|
var STANDARD_EXACT_SCHEME = "exact";
|
|
1297
1140
|
var standardExactRequirementSchema = z2.object({
|
|
@@ -1372,8 +1215,26 @@ function selectPayableSolanaRequirement(requirements, options) {
|
|
|
1372
1215
|
feePayer: requirement.extra?.feePayer ?? null
|
|
1373
1216
|
};
|
|
1374
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
|
+
}
|
|
1375
1236
|
|
|
1376
|
-
// src/client/standard-x402-payer.ts
|
|
1237
|
+
// ../../src/client/standard-x402-payer.ts
|
|
1377
1238
|
var StandardX402PayError = class extends Error {
|
|
1378
1239
|
constructor(reason, message, detail = null) {
|
|
1379
1240
|
super(message);
|
|
@@ -1391,6 +1252,10 @@ var StandardX402Payer = class {
|
|
|
1391
1252
|
defaultMaxAmountRawUsdc;
|
|
1392
1253
|
network;
|
|
1393
1254
|
usdcMint;
|
|
1255
|
+
stateStore;
|
|
1256
|
+
pending = /* @__PURE__ */ new Map();
|
|
1257
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
1258
|
+
nowMs;
|
|
1394
1259
|
constructor(config) {
|
|
1395
1260
|
this.realizer = config.realizer;
|
|
1396
1261
|
this.x402Fetch = config.x402Fetch;
|
|
@@ -1398,10 +1263,59 @@ var StandardX402Payer = class {
|
|
|
1398
1263
|
this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
|
|
1399
1264
|
this.network = config.network ?? SOLANA_MAINNET_NETWORK;
|
|
1400
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
|
+
}
|
|
1273
|
+
}
|
|
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;
|
|
1401
1295
|
}
|
|
1402
|
-
async
|
|
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
|
+
}
|
|
1403
1317
|
const init = {
|
|
1404
|
-
method
|
|
1318
|
+
method,
|
|
1405
1319
|
...input.body === void 0 ? {} : { body: input.body },
|
|
1406
1320
|
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
1407
1321
|
};
|
|
@@ -1430,15 +1344,55 @@ var StandardX402Payer = class {
|
|
|
1430
1344
|
error
|
|
1431
1345
|
);
|
|
1432
1346
|
}
|
|
1433
|
-
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
|
+
}
|
|
1434
1383
|
const bodyText = await response.text();
|
|
1435
1384
|
if (response.status !== 200) {
|
|
1385
|
+
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1386
|
+
status: response.status,
|
|
1387
|
+
body: bodyText
|
|
1388
|
+
});
|
|
1436
1389
|
throw new StandardX402PayError(
|
|
1437
|
-
"
|
|
1438
|
-
`the x402 payment
|
|
1439
|
-
{ 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 }
|
|
1440
1393
|
);
|
|
1441
1394
|
}
|
|
1395
|
+
this.clearDelivered(pendingKey);
|
|
1442
1396
|
return {
|
|
1443
1397
|
paid: true,
|
|
1444
1398
|
status: response.status,
|
|
@@ -1484,260 +1438,971 @@ var StandardX402Payer = class {
|
|
|
1484
1438
|
);
|
|
1485
1439
|
}
|
|
1486
1440
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|
+
}
|
|
1494
1454
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
tx.sign([keypair]);
|
|
1500
|
-
return tx;
|
|
1455
|
+
markUnknown(key, detail) {
|
|
1456
|
+
const current = this.pending.get(key);
|
|
1457
|
+
if (current === void 0) {
|
|
1458
|
+
return;
|
|
1501
1459
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
network: config.network ?? "solana",
|
|
1515
|
-
rpcUrl: config.rpcUrl
|
|
1516
|
-
});
|
|
1517
|
-
const x402Fetch = (url, init) => client.fetch(url, init);
|
|
1518
|
-
return new StandardX402Payer({
|
|
1519
|
-
realizer,
|
|
1520
|
-
x402Fetch,
|
|
1521
|
-
defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
// src/client/paid-fetch.ts
|
|
1526
|
-
function formatRawUsdcAmount(raw) {
|
|
1527
|
-
const value = BigInt(raw);
|
|
1528
|
-
const negative = value < 0n;
|
|
1529
|
-
const abs = negative ? -value : value;
|
|
1530
|
-
const whole = abs / 1000000n;
|
|
1531
|
-
const frac = (abs % 1000000n).toString().padStart(6, "0");
|
|
1532
|
-
return `${negative ? "-" : ""}${whole}.${frac}`;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
// src/solana/keys.ts
|
|
1536
|
-
import { readFileSync } from "node:fs";
|
|
1537
|
-
import bs586 from "bs58";
|
|
1538
|
-
import {
|
|
1539
|
-
createKeyPairSignerFromBytes
|
|
1540
|
-
} from "@solana/kit";
|
|
1541
|
-
async function loadKeyPairSigner(params) {
|
|
1542
|
-
const { base58Secret, jsonFilePath, label } = params;
|
|
1543
|
-
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
1544
|
-
const bytes = bs586.decode(base58Secret);
|
|
1545
|
-
if (bytes.length !== 64) {
|
|
1546
|
-
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
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;
|
|
1547
1472
|
}
|
|
1548
|
-
return createKeyPairSignerFromBytes(bytes);
|
|
1549
1473
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1474
|
+
tryMarkUnknown(key, detail) {
|
|
1475
|
+
try {
|
|
1476
|
+
this.markUnknown(key, detail);
|
|
1477
|
+
return null;
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
return error;
|
|
1554
1480
|
}
|
|
1555
|
-
return createKeyPairSignerFromBytes(Uint8Array.from(raw));
|
|
1556
1481
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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
|
+
}
|
|
1567
1494
|
}
|
|
1568
|
-
|
|
1569
|
-
const
|
|
1570
|
-
|
|
1571
|
-
|
|
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
|
+
);
|
|
1572
1507
|
}
|
|
1573
|
-
return Uint8Array.from(raw);
|
|
1574
1508
|
}
|
|
1575
|
-
|
|
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}`;
|
|
1576
1518
|
}
|
|
1577
1519
|
|
|
1578
|
-
// src/
|
|
1579
|
-
import {
|
|
1580
|
-
|
|
1581
|
-
|
|
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;
|
|
1582
1558
|
}
|
|
1583
1559
|
|
|
1584
|
-
//
|
|
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.facilitatorBaseUrl.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
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
|
|
1776
|
+
// ../../src/client/mcp-payment-server.ts
|
|
1585
1777
|
var TOOL_NAME = "fetch_with_subly_payment";
|
|
1586
|
-
var
|
|
1587
|
-
var
|
|
1588
|
-
var
|
|
1589
|
-
var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? DEFAULT_MAX_AMOUNT_RAW_USDC : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
|
|
1590
|
-
var keyPairSigner = await loadKeyPairSigner({
|
|
1591
|
-
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1592
|
-
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1593
|
-
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1594
|
-
});
|
|
1595
|
-
var agentSecretKey = loadSecretKeyBytes({
|
|
1596
|
-
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1597
|
-
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1598
|
-
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1599
|
-
});
|
|
1600
|
-
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
1601
|
-
var rpc = createRpc(rpcUrl);
|
|
1602
|
-
var payer = createStandardX402Payer({
|
|
1603
|
-
facilitatorBaseUrl,
|
|
1604
|
-
signer,
|
|
1605
|
-
agentSecretKey,
|
|
1606
|
-
rpc,
|
|
1607
|
-
rpcUrl,
|
|
1608
|
-
defaultMaxAmountRawUsdc
|
|
1609
|
-
});
|
|
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";
|
|
1610
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.
|
|
1611
1782
|
|
|
1612
|
-
|
|
1613
|
-
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.
|
|
1614
|
-
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.
|
|
1615
|
-
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.
|
|
1616
1784
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
)
|
|
1622
|
-
|
|
1623
|
-
|
|
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 1 USDC = 1000000 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.`;
|
|
1790
|
+
async function runMcpPaymentServer(config) {
|
|
1791
|
+
const { payer: payer2, signer: signer2, facilitatorBaseUrl, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
|
|
1792
|
+
const vaultFlows = config.vaultFlows ?? null;
|
|
1793
|
+
const server = new Server(
|
|
1794
|
+
{ name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
|
|
1795
|
+
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
1796
|
+
);
|
|
1797
|
+
const vaultTools = vaultFlows === null ? [] : [
|
|
1624
1798
|
{
|
|
1625
|
-
name:
|
|
1626
|
-
description:
|
|
1627
|
-
defaultMaxAmountRawUsdc
|
|
1628
|
-
)} USDC) are refused without paying. Payments are refused when the spendable yield budget cannot cover them \u2014 the principal is never spent. Use only for URLs you intend to purchase access to.`,
|
|
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 deposit is 1 USDC (1000000 raw). The deposited amount becomes protected principal: payments can only ever spend the yield on top of it.",
|
|
1629
1801
|
inputSchema: {
|
|
1630
1802
|
type: "object",
|
|
1631
1803
|
properties: {
|
|
1632
|
-
|
|
1804
|
+
amountRawUsdc: {
|
|
1633
1805
|
type: "string",
|
|
1634
|
-
description:
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1806
|
+
description: 'Amount to deposit in raw USDC units (6 decimals, e.g. "1000000" = 1 USDC). Must be at least 1000000.'
|
|
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: {
|
|
1637
1826
|
type: "string",
|
|
1638
|
-
description: '
|
|
1827
|
+
description: 'Amount to withdraw in raw USDC units (6 decimals, e.g. "1000000" = 1 USDC).'
|
|
1639
1828
|
}
|
|
1640
1829
|
},
|
|
1641
|
-
required: ["
|
|
1830
|
+
required: ["amountRawUsdc"]
|
|
1642
1831
|
},
|
|
1643
1832
|
annotations: {
|
|
1644
|
-
title: "
|
|
1833
|
+
title: "Withdraw from the Subly vault",
|
|
1645
1834
|
readOnlyHint: false,
|
|
1646
1835
|
destructiveHint: true,
|
|
1647
1836
|
idempotentHint: false,
|
|
1648
|
-
openWorldHint:
|
|
1837
|
+
openWorldHint: false
|
|
1649
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
|
+
];
|
|
1853
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1854
|
+
tools: [
|
|
1855
|
+
...vaultTools,
|
|
1856
|
+
{
|
|
1857
|
+
name: TOOL_NAME,
|
|
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(
|
|
1859
|
+
defaultMaxAmountRawUsdc2
|
|
1860
|
+
)} USDC) are refused without paying. Payments are refused when the spendable yield budget cannot cover them \u2014 the principal is never spent. Use only for URLs you intend to purchase access to.`,
|
|
1861
|
+
inputSchema: {
|
|
1862
|
+
type: "object",
|
|
1863
|
+
properties: {
|
|
1864
|
+
url: {
|
|
1865
|
+
type: "string",
|
|
1866
|
+
description: "URL to fetch. Must match the seller's resource URL exactly."
|
|
1867
|
+
},
|
|
1868
|
+
method: {
|
|
1869
|
+
type: "string",
|
|
1870
|
+
description: "HTTP method (default GET). Some x402 sellers deliver the paid resource over POST (e.g. an API that takes a JSON body)."
|
|
1871
|
+
},
|
|
1872
|
+
body: {
|
|
1873
|
+
type: "string",
|
|
1874
|
+
description: "Request body sent on both the probe and the paid retry. Provide a JSON string for POST sellers; sent as content-type application/json unless headers override it."
|
|
1875
|
+
},
|
|
1876
|
+
headers: {
|
|
1877
|
+
type: "object",
|
|
1878
|
+
description: "Extra request headers (object of string values), merged into both the probe and the paid retry.",
|
|
1879
|
+
additionalProperties: { type: "string" }
|
|
1880
|
+
},
|
|
1881
|
+
maxAmountRawUsdc: {
|
|
1882
|
+
type: "string",
|
|
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."
|
|
1888
|
+
}
|
|
1889
|
+
},
|
|
1890
|
+
required: ["url"]
|
|
1891
|
+
},
|
|
1892
|
+
annotations: {
|
|
1893
|
+
title: "Fetch with Subly payment",
|
|
1894
|
+
readOnlyHint: false,
|
|
1895
|
+
destructiveHint: true,
|
|
1896
|
+
idempotentHint: false,
|
|
1897
|
+
openWorldHint: true
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
]
|
|
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
|
+
);
|
|
1650
1912
|
}
|
|
1651
|
-
]
|
|
1652
|
-
}));
|
|
1653
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1654
|
-
if (request.params.name !== TOOL_NAME) {
|
|
1655
1913
|
return {
|
|
1656
1914
|
content: [
|
|
1657
|
-
{
|
|
1915
|
+
{
|
|
1916
|
+
type: "text",
|
|
1917
|
+
text: error instanceof Error ? error.message : String(error)
|
|
1918
|
+
}
|
|
1658
1919
|
],
|
|
1659
1920
|
isError: true
|
|
1660
1921
|
};
|
|
1661
|
-
}
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
content: [{ type: "text", text: "missing required argument: url" }],
|
|
1667
|
-
isError: true
|
|
1668
|
-
};
|
|
1669
|
-
}
|
|
1670
|
-
let maxAmountRawUsdc;
|
|
1671
|
-
if (args.maxAmountRawUsdc !== void 0) {
|
|
1672
|
-
const raw = args.maxAmountRawUsdc;
|
|
1922
|
+
};
|
|
1923
|
+
const parseRawAmount = (value) => {
|
|
1924
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1673
1927
|
try {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
}
|
|
1677
|
-
maxAmountRawUsdc = BigInt(raw);
|
|
1928
|
+
const amount = BigInt(value);
|
|
1929
|
+
return amount > 0n ? amount : null;
|
|
1678
1930
|
} catch {
|
|
1679
|
-
return
|
|
1680
|
-
|
|
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
|
+
};
|
|
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({ facilitatorBaseUrl, 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(
|
|
1681
1988
|
{
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
}
|
|
1685
|
-
|
|
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
|
+
}
|
|
2014
|
+
if (request.params.name !== TOOL_NAME) {
|
|
2015
|
+
return {
|
|
2016
|
+
content: [{ type: "text", text: `unknown tool: ${request.params.name}` }],
|
|
1686
2017
|
isError: true
|
|
1687
2018
|
};
|
|
1688
2019
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
2020
|
+
const args = request.params.arguments ?? {};
|
|
2021
|
+
const url = args.url;
|
|
2022
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
2023
|
+
return {
|
|
2024
|
+
content: [{ type: "text", text: "missing required argument: url" }],
|
|
2025
|
+
isError: true
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
let maxAmountRawUsdc;
|
|
2029
|
+
if (args.maxAmountRawUsdc !== void 0) {
|
|
2030
|
+
const raw = args.maxAmountRawUsdc;
|
|
2031
|
+
try {
|
|
2032
|
+
if (typeof raw !== "string" && typeof raw !== "number") {
|
|
2033
|
+
throw new TypeError("not a string or number");
|
|
2034
|
+
}
|
|
2035
|
+
maxAmountRawUsdc = BigInt(raw);
|
|
2036
|
+
} catch {
|
|
2037
|
+
return {
|
|
2038
|
+
content: [
|
|
2039
|
+
{
|
|
2040
|
+
type: "text",
|
|
2041
|
+
text: "maxAmountRawUsdc must be an integer raw USDC amount"
|
|
2042
|
+
}
|
|
2043
|
+
],
|
|
2044
|
+
isError: true
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const method = typeof args.method === "string" ? args.method : void 0;
|
|
2049
|
+
const body = typeof args.body === "string" ? args.body : void 0;
|
|
2050
|
+
const forceNewPayment = args.forceNewPayment === true;
|
|
2051
|
+
const headers = args.headers !== null && typeof args.headers === "object" && !Array.isArray(args.headers) ? Object.fromEntries(
|
|
2052
|
+
Object.entries(args.headers).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])
|
|
2053
|
+
) : void 0;
|
|
2054
|
+
const mergedHeaders = body === void 0 ? headers : { "content-type": "application/json", ...headers ?? {} };
|
|
2055
|
+
try {
|
|
2056
|
+
const result = await payer2.pay({
|
|
2057
|
+
url,
|
|
2058
|
+
...method === void 0 ? {} : { method },
|
|
2059
|
+
...body === void 0 ? {} : { body },
|
|
2060
|
+
...mergedHeaders === void 0 ? {} : { headers: mergedHeaders },
|
|
2061
|
+
...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
|
|
2062
|
+
...forceNewPayment ? { forceNewPayment } : {}
|
|
2063
|
+
});
|
|
2064
|
+
return {
|
|
2065
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2066
|
+
};
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
if (error instanceof StandardX402PayError) {
|
|
2069
|
+
return {
|
|
2070
|
+
content: [
|
|
2071
|
+
{
|
|
2072
|
+
type: "text",
|
|
2073
|
+
text: JSON.stringify(
|
|
2074
|
+
{
|
|
2075
|
+
paid: false,
|
|
2076
|
+
refused: true,
|
|
2077
|
+
reason: error.reason,
|
|
2078
|
+
message: error.message,
|
|
2079
|
+
detail: error.detail
|
|
2080
|
+
},
|
|
2081
|
+
null,
|
|
2082
|
+
2
|
|
2083
|
+
)
|
|
2084
|
+
}
|
|
2085
|
+
],
|
|
2086
|
+
isError: true
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
1700
2089
|
return {
|
|
1701
2090
|
content: [
|
|
1702
2091
|
{
|
|
1703
2092
|
type: "text",
|
|
1704
|
-
text:
|
|
1705
|
-
{
|
|
1706
|
-
paid: false,
|
|
1707
|
-
refused: true,
|
|
1708
|
-
reason: error.reason,
|
|
1709
|
-
message: error.message,
|
|
1710
|
-
detail: error.detail
|
|
1711
|
-
},
|
|
1712
|
-
null,
|
|
1713
|
-
2
|
|
1714
|
-
)
|
|
2093
|
+
text: error instanceof Error ? error.message : String(error)
|
|
1715
2094
|
}
|
|
1716
2095
|
],
|
|
1717
2096
|
isError: true
|
|
1718
2097
|
};
|
|
1719
2098
|
}
|
|
2099
|
+
});
|
|
2100
|
+
try {
|
|
2101
|
+
await ensureWalletOnboarded({ facilitatorBaseUrl, signer: signer2 });
|
|
2102
|
+
console.error("[subly-mcp] wallet registered and synced at the relayer");
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
console.error(
|
|
2105
|
+
`[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
const transport = new StdioServerTransport();
|
|
2109
|
+
await server.connect(transport);
|
|
2110
|
+
console.error(
|
|
2111
|
+
`[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc2)} USDC`
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// ../../src/client/relayer-yield-realizer.ts
|
|
2116
|
+
var REALIZE_OVERHEAD_RAW_USDC = 2500n;
|
|
2117
|
+
var RelayerRealizeError = class extends Error {
|
|
2118
|
+
constructor(code, message, detail = null) {
|
|
2119
|
+
super(message);
|
|
2120
|
+
this.code = code;
|
|
2121
|
+
this.detail = detail;
|
|
2122
|
+
this.name = "RelayerRealizeError";
|
|
2123
|
+
}
|
|
2124
|
+
code;
|
|
2125
|
+
detail;
|
|
2126
|
+
};
|
|
2127
|
+
var RelayerYieldRealizer = class {
|
|
2128
|
+
vaultFlows;
|
|
2129
|
+
constructor(config) {
|
|
2130
|
+
this.vaultFlows = new VaultFlowClient({
|
|
2131
|
+
facilitatorBaseUrl: config.facilitatorBaseUrl,
|
|
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
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
async ensureUsdcAvailable(input) {
|
|
2139
|
+
const shortfallRawUsdc = input.amountRawUsdc;
|
|
2140
|
+
await this.assertSpendableYield(shortfallRawUsdc);
|
|
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);
|
|
2151
|
+
}
|
|
2152
|
+
if (outcome.status !== "confirmed" || outcome.txSignature === null) {
|
|
2153
|
+
throw new RelayerRealizeError(
|
|
2154
|
+
"realize_not_confirmed",
|
|
2155
|
+
`yield realize withdrawal did not confirm (status=${outcome.status})`,
|
|
2156
|
+
outcome
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
1720
2159
|
return {
|
|
1721
|
-
|
|
2160
|
+
realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
|
|
2161
|
+
txSignature: outcome.txSignature
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
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
|
+
*/
|
|
2169
|
+
async assertSpendableYield(shortfallRawUsdc) {
|
|
2170
|
+
let spendable;
|
|
2171
|
+
try {
|
|
2172
|
+
const budget = await this.vaultFlows.getBudget();
|
|
2173
|
+
spendable = BigInt(budget.spendableYieldRawUsdc);
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
throw new RelayerRealizeError(
|
|
2176
|
+
"budget_unavailable",
|
|
2177
|
+
"could not read the spendable-yield budget",
|
|
2178
|
+
error
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
|
|
2182
|
+
if (spendable < requiredRawUsdc) {
|
|
2183
|
+
throw new RelayerRealizeError(
|
|
2184
|
+
"insufficient_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`,
|
|
2186
|
+
{ spendableYieldRawUsdc: spendable.toString() }
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
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
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
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
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
return new RelayerRealizeError(
|
|
2207
|
+
error.step === "submit" ? "submit_failed" : "prepare_failed",
|
|
2208
|
+
error.message,
|
|
2209
|
+
error.detail
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
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
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// ../../src/client/relayer-payer.ts
|
|
2226
|
+
function createRelayerX402Payer(config) {
|
|
2227
|
+
const realizer = new RelayerYieldRealizer({
|
|
2228
|
+
facilitatorBaseUrl: config.facilitatorBaseUrl,
|
|
2229
|
+
signer: config.signer,
|
|
2230
|
+
rpc: config.rpc
|
|
2231
|
+
});
|
|
2232
|
+
return new StandardX402Payer({
|
|
2233
|
+
realizer,
|
|
2234
|
+
x402Fetch: config.x402Fetch,
|
|
2235
|
+
defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc,
|
|
2236
|
+
...config.stateStore === void 0 ? {} : { stateStore: config.stateStore }
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
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
|
+
|
|
2291
|
+
// ../../src/solana/keys.ts
|
|
2292
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
2293
|
+
import bs586 from "bs58";
|
|
2294
|
+
import {
|
|
2295
|
+
createKeyPairSignerFromBytes
|
|
2296
|
+
} from "@solana/kit";
|
|
2297
|
+
async function loadKeyPairSigner(params) {
|
|
2298
|
+
const { base58Secret, jsonFilePath, label } = params;
|
|
2299
|
+
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
2300
|
+
const bytes = bs586.decode(base58Secret);
|
|
2301
|
+
if (bytes.length !== 64) {
|
|
2302
|
+
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
2303
|
+
}
|
|
2304
|
+
return createKeyPairSignerFromBytes(bytes);
|
|
2305
|
+
}
|
|
2306
|
+
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
2307
|
+
const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
|
|
2308
|
+
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
2309
|
+
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
2310
|
+
}
|
|
2311
|
+
return createKeyPairSignerFromBytes(Uint8Array.from(raw));
|
|
2312
|
+
}
|
|
2313
|
+
throw new Error(`${label} keypair is not configured`);
|
|
2314
|
+
}
|
|
2315
|
+
function loadSecretKeyBytes(params) {
|
|
2316
|
+
const { base58Secret, jsonFilePath, label } = params;
|
|
2317
|
+
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
2318
|
+
const bytes = bs586.decode(base58Secret);
|
|
2319
|
+
if (bytes.length !== 64) {
|
|
2320
|
+
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
2321
|
+
}
|
|
2322
|
+
return bytes;
|
|
2323
|
+
}
|
|
2324
|
+
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
2325
|
+
const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
|
|
2326
|
+
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
2327
|
+
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
2328
|
+
}
|
|
2329
|
+
return Uint8Array.from(raw);
|
|
2330
|
+
}
|
|
2331
|
+
throw new Error(`${label} keypair is not configured`);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// ../../src/solana/rpc.ts
|
|
2335
|
+
import { createSolanaRpc } from "@solana/kit";
|
|
2336
|
+
function createRpc(url) {
|
|
2337
|
+
return createSolanaRpc(url);
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// src/svm-x402-fetch.ts
|
|
2341
|
+
import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
|
|
2342
|
+
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
|
|
2343
|
+
import { ExactSvmScheme, toClientSvmSigner } from "@x402/svm";
|
|
2344
|
+
async function createSvmX402Fetch(params) {
|
|
2345
|
+
const signer2 = toClientSvmSigner(
|
|
2346
|
+
await createKeyPairSignerFromBytes2(params.agentSecretKey)
|
|
2347
|
+
);
|
|
2348
|
+
return (url, init, expected) => {
|
|
2349
|
+
const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
|
|
2350
|
+
schemes: [
|
|
1722
2351
|
{
|
|
1723
|
-
|
|
1724
|
-
|
|
2352
|
+
network: expected.requirement.network,
|
|
2353
|
+
client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
|
|
1725
2354
|
}
|
|
1726
2355
|
],
|
|
1727
|
-
|
|
1728
|
-
};
|
|
1729
|
-
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
console.error(
|
|
1736
|
-
`[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
|
|
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)
|
|
1737
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;
|
|
1738
2371
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
);
|
|
2372
|
+
|
|
2373
|
+
// src/mcp-server.ts
|
|
2374
|
+
var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
|
|
2375
|
+
var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
|
|
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");
|
|
2378
|
+
var keyPairSigner = await loadKeyPairSigner({
|
|
2379
|
+
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
2380
|
+
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
2381
|
+
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
2382
|
+
});
|
|
2383
|
+
var agentSecretKey = loadSecretKeyBytes({
|
|
2384
|
+
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
2385
|
+
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
2386
|
+
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
2387
|
+
});
|
|
2388
|
+
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
2389
|
+
var rpc = createRpc(rpcUrl);
|
|
2390
|
+
var payer = createRelayerX402Payer({
|
|
2391
|
+
facilitatorBaseUrl: relayerBaseUrl,
|
|
2392
|
+
signer,
|
|
2393
|
+
rpc,
|
|
2394
|
+
x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
|
|
2395
|
+
defaultMaxAmountRawUsdc,
|
|
2396
|
+
stateStore: fileStandardX402StateStore(pendingStatePath)
|
|
2397
|
+
});
|
|
2398
|
+
await runMcpPaymentServer({
|
|
2399
|
+
payer,
|
|
2400
|
+
signer,
|
|
2401
|
+
facilitatorBaseUrl: relayerBaseUrl,
|
|
2402
|
+
defaultMaxAmountRawUsdc,
|
|
2403
|
+
vaultFlows: new VaultFlowClient({
|
|
2404
|
+
facilitatorBaseUrl: relayerBaseUrl,
|
|
2405
|
+
signer,
|
|
2406
|
+
rpc
|
|
2407
|
+
})
|
|
2408
|
+
});
|