@subly_fi/pay 0.1.1 → 0.3.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/dist/mcp-server.js +678 -771
- package/dist/pay.js +456 -556
- package/package.json +10 -6
package/dist/mcp-server.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
// ../../demo/mcp-server.ts
|
|
2
|
-
import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
3
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import {
|
|
6
|
-
CallToolRequestSchema,
|
|
7
|
-
ListToolsRequestSchema
|
|
8
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
|
|
10
1
|
// ../../src/client/agent-wallet-signer.ts
|
|
11
2
|
import { signBytes } from "@solana/kit";
|
|
12
3
|
import bs584 from "bs58";
|
|
@@ -28,7 +19,6 @@ import {
|
|
|
28
19
|
|
|
29
20
|
// ../../src/lib/hash.ts
|
|
30
21
|
import { createHash } from "node:crypto";
|
|
31
|
-
var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
32
22
|
function sha256TaggedHex(data) {
|
|
33
23
|
return `sha256-${createHash("sha256").update(data).digest("hex")}`;
|
|
34
24
|
}
|
|
@@ -125,12 +115,12 @@ function deriveAssociatedTokenAddress(params) {
|
|
|
125
115
|
"associatedTokenProgramId"
|
|
126
116
|
);
|
|
127
117
|
for (let bump = 255; bump >= 0; bump -= 1) {
|
|
128
|
-
const
|
|
118
|
+
const address3 = createProgramAddress(
|
|
129
119
|
[owner, tokenProgramId, mint, Uint8Array.of(bump)],
|
|
130
120
|
associatedTokenProgramId
|
|
131
121
|
);
|
|
132
|
-
if (
|
|
133
|
-
return bs582.encode(
|
|
122
|
+
if (address3 !== null) {
|
|
123
|
+
return bs582.encode(address3);
|
|
134
124
|
}
|
|
135
125
|
}
|
|
136
126
|
throw new Error("Unable to derive associated token account address");
|
|
@@ -955,52 +945,108 @@ var LocalKeypairAgentWalletSigner = class {
|
|
|
955
945
|
}
|
|
956
946
|
};
|
|
957
947
|
|
|
958
|
-
// ../../src/client/
|
|
959
|
-
import {
|
|
960
|
-
import {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
976
|
-
const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
|
|
977
|
-
const lookups = compiled.addressTableLookups ?? [];
|
|
978
|
-
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
948
|
+
// ../../src/client/mcp-payment-server.ts
|
|
949
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
950
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
951
|
+
import {
|
|
952
|
+
CallToolRequestSchema,
|
|
953
|
+
ListToolsRequestSchema
|
|
954
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
955
|
+
|
|
956
|
+
// ../../src/api/wallet-auth.ts
|
|
957
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
958
|
+
import bs585 from "bs58";
|
|
959
|
+
import nacl from "tweetnacl";
|
|
960
|
+
var WALLET_AUTH_WALLET_HEADER = "x-subly-wallet";
|
|
961
|
+
var WALLET_AUTH_SIGNED_AT_HEADER = "x-subly-signed-at";
|
|
962
|
+
var WALLET_AUTH_SIGNATURE_HEADER = "x-subly-signature";
|
|
963
|
+
function sha256Hex(data) {
|
|
964
|
+
return createHash3("sha256").update(data, "utf8").digest("hex");
|
|
979
965
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const tables = await fetchAllMaybeAddressLookupTable(
|
|
986
|
-
rpc2,
|
|
987
|
-
addresses.map((value) => address(value))
|
|
966
|
+
function walletAuthMessage(params) {
|
|
967
|
+
return new TextEncoder().encode(
|
|
968
|
+
`subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
|
|
969
|
+
params.rawBody
|
|
970
|
+
)}:${params.signedAtMs}`
|
|
988
971
|
);
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ../../src/client/wallet-auth-headers.ts
|
|
975
|
+
async function walletAuthHeaders(params) {
|
|
976
|
+
const signedAtMs = String(Date.now());
|
|
977
|
+
const message = walletAuthMessage({
|
|
978
|
+
method: params.method,
|
|
979
|
+
path: new URL(params.url).pathname,
|
|
980
|
+
rawBody: params.body ?? "",
|
|
981
|
+
signedAtMs
|
|
982
|
+
});
|
|
983
|
+
return {
|
|
984
|
+
[WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
|
|
985
|
+
[WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
|
|
986
|
+
[WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// ../../src/client/onboarding.ts
|
|
991
|
+
var SELF_SERVE_POLICY_ID = "self-serve";
|
|
992
|
+
var OnboardingError = class extends Error {
|
|
993
|
+
constructor(step, message, detail = null) {
|
|
994
|
+
super(message);
|
|
995
|
+
this.step = step;
|
|
996
|
+
this.detail = detail;
|
|
997
|
+
this.name = "OnboardingError";
|
|
994
998
|
}
|
|
995
|
-
|
|
999
|
+
step;
|
|
1000
|
+
detail;
|
|
1001
|
+
};
|
|
1002
|
+
async function ensureWalletOnboarded(params) {
|
|
1003
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
1004
|
+
const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
|
|
1005
|
+
const post = async (step, path, body) => {
|
|
1006
|
+
const url = `${baseUrl}${path}`;
|
|
1007
|
+
const serialized = JSON.stringify(body);
|
|
1008
|
+
const response = await fetchImpl(url, {
|
|
1009
|
+
method: "POST",
|
|
1010
|
+
headers: {
|
|
1011
|
+
...await walletAuthHeaders({
|
|
1012
|
+
signer: params.signer,
|
|
1013
|
+
method: "POST",
|
|
1014
|
+
url,
|
|
1015
|
+
body: serialized
|
|
1016
|
+
}),
|
|
1017
|
+
"content-type": "application/json"
|
|
1018
|
+
},
|
|
1019
|
+
body: serialized
|
|
1020
|
+
});
|
|
1021
|
+
if (response.status !== 200) {
|
|
1022
|
+
let detail = null;
|
|
1023
|
+
try {
|
|
1024
|
+
detail = await response.json();
|
|
1025
|
+
} catch {
|
|
1026
|
+
detail = null;
|
|
1027
|
+
}
|
|
1028
|
+
throw new OnboardingError(
|
|
1029
|
+
step,
|
|
1030
|
+
`wallet onboarding ${step} failed with ${response.status}`,
|
|
1031
|
+
detail
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
const wallet = params.signer.walletAddress;
|
|
1036
|
+
await post("register", "/v1/wallets/agent", {
|
|
1037
|
+
wallet,
|
|
1038
|
+
signingPolicyId: SELF_SERVE_POLICY_ID,
|
|
1039
|
+
signingMode: "non_interactive",
|
|
1040
|
+
signerValidationMode: params.signer.validationMode,
|
|
1041
|
+
signerProvider: "local-keypair",
|
|
1042
|
+
activateForPayments: true
|
|
1043
|
+
});
|
|
1044
|
+
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
|
|
996
1045
|
}
|
|
997
1046
|
|
|
998
1047
|
// ../../src/x402/headers.ts
|
|
999
1048
|
import { z } from "zod";
|
|
1000
1049
|
var PAYMENT_REQUIRED_HEADER = "payment-required";
|
|
1001
|
-
var PAYMENT_SIGNATURE_HEADER = "payment-signature";
|
|
1002
|
-
var PAYMENT_RESPONSE_HEADER = "payment-response";
|
|
1003
|
-
var X402_VERSION = 2;
|
|
1004
1050
|
var MAX_HEADER_JSON_BYTES = 16384;
|
|
1005
1051
|
var X402HeaderError = class extends Error {
|
|
1006
1052
|
reason;
|
|
@@ -1047,16 +1093,6 @@ var sublyPaymentPayloadSchema = z.object({
|
|
|
1047
1093
|
temporarySettlementSignature: z.string().min(1).max(128)
|
|
1048
1094
|
})
|
|
1049
1095
|
}).loose();
|
|
1050
|
-
function encodeX402Header(value) {
|
|
1051
|
-
const json = JSON.stringify(value);
|
|
1052
|
-
if (Buffer.byteLength(json, "utf8") > MAX_HEADER_JSON_BYTES) {
|
|
1053
|
-
throw new X402HeaderError(
|
|
1054
|
-
"header_too_large",
|
|
1055
|
-
`x402 header JSON exceeds ${MAX_HEADER_JSON_BYTES} bytes`
|
|
1056
|
-
);
|
|
1057
|
-
}
|
|
1058
|
-
return Buffer.from(json, "utf8").toString("base64");
|
|
1059
|
-
}
|
|
1060
1096
|
function decodeX402Header(headerValue) {
|
|
1061
1097
|
if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
|
|
1062
1098
|
throw new X402HeaderError(
|
|
@@ -1074,340 +1110,562 @@ function decodeX402Header(headerValue) {
|
|
|
1074
1110
|
);
|
|
1075
1111
|
}
|
|
1076
1112
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1113
|
+
|
|
1114
|
+
// ../../src/client/paid-fetch.ts
|
|
1115
|
+
function formatRawUsdcAmount(raw) {
|
|
1116
|
+
const value = BigInt(raw);
|
|
1117
|
+
const negative = value < 0n;
|
|
1118
|
+
const abs = negative ? -value : value;
|
|
1119
|
+
const whole = abs / 1000000n;
|
|
1120
|
+
const frac = (abs % 1000000n).toString().padStart(6, "0");
|
|
1121
|
+
return `${negative ? "-" : ""}${whole}.${frac}`;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// ../../src/x402/standard-requirements.ts
|
|
1125
|
+
import { z as z2 } from "zod";
|
|
1126
|
+
var STANDARD_EXACT_SCHEME = "exact";
|
|
1127
|
+
var standardExactRequirementSchema = z2.object({
|
|
1128
|
+
scheme: z2.literal(STANDARD_EXACT_SCHEME),
|
|
1129
|
+
/** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
|
|
1130
|
+
network: z2.string().min(1),
|
|
1131
|
+
/** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
|
|
1132
|
+
asset: z2.string().min(1),
|
|
1133
|
+
/** Exact price in the asset's atomic units, as a decimal string. */
|
|
1134
|
+
amount: z2.string().regex(/^[1-9]\d*$/),
|
|
1135
|
+
/** Recipient wallet; the transfer destination ATA is derived from it. */
|
|
1136
|
+
payTo: z2.string().min(1),
|
|
1137
|
+
maxTimeoutSeconds: z2.number().int().positive().optional(),
|
|
1138
|
+
extra: z2.object({
|
|
1139
|
+
/** Facilitator address that pays the tx fee (gas sponsorship). */
|
|
1140
|
+
feePayer: z2.string().min(1).optional()
|
|
1141
|
+
}).loose().optional()
|
|
1142
|
+
}).loose();
|
|
1143
|
+
var standardPaymentRequiredSchema = z2.object({
|
|
1144
|
+
x402Version: z2.number().int(),
|
|
1145
|
+
accepts: z2.array(z2.unknown()),
|
|
1146
|
+
error: z2.string().optional(),
|
|
1147
|
+
resource: z2.object({ url: z2.string().optional() }).loose().optional()
|
|
1148
|
+
}).loose();
|
|
1149
|
+
var StandardX402ChallengeError = class extends Error {
|
|
1150
|
+
reason;
|
|
1151
|
+
constructor(reason, message) {
|
|
1152
|
+
super(message);
|
|
1153
|
+
this.name = "StandardX402ChallengeError";
|
|
1154
|
+
this.reason = reason;
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
function parseStandardChallenge(challenge) {
|
|
1158
|
+
const parsed = standardPaymentRequiredSchema.safeParse(challenge);
|
|
1079
1159
|
if (!parsed.success) {
|
|
1080
|
-
throw new
|
|
1160
|
+
throw new StandardX402ChallengeError(
|
|
1081
1161
|
"invalid_payment_required",
|
|
1082
|
-
"
|
|
1162
|
+
"Response is not a valid x402 PaymentRequired object"
|
|
1083
1163
|
);
|
|
1084
1164
|
}
|
|
1085
|
-
const
|
|
1086
|
-
const requirement =
|
|
1087
|
-
|
|
1165
|
+
const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
|
|
1166
|
+
const requirement = standardExactRequirementSchema.safeParse(candidate);
|
|
1167
|
+
if (!requirement.success) {
|
|
1168
|
+
return [];
|
|
1169
|
+
}
|
|
1170
|
+
return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
|
|
1088
1171
|
});
|
|
1089
|
-
return { paymentRequired: parsed.data,
|
|
1172
|
+
return { paymentRequired: parsed.data, solanaExactRequirements };
|
|
1173
|
+
}
|
|
1174
|
+
function decodeStandardPaymentRequiredHeader(headerValue) {
|
|
1175
|
+
let decoded;
|
|
1176
|
+
try {
|
|
1177
|
+
decoded = decodeX402Header(headerValue);
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
throw new StandardX402ChallengeError(
|
|
1180
|
+
error instanceof X402HeaderError ? error.reason : "invalid_header",
|
|
1181
|
+
"Cannot decode the payment-required header"
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
return parseStandardChallenge(decoded);
|
|
1090
1185
|
}
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
|
|
1186
|
+
function selectPayableSolanaRequirement(requirements, options) {
|
|
1187
|
+
const network = options?.network ?? SOLANA_MAINNET_NETWORK;
|
|
1188
|
+
const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1189
|
+
const requirement = requirements.find(
|
|
1190
|
+
(candidate) => candidate.network === network && candidate.asset === usdcMint
|
|
1191
|
+
) ?? null;
|
|
1192
|
+
if (requirement === null) {
|
|
1193
|
+
throw new StandardX402ChallengeError(
|
|
1194
|
+
"no_payable_requirement",
|
|
1195
|
+
`The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
|
|
1196
|
+
);
|
|
1094
1197
|
}
|
|
1095
|
-
return
|
|
1096
|
-
|
|
1097
|
-
|
|
1198
|
+
return {
|
|
1199
|
+
requirement,
|
|
1200
|
+
amountRawUsdc: BigInt(requirement.amount),
|
|
1201
|
+
payTo: requirement.payTo,
|
|
1202
|
+
feePayer: requirement.extra?.feePayer ?? null
|
|
1203
|
+
};
|
|
1098
1204
|
}
|
|
1099
1205
|
|
|
1100
|
-
// ../../src/client/
|
|
1101
|
-
var
|
|
1206
|
+
// ../../src/client/standard-x402-payer.ts
|
|
1207
|
+
var StandardX402PayError = class extends Error {
|
|
1102
1208
|
constructor(reason, message, detail = null) {
|
|
1103
1209
|
super(message);
|
|
1104
1210
|
this.reason = reason;
|
|
1105
1211
|
this.detail = detail;
|
|
1106
|
-
this.name = "
|
|
1212
|
+
this.name = "StandardX402PayError";
|
|
1107
1213
|
}
|
|
1108
1214
|
reason;
|
|
1109
1215
|
detail;
|
|
1110
1216
|
};
|
|
1111
|
-
var
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
const value = BigInt(raw);
|
|
1116
|
-
const negative = value < 0n;
|
|
1117
|
-
const abs = negative ? -value : value;
|
|
1118
|
-
const whole = abs / 1000000n;
|
|
1119
|
-
const frac = (abs % 1000000n).toString().padStart(6, "0");
|
|
1120
|
-
return `${negative ? "-" : ""}${whole}.${frac}`;
|
|
1121
|
-
}
|
|
1122
|
-
var PaidFetchService = class {
|
|
1123
|
-
signatureBuilder;
|
|
1124
|
-
fetchImpl;
|
|
1125
|
-
fetchBudget;
|
|
1126
|
-
paymentStatusFor;
|
|
1217
|
+
var StandardX402Payer = class {
|
|
1218
|
+
realizer;
|
|
1219
|
+
x402Fetch;
|
|
1220
|
+
probeFetch;
|
|
1127
1221
|
defaultMaxAmountRawUsdc;
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
maxBodyChars;
|
|
1131
|
-
nowMs;
|
|
1132
|
-
stateStore;
|
|
1133
|
-
pending = /* @__PURE__ */ new Map();
|
|
1134
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
1222
|
+
network;
|
|
1223
|
+
usdcMint;
|
|
1135
1224
|
constructor(config) {
|
|
1136
|
-
this.
|
|
1137
|
-
this.
|
|
1138
|
-
this.
|
|
1139
|
-
this.paymentStatusFor = config.paymentStatusFor ?? (async () => "indeterminate");
|
|
1225
|
+
this.realizer = config.realizer;
|
|
1226
|
+
this.x402Fetch = config.x402Fetch;
|
|
1227
|
+
this.probeFetch = config.probeFetch ?? fetch;
|
|
1140
1228
|
this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
|
|
1141
|
-
this.
|
|
1142
|
-
this.
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
/**
|
|
1154
|
-
* GET the URL, paying a 402 challenge when needed. Concurrent calls for the
|
|
1155
|
-
* same URL share one flow and one result.
|
|
1156
|
-
*/
|
|
1157
|
-
paidFetch(params) {
|
|
1158
|
-
const existing = this.inFlight.get(params.url);
|
|
1159
|
-
if (existing !== void 0) {
|
|
1160
|
-
return existing;
|
|
1161
|
-
}
|
|
1162
|
-
const flow = this.run(params).finally(() => {
|
|
1163
|
-
this.inFlight.delete(params.url);
|
|
1164
|
-
});
|
|
1165
|
-
this.inFlight.set(params.url, flow);
|
|
1166
|
-
return flow;
|
|
1167
|
-
}
|
|
1168
|
-
async run(params) {
|
|
1169
|
-
const { url } = params;
|
|
1170
|
-
const pending = this.pending.get(url);
|
|
1171
|
-
if (pending !== void 0) {
|
|
1172
|
-
const expired = this.nowMs() - pending.challengeAtMs > this.pendingTtlMs;
|
|
1173
|
-
if (pending.unresolved || expired) {
|
|
1174
|
-
if (params.forceNewPayment === true) {
|
|
1175
|
-
this.untrack(url);
|
|
1176
|
-
} else {
|
|
1177
|
-
const outcome = await this.paymentStatusFor(pending.paymentId);
|
|
1178
|
-
if (outcome === "not_settled") {
|
|
1179
|
-
this.untrack(url);
|
|
1180
|
-
} else if (outcome === "settled") {
|
|
1181
|
-
throw new PaidFetchError(
|
|
1182
|
-
"payment_already_settled",
|
|
1183
|
-
`the previous payment for this URL settled (paymentId=${pending.paymentId}) but the content delivery was lost, and the signature can no longer be retried. Calling again with forceNewPayment=true will PAY A SECOND TIME for the same resource.`,
|
|
1184
|
-
{ paymentId: pending.paymentId }
|
|
1185
|
-
);
|
|
1186
|
-
} else {
|
|
1187
|
-
throw new PaidFetchError(
|
|
1188
|
-
"payment_outcome_unknown",
|
|
1189
|
-
`a previously signed payment for this URL (paymentId=${pending.paymentId}) has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call this tool with forceNewPayment=true.`,
|
|
1190
|
-
{ paymentId: pending.paymentId }
|
|
1191
|
-
);
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
} else {
|
|
1195
|
-
return this.deliver(url, pending, {
|
|
1196
|
-
retried: true,
|
|
1197
|
-
budgetBefore: null
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
const challengeAtMs = this.nowMs();
|
|
1202
|
-
const first = await this.fetchImpl(url);
|
|
1203
|
-
const firstText = await first.text();
|
|
1204
|
-
if (first.status !== 402) {
|
|
1205
|
-
return {
|
|
1206
|
-
paid: false,
|
|
1207
|
-
status: first.status,
|
|
1208
|
-
body: this.truncated(firstText)
|
|
1209
|
-
};
|
|
1229
|
+
this.network = config.network ?? SOLANA_MAINNET_NETWORK;
|
|
1230
|
+
this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1231
|
+
}
|
|
1232
|
+
async pay(input) {
|
|
1233
|
+
const init = {
|
|
1234
|
+
method: input.method ?? "GET",
|
|
1235
|
+
...input.body === void 0 ? {} : { body: input.body },
|
|
1236
|
+
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
1237
|
+
};
|
|
1238
|
+
const probe = await this.probeFetch(input.url, init);
|
|
1239
|
+
if (probe.status !== 402) {
|
|
1240
|
+
return { paid: false, status: probe.status, body: await probe.text() };
|
|
1210
1241
|
}
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
"
|
|
1242
|
+
const selected = await this.selectRequirement(probe);
|
|
1243
|
+
const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
|
|
1244
|
+
if (selected.amountRawUsdc > cap) {
|
|
1245
|
+
throw new StandardX402PayError(
|
|
1246
|
+
"amount_exceeds_client_cap",
|
|
1247
|
+
`the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
|
|
1248
|
+
{ amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
|
|
1216
1249
|
);
|
|
1217
1250
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1251
|
+
let realized;
|
|
1252
|
+
try {
|
|
1253
|
+
realized = await this.realizer.ensureUsdcAvailable({
|
|
1254
|
+
amountRawUsdc: selected.amountRawUsdc
|
|
1255
|
+
});
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
throw new StandardX402PayError(
|
|
1258
|
+
"realize_failed",
|
|
1259
|
+
`could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
|
|
1260
|
+
error
|
|
1223
1261
|
);
|
|
1224
1262
|
}
|
|
1225
|
-
const
|
|
1226
|
-
const
|
|
1227
|
-
if (
|
|
1228
|
-
throw new
|
|
1229
|
-
"
|
|
1230
|
-
`the
|
|
1231
|
-
{
|
|
1232
|
-
amountRawUsdc: requirement.amountRawUsdc,
|
|
1233
|
-
maxAmountRawUsdc: maxAmountRawUsdc.toString(),
|
|
1234
|
-
payTo: requirement.payTo
|
|
1235
|
-
}
|
|
1263
|
+
const response = await this.x402Fetch(input.url, init);
|
|
1264
|
+
const bodyText = await response.text();
|
|
1265
|
+
if (response.status !== 200) {
|
|
1266
|
+
throw new StandardX402PayError(
|
|
1267
|
+
"delivery_failed",
|
|
1268
|
+
`the x402 payment did not deliver (status ${response.status})`,
|
|
1269
|
+
{ status: response.status, body: bodyText }
|
|
1236
1270
|
);
|
|
1237
1271
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
challengeAtMs,
|
|
1250
|
-
unresolved: false
|
|
1272
|
+
return {
|
|
1273
|
+
paid: true,
|
|
1274
|
+
status: response.status,
|
|
1275
|
+
body: bodyText,
|
|
1276
|
+
payment: {
|
|
1277
|
+
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1278
|
+
payTo: selected.payTo,
|
|
1279
|
+
feePayer: selected.feePayer,
|
|
1280
|
+
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1281
|
+
realizeTxSignature: realized.txSignature
|
|
1282
|
+
}
|
|
1251
1283
|
};
|
|
1252
|
-
this.track(url, entry);
|
|
1253
|
-
return this.deliver(url, entry, { retried: false, budgetBefore });
|
|
1254
1284
|
}
|
|
1255
|
-
/**
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1285
|
+
/** Reads the challenge from the header (preferred) or the JSON body. */
|
|
1286
|
+
async selectRequirement(probe) {
|
|
1287
|
+
const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1288
|
+
let requirements;
|
|
1289
|
+
try {
|
|
1290
|
+
if (header !== null) {
|
|
1291
|
+
requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
|
|
1292
|
+
} else {
|
|
1293
|
+
requirements = parseStandardChallenge(
|
|
1294
|
+
await probe.json()
|
|
1295
|
+
).solanaExactRequirements;
|
|
1296
|
+
}
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
throw new StandardX402PayError(
|
|
1299
|
+
error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
|
|
1300
|
+
"could not parse the x402 402 challenge",
|
|
1301
|
+
error
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1264
1304
|
try {
|
|
1265
|
-
|
|
1266
|
-
|
|
1305
|
+
return selectPayableSolanaRequirement(requirements, {
|
|
1306
|
+
network: this.network,
|
|
1307
|
+
usdcMint: this.usdcMint
|
|
1267
1308
|
});
|
|
1268
|
-
secondText = await second.text();
|
|
1269
1309
|
} catch (error) {
|
|
1270
|
-
throw new
|
|
1271
|
-
"
|
|
1272
|
-
|
|
1273
|
-
|
|
1310
|
+
throw new StandardX402PayError(
|
|
1311
|
+
"no_payable_requirement",
|
|
1312
|
+
error instanceof Error ? error.message : String(error),
|
|
1313
|
+
error
|
|
1274
1314
|
);
|
|
1275
1315
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
// ../../src/client/mcp-payment-server.ts
|
|
1320
|
+
var TOOL_NAME = "fetch_with_subly_payment";
|
|
1321
|
+
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
|
+
|
|
1323
|
+
Before payments can succeed the operator of this server must have, once:
|
|
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.
|
|
1327
|
+
|
|
1328
|
+
Then use fetch_with_subly_payment(url) to GET or POST 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 wait for yield, do not loop.`;
|
|
1329
|
+
async function runMcpPaymentServer(config) {
|
|
1330
|
+
const { payer: payer2, signer: signer2, facilitatorBaseUrl: facilitatorBaseUrl2, defaultMaxAmountRawUsdc: defaultMaxAmountRawUsdc2 } = config;
|
|
1331
|
+
const server = new Server(
|
|
1332
|
+
{ name: "subly-payments", version: config.serverVersion ?? "0.3.0" },
|
|
1333
|
+
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
1334
|
+
);
|
|
1335
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1336
|
+
tools: [
|
|
1337
|
+
{
|
|
1338
|
+
name: TOOL_NAME,
|
|
1339
|
+
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(
|
|
1340
|
+
defaultMaxAmountRawUsdc2
|
|
1341
|
+
)} 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.`,
|
|
1342
|
+
inputSchema: {
|
|
1343
|
+
type: "object",
|
|
1344
|
+
properties: {
|
|
1345
|
+
url: {
|
|
1346
|
+
type: "string",
|
|
1347
|
+
description: "URL to fetch. Must match the seller's resource URL exactly."
|
|
1348
|
+
},
|
|
1349
|
+
method: {
|
|
1350
|
+
type: "string",
|
|
1351
|
+
description: "HTTP method (default GET). Some x402 sellers deliver the paid resource over POST (e.g. an API that takes a JSON body)."
|
|
1352
|
+
},
|
|
1353
|
+
body: {
|
|
1354
|
+
type: "string",
|
|
1355
|
+
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."
|
|
1356
|
+
},
|
|
1357
|
+
headers: {
|
|
1358
|
+
type: "object",
|
|
1359
|
+
description: "Extra request headers (object of string values), merged into both the probe and the paid retry.",
|
|
1360
|
+
additionalProperties: { type: "string" }
|
|
1361
|
+
},
|
|
1362
|
+
maxAmountRawUsdc: {
|
|
1363
|
+
type: "string",
|
|
1364
|
+
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.'
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
required: ["url"]
|
|
1368
|
+
},
|
|
1369
|
+
annotations: {
|
|
1370
|
+
title: "Fetch with Subly payment",
|
|
1371
|
+
readOnlyHint: false,
|
|
1372
|
+
destructiveHint: true,
|
|
1373
|
+
idempotentHint: false,
|
|
1374
|
+
openWorldHint: true
|
|
1292
1375
|
}
|
|
1376
|
+
}
|
|
1377
|
+
]
|
|
1378
|
+
}));
|
|
1379
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1380
|
+
if (request.params.name !== TOOL_NAME) {
|
|
1381
|
+
return {
|
|
1382
|
+
content: [{ type: "text", text: `unknown tool: ${request.params.name}` }],
|
|
1383
|
+
isError: true
|
|
1293
1384
|
};
|
|
1294
1385
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
"
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
);
|
|
1386
|
+
const args = request.params.arguments ?? {};
|
|
1387
|
+
const url = args.url;
|
|
1388
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
1389
|
+
return {
|
|
1390
|
+
content: [{ type: "text", text: "missing required argument: url" }],
|
|
1391
|
+
isError: true
|
|
1392
|
+
};
|
|
1303
1393
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
{
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1394
|
+
let maxAmountRawUsdc;
|
|
1395
|
+
if (args.maxAmountRawUsdc !== void 0) {
|
|
1396
|
+
const raw = args.maxAmountRawUsdc;
|
|
1397
|
+
try {
|
|
1398
|
+
if (typeof raw !== "string" && typeof raw !== "number") {
|
|
1399
|
+
throw new TypeError("not a string or number");
|
|
1400
|
+
}
|
|
1401
|
+
maxAmountRawUsdc = BigInt(raw);
|
|
1402
|
+
} catch {
|
|
1403
|
+
return {
|
|
1404
|
+
content: [
|
|
1405
|
+
{
|
|
1406
|
+
type: "text",
|
|
1407
|
+
text: "maxAmountRawUsdc must be an integer raw USDC amount"
|
|
1408
|
+
}
|
|
1409
|
+
],
|
|
1410
|
+
isError: true
|
|
1411
|
+
};
|
|
1311
1412
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1413
|
+
}
|
|
1414
|
+
const method = typeof args.method === "string" ? args.method : void 0;
|
|
1415
|
+
const body = typeof args.body === "string" ? args.body : void 0;
|
|
1416
|
+
const headers = args.headers !== null && typeof args.headers === "object" && !Array.isArray(args.headers) ? Object.fromEntries(
|
|
1417
|
+
Object.entries(args.headers).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])
|
|
1418
|
+
) : void 0;
|
|
1419
|
+
const mergedHeaders = body === void 0 ? headers : { "content-type": "application/json", ...headers ?? {} };
|
|
1420
|
+
try {
|
|
1421
|
+
const result = await payer2.pay({
|
|
1422
|
+
url,
|
|
1423
|
+
...method === void 0 ? {} : { method },
|
|
1424
|
+
...body === void 0 ? {} : { body },
|
|
1425
|
+
...mergedHeaders === void 0 ? {} : { headers: mergedHeaders },
|
|
1426
|
+
...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc }
|
|
1427
|
+
});
|
|
1428
|
+
return {
|
|
1429
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1430
|
+
};
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
if (error instanceof StandardX402PayError) {
|
|
1433
|
+
return {
|
|
1434
|
+
content: [
|
|
1435
|
+
{
|
|
1436
|
+
type: "text",
|
|
1437
|
+
text: JSON.stringify(
|
|
1438
|
+
{
|
|
1439
|
+
paid: false,
|
|
1440
|
+
refused: true,
|
|
1441
|
+
reason: error.reason,
|
|
1442
|
+
message: error.message,
|
|
1443
|
+
detail: error.detail
|
|
1444
|
+
},
|
|
1445
|
+
null,
|
|
1446
|
+
2
|
|
1447
|
+
)
|
|
1448
|
+
}
|
|
1449
|
+
],
|
|
1450
|
+
isError: true
|
|
1451
|
+
};
|
|
1319
1452
|
}
|
|
1453
|
+
return {
|
|
1454
|
+
content: [
|
|
1455
|
+
{
|
|
1456
|
+
type: "text",
|
|
1457
|
+
text: error instanceof Error ? error.message : String(error)
|
|
1458
|
+
}
|
|
1459
|
+
],
|
|
1460
|
+
isError: true
|
|
1461
|
+
};
|
|
1320
1462
|
}
|
|
1321
|
-
|
|
1322
|
-
this.persist();
|
|
1323
|
-
}
|
|
1324
|
-
untrack(url) {
|
|
1325
|
-
this.pending.delete(url);
|
|
1326
|
-
this.persist();
|
|
1327
|
-
}
|
|
1328
|
-
persist() {
|
|
1329
|
-
if (this.stateStore === null) {
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
this.stateStore.save(
|
|
1333
|
-
[...this.pending.entries()].map(([url, entry]) => ({ url, ...entry }))
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
truncated(text) {
|
|
1337
|
-
return text.length > this.maxBodyChars ? `${text.slice(0, this.maxBodyChars)}
|
|
1338
|
-
... (truncated)` : text;
|
|
1339
|
-
}
|
|
1340
|
-
};
|
|
1341
|
-
function receiptTransaction(headers) {
|
|
1463
|
+
});
|
|
1342
1464
|
try {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
} catch {
|
|
1350
|
-
return null;
|
|
1465
|
+
await ensureWalletOnboarded({ facilitatorBaseUrl: facilitatorBaseUrl2, signer: signer2 });
|
|
1466
|
+
console.error("[subly-mcp] wallet registered and synced at the relayer");
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
console.error(
|
|
1469
|
+
`[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
|
|
1470
|
+
);
|
|
1351
1471
|
}
|
|
1472
|
+
const transport = new StdioServerTransport();
|
|
1473
|
+
await server.connect(transport);
|
|
1474
|
+
console.error(
|
|
1475
|
+
`[subly-mcp] ready: agent wallet ${signer2.walletAddress}, relayer ${facilitatorBaseUrl2}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc2)} USDC`
|
|
1476
|
+
);
|
|
1352
1477
|
}
|
|
1353
1478
|
|
|
1354
|
-
// ../../src/
|
|
1355
|
-
import {
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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));
|
|
1363
1503
|
}
|
|
1364
|
-
function
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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))
|
|
1369
1512
|
);
|
|
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;
|
|
1370
1520
|
}
|
|
1371
1521
|
|
|
1372
|
-
// ../../src/client/
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
const message = walletAuthMessage({
|
|
1376
|
-
method: params.method,
|
|
1377
|
-
path: new URL(params.url).pathname,
|
|
1378
|
-
rawBody: params.body ?? "",
|
|
1379
|
-
signedAtMs
|
|
1380
|
-
});
|
|
1381
|
-
return {
|
|
1382
|
-
[WALLET_AUTH_WALLET_HEADER]: params.signer.walletAddress,
|
|
1383
|
-
[WALLET_AUTH_SIGNED_AT_HEADER]: signedAtMs,
|
|
1384
|
-
[WALLET_AUTH_SIGNATURE_HEADER]: await params.signer.signApiMessage(message)
|
|
1385
|
-
};
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
// ../../src/client/onboarding.ts
|
|
1389
|
-
var SELF_SERVE_POLICY_ID = "self-serve";
|
|
1390
|
-
var OnboardingError = class extends Error {
|
|
1391
|
-
constructor(step, message, detail = null) {
|
|
1522
|
+
// ../../src/client/relayer-yield-realizer.ts
|
|
1523
|
+
var RelayerRealizeError = class extends Error {
|
|
1524
|
+
constructor(code, message, detail = null) {
|
|
1392
1525
|
super(message);
|
|
1393
|
-
this.
|
|
1526
|
+
this.code = code;
|
|
1394
1527
|
this.detail = detail;
|
|
1395
|
-
this.name = "
|
|
1528
|
+
this.name = "RelayerRealizeError";
|
|
1396
1529
|
}
|
|
1397
|
-
|
|
1530
|
+
code;
|
|
1398
1531
|
detail;
|
|
1399
1532
|
};
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1533
|
+
var RelayerYieldRealizer = class {
|
|
1534
|
+
config;
|
|
1535
|
+
signer;
|
|
1536
|
+
rpc;
|
|
1537
|
+
fetchImpl;
|
|
1538
|
+
lookupTablesFor;
|
|
1539
|
+
constructor(config) {
|
|
1540
|
+
this.config = {
|
|
1541
|
+
facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
|
|
1542
|
+
usdcMint: config.usdcMint ?? SUBLY_VAULT.usdcMint,
|
|
1543
|
+
forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
|
|
1544
|
+
};
|
|
1545
|
+
this.signer = config.signer;
|
|
1546
|
+
this.rpc = config.rpc;
|
|
1547
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1548
|
+
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(this.rpc, serializedTransaction));
|
|
1549
|
+
}
|
|
1550
|
+
async ensureUsdcAvailable(input) {
|
|
1551
|
+
const agentAta = deriveAssociatedTokenAddress({
|
|
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
|
+
}
|
|
1565
|
+
await this.assertSpendableYield(shortfallRawUsdc);
|
|
1566
|
+
const prepared = await this.postJson("/v1/withdrawals/prepare", {
|
|
1567
|
+
wallet: this.signer.walletAddress,
|
|
1568
|
+
amountRawUsdc: shortfallRawUsdc.toString()
|
|
1569
|
+
});
|
|
1570
|
+
const signed = await this.signer.signWithdrawal({
|
|
1571
|
+
intent: prepared.signingIntent,
|
|
1572
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1573
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1574
|
+
});
|
|
1575
|
+
let settled = await this.postJson("/v1/withdrawals/submit", {
|
|
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);
|
|
1582
|
+
}
|
|
1583
|
+
if (settled.status !== "confirmed" || settled.txSignature === null) {
|
|
1584
|
+
throw new RelayerRealizeError(
|
|
1585
|
+
"realize_not_confirmed",
|
|
1586
|
+
`yield realize withdrawal did not confirm (status=${settled.status})`,
|
|
1587
|
+
settled
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
return {
|
|
1591
|
+
realizedRawUsdc: BigInt(settled.actualWithdrawRawUsdc ?? "0"),
|
|
1592
|
+
txSignature: settled.txSignature
|
|
1593
|
+
};
|
|
1594
|
+
}
|
|
1595
|
+
/** Refuses to realize more than the ledger's spendable yield (principal). */
|
|
1596
|
+
async assertSpendableYield(shortfallRawUsdc) {
|
|
1597
|
+
const url = `${this.config.facilitatorBaseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
1598
|
+
let response;
|
|
1599
|
+
try {
|
|
1600
|
+
response = await this.fetchImpl(url, {
|
|
1601
|
+
headers: await walletAuthHeaders({
|
|
1602
|
+
signer: this.signer,
|
|
1603
|
+
method: "GET",
|
|
1604
|
+
url
|
|
1605
|
+
})
|
|
1606
|
+
});
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
throw new RelayerRealizeError(
|
|
1609
|
+
"budget_unavailable",
|
|
1610
|
+
"could not read the spendable-yield budget",
|
|
1611
|
+
error
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
if (response.status !== 200) {
|
|
1615
|
+
throw new RelayerRealizeError(
|
|
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) {
|
|
1623
|
+
throw new RelayerRealizeError(
|
|
1624
|
+
"insufficient_yield",
|
|
1625
|
+
`spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC; the principal is never spent \u2014 wait for more yield`,
|
|
1626
|
+
{ spendableYieldRawUsdc: spendable.toString() }
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Polls GET /v1/withdrawals/:id (which reconciles a submitted intent against
|
|
1632
|
+
* the chain) until it leaves the "submitted" state or the timeout elapses.
|
|
1633
|
+
*/
|
|
1634
|
+
async pollUntilSettled(withdrawalId, { timeoutMs = 9e4, intervalMs = 2500 } = {}) {
|
|
1635
|
+
const deadline = Date.now() + timeoutMs;
|
|
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}`;
|
|
1405
1663
|
const serialized = JSON.stringify(body);
|
|
1406
|
-
const response = await fetchImpl(url, {
|
|
1664
|
+
const response = await this.fetchImpl(url, {
|
|
1407
1665
|
method: "POST",
|
|
1408
1666
|
headers: {
|
|
1409
1667
|
...await walletAuthHeaders({
|
|
1410
|
-
signer:
|
|
1668
|
+
signer: this.signer,
|
|
1411
1669
|
method: "POST",
|
|
1412
1670
|
url,
|
|
1413
1671
|
body: serialized
|
|
@@ -1416,30 +1674,38 @@ async function ensureWalletOnboarded(params) {
|
|
|
1416
1674
|
},
|
|
1417
1675
|
body: serialized
|
|
1418
1676
|
});
|
|
1677
|
+
const text = await response.text();
|
|
1419
1678
|
if (response.status !== 200) {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
} catch {
|
|
1424
|
-
detail = null;
|
|
1425
|
-
}
|
|
1426
|
-
throw new OnboardingError(
|
|
1427
|
-
step,
|
|
1428
|
-
`wallet onboarding ${step} failed with ${response.status}`,
|
|
1429
|
-
detail
|
|
1679
|
+
throw new RelayerRealizeError(
|
|
1680
|
+
path.endsWith("/submit") ? "submit_failed" : "prepare_failed",
|
|
1681
|
+
`${path} failed with ${response.status}: ${text}`
|
|
1430
1682
|
);
|
|
1431
1683
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1684
|
+
return JSON.parse(text);
|
|
1685
|
+
}
|
|
1686
|
+
async readTokenBalance(ata) {
|
|
1687
|
+
try {
|
|
1688
|
+
const response = await this.rpc.getTokenAccountBalance(address2(ata), { commitment: "confirmed" }).send();
|
|
1689
|
+
return BigInt(response.value.amount);
|
|
1690
|
+
} catch {
|
|
1691
|
+
return 0n;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1696
|
+
// ../../src/client/relayer-payer.ts
|
|
1697
|
+
function createRelayerX402Payer(config) {
|
|
1698
|
+
const realizer = new RelayerYieldRealizer({
|
|
1699
|
+
facilitatorBaseUrl: config.facilitatorBaseUrl,
|
|
1700
|
+
signer: config.signer,
|
|
1701
|
+
rpc: config.rpc,
|
|
1702
|
+
forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
|
|
1703
|
+
});
|
|
1704
|
+
return new StandardX402Payer({
|
|
1705
|
+
realizer,
|
|
1706
|
+
x402Fetch: config.x402Fetch,
|
|
1707
|
+
defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc
|
|
1441
1708
|
});
|
|
1442
|
-
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
|
|
1443
1709
|
}
|
|
1444
1710
|
|
|
1445
1711
|
// ../../src/solana/keys.ts
|
|
@@ -1466,6 +1732,24 @@ async function loadKeyPairSigner(params) {
|
|
|
1466
1732
|
}
|
|
1467
1733
|
throw new Error(`${label} keypair is not configured`);
|
|
1468
1734
|
}
|
|
1735
|
+
function loadSecretKeyBytes(params) {
|
|
1736
|
+
const { base58Secret, jsonFilePath, label } = params;
|
|
1737
|
+
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
1738
|
+
const bytes = bs586.decode(base58Secret);
|
|
1739
|
+
if (bytes.length !== 64) {
|
|
1740
|
+
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
1741
|
+
}
|
|
1742
|
+
return bytes;
|
|
1743
|
+
}
|
|
1744
|
+
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1745
|
+
const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
|
|
1746
|
+
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1747
|
+
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1748
|
+
}
|
|
1749
|
+
return Uint8Array.from(raw);
|
|
1750
|
+
}
|
|
1751
|
+
throw new Error(`${label} keypair is not configured`);
|
|
1752
|
+
}
|
|
1469
1753
|
|
|
1470
1754
|
// ../../src/solana/rpc.ts
|
|
1471
1755
|
import { createSolanaRpc } from "@solana/kit";
|
|
@@ -1473,428 +1757,51 @@ function createRpc(url) {
|
|
|
1473
1757
|
return createSolanaRpc(url);
|
|
1474
1758
|
}
|
|
1475
1759
|
|
|
1476
|
-
//
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
signer;
|
|
1490
|
-
lookupTablesFor;
|
|
1491
|
-
fetchImpl;
|
|
1492
|
-
constructor(config) {
|
|
1493
|
-
this.config = {
|
|
1494
|
-
facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
|
|
1495
|
-
network: config.network ?? SOLANA_MAINNET_NETWORK
|
|
1496
|
-
};
|
|
1497
|
-
this.signer = config.signer;
|
|
1498
|
-
this.lookupTablesFor = config.lookupTablesFor ?? null;
|
|
1499
|
-
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1500
|
-
}
|
|
1501
|
-
/**
|
|
1502
|
-
* Builds the PAYMENT-SIGNATURE header for a 402 challenge. The request
|
|
1503
|
-
* method, URL, and body must be exactly the request being retried; they are
|
|
1504
|
-
* bound into the payment and verified again by the seller and facilitator.
|
|
1505
|
-
*/
|
|
1506
|
-
async buildPaymentSignatureHeader(input) {
|
|
1507
|
-
const requirement = this.selectRequirement(input.paymentRequiredHeader);
|
|
1508
|
-
if (requirement.resource !== input.url) {
|
|
1509
|
-
throw new X402ClientError(
|
|
1510
|
-
"resource_mismatch",
|
|
1511
|
-
"PAYMENT-REQUIRED resource does not match the request URL"
|
|
1512
|
-
);
|
|
1513
|
-
}
|
|
1514
|
-
const requestBodyHash = requestBodyHashFor(input.body);
|
|
1515
|
-
const prepared = await this.preparePayment({
|
|
1516
|
-
requirement,
|
|
1517
|
-
httpMethod: input.httpMethod.toUpperCase(),
|
|
1518
|
-
requestBodyHash
|
|
1519
|
-
});
|
|
1520
|
-
const lookupTables = this.lookupTablesFor === null ? void 0 : await this.lookupTablesFor(prepared.intentJson.serializedTransaction);
|
|
1521
|
-
const signed = await this.signer.signPayment({
|
|
1522
|
-
intent: prepared.intentJson.signingIntent,
|
|
1523
|
-
serializedTransaction: prepared.intentJson.serializedTransaction,
|
|
1524
|
-
lookupTables
|
|
1525
|
-
});
|
|
1526
|
-
const payload = {
|
|
1527
|
-
x402Version: X402_VERSION,
|
|
1528
|
-
scheme: requirement.scheme,
|
|
1529
|
-
network: requirement.network,
|
|
1530
|
-
payload: {
|
|
1531
|
-
paymentId: prepared.paymentId,
|
|
1532
|
-
requestBindingHash: prepared.requestBindingHash,
|
|
1533
|
-
preparedMessageHash: prepared.preparedMessageHash,
|
|
1534
|
-
serializedTransaction: signed.serializedTransaction,
|
|
1535
|
-
agentSignature: signed.agentSignature,
|
|
1536
|
-
temporarySettlementSignature: prepared.temporarySettlementSignature
|
|
1537
|
-
}
|
|
1538
|
-
};
|
|
1539
|
-
return {
|
|
1540
|
-
headerValue: encodeX402Header(payload),
|
|
1541
|
-
paymentId: prepared.paymentId
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
/**
|
|
1545
|
-
* Convenience wrapper: performs the request, and on a 402 with a Subly
|
|
1546
|
-
* requirement pays from vault yield and retries once.
|
|
1547
|
-
*/
|
|
1548
|
-
async fetchWithPayment(url, init) {
|
|
1549
|
-
const first = await this.fetchImpl(url, init);
|
|
1550
|
-
if (first.status !== 402) {
|
|
1551
|
-
return first;
|
|
1552
|
-
}
|
|
1553
|
-
const challenge = first.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1554
|
-
if (challenge === null) {
|
|
1555
|
-
return first;
|
|
1556
|
-
}
|
|
1557
|
-
const { headerValue } = await this.buildPaymentSignatureHeader({
|
|
1558
|
-
paymentRequiredHeader: challenge,
|
|
1559
|
-
httpMethod: init?.method ?? "GET",
|
|
1560
|
-
url,
|
|
1561
|
-
body: init?.body ?? null
|
|
1562
|
-
});
|
|
1563
|
-
return this.fetchImpl(url, {
|
|
1564
|
-
...init,
|
|
1565
|
-
headers: {
|
|
1566
|
-
...init?.headers,
|
|
1567
|
-
[PAYMENT_SIGNATURE_HEADER]: headerValue
|
|
1760
|
+
// src/svm-x402-fetch.ts
|
|
1761
|
+
import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
|
|
1762
|
+
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
|
|
1763
|
+
import { ExactSvmScheme, toClientSvmSigner } from "@x402/svm";
|
|
1764
|
+
async function createSvmX402Fetch(params) {
|
|
1765
|
+
const signer2 = toClientSvmSigner(
|
|
1766
|
+
await createKeyPairSignerFromBytes2(params.agentSecretKey)
|
|
1767
|
+
);
|
|
1768
|
+
const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
|
|
1769
|
+
schemes: [
|
|
1770
|
+
{
|
|
1771
|
+
network: "solana:*",
|
|
1772
|
+
client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
|
|
1568
1773
|
}
|
|
1569
|
-
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
try {
|
|
1574
|
-
sublyRequirements = decodePaymentRequiredHeader(
|
|
1575
|
-
paymentRequiredHeader
|
|
1576
|
-
).sublyRequirements;
|
|
1577
|
-
} catch (error) {
|
|
1578
|
-
throw new X402ClientError(
|
|
1579
|
-
error instanceof X402HeaderError ? error.reason : "invalid_challenge",
|
|
1580
|
-
"Cannot decode the PAYMENT-REQUIRED header"
|
|
1581
|
-
);
|
|
1582
|
-
}
|
|
1583
|
-
const selected = sublyRequirements.find(
|
|
1584
|
-
(candidate) => candidate.network === this.config.network && candidate.extra.vault === SUBLY_VAULT.address && candidate.extra.shareMint === SUBLY_VAULT.shareMint && candidate.asset === SUBLY_VAULT.usdcMint
|
|
1585
|
-
) ?? null;
|
|
1586
|
-
if (selected === null) {
|
|
1587
|
-
throw new X402ClientError(
|
|
1588
|
-
"no_supported_requirement",
|
|
1589
|
-
"The 402 challenge contains no supported subly-yield-exact requirement"
|
|
1590
|
-
);
|
|
1591
|
-
}
|
|
1592
|
-
return selected;
|
|
1593
|
-
}
|
|
1594
|
-
async preparePayment(input) {
|
|
1595
|
-
const { requirement } = input;
|
|
1596
|
-
const wallet = this.signer.walletAddress;
|
|
1597
|
-
const prepareUrl = `${this.config.facilitatorBaseUrl}/v1/payments/prepare`;
|
|
1598
|
-
const prepareBody = JSON.stringify({
|
|
1599
|
-
wallet,
|
|
1600
|
-
scheme: requirement.scheme,
|
|
1601
|
-
network: requirement.network,
|
|
1602
|
-
vault: requirement.extra.vault,
|
|
1603
|
-
shareMint: requirement.extra.shareMint,
|
|
1604
|
-
asset: requirement.asset,
|
|
1605
|
-
seller: requirement.extra.seller,
|
|
1606
|
-
sellerRequestId: requirement.extra.sellerRequestId,
|
|
1607
|
-
httpMethod: input.httpMethod,
|
|
1608
|
-
canonicalResourceUrl: requirement.resource,
|
|
1609
|
-
requestBodyHash: input.requestBodyHash,
|
|
1610
|
-
amountRawUsdc: requirement.amountRawUsdc,
|
|
1611
|
-
payTo: requirement.payTo,
|
|
1612
|
-
sellerUsdcAta: requirement.extra.sellerUsdcAta,
|
|
1613
|
-
dustRecipientUsdcAta: deriveAssociatedTokenAddress({
|
|
1614
|
-
owner: wallet,
|
|
1615
|
-
mint: SUBLY_VAULT.usdcMint
|
|
1616
|
-
})
|
|
1617
|
-
});
|
|
1618
|
-
const response = await this.fetchImpl(prepareUrl, {
|
|
1619
|
-
method: "POST",
|
|
1620
|
-
headers: {
|
|
1621
|
-
...await walletAuthHeaders({
|
|
1622
|
-
signer: this.signer,
|
|
1623
|
-
method: "POST",
|
|
1624
|
-
url: prepareUrl,
|
|
1625
|
-
body: prepareBody
|
|
1626
|
-
}),
|
|
1627
|
-
"content-type": "application/json"
|
|
1628
|
-
},
|
|
1629
|
-
body: prepareBody
|
|
1630
|
-
});
|
|
1631
|
-
const body = await response.json();
|
|
1632
|
-
if (response.status !== 200) {
|
|
1633
|
-
const error = body.error;
|
|
1634
|
-
throw new X402ClientError(
|
|
1635
|
-
typeof error?.code === "string" ? error.code : "prepare_failed",
|
|
1636
|
-
"Payment preparation failed at the facilitator",
|
|
1637
|
-
body
|
|
1638
|
-
);
|
|
1639
|
-
}
|
|
1640
|
-
const prepared = body;
|
|
1641
|
-
if (typeof prepared.paymentId !== "string" || typeof prepared.preparedMessageHash !== "string" || typeof prepared.intentJson?.serializedTransaction !== "string" || typeof prepared.intentJson?.signingIntent !== "object") {
|
|
1642
|
-
throw new X402ClientError(
|
|
1643
|
-
"invalid_prepare_response",
|
|
1644
|
-
"Facilitator prepare response is missing required fields"
|
|
1645
|
-
);
|
|
1646
|
-
}
|
|
1647
|
-
return prepared;
|
|
1648
|
-
}
|
|
1649
|
-
};
|
|
1774
|
+
]
|
|
1775
|
+
});
|
|
1776
|
+
return (url, init) => wrapped(url, init);
|
|
1777
|
+
}
|
|
1650
1778
|
|
|
1651
|
-
//
|
|
1652
|
-
var TOOL_NAME = "fetch_with_subly_payment";
|
|
1653
|
-
var DEFAULT_MAX_AMOUNT_RAW_USDC = 10000n;
|
|
1779
|
+
// src/mcp-server.ts
|
|
1654
1780
|
var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
|
|
1655
|
-
var
|
|
1781
|
+
var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
|
|
1782
|
+
var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
|
|
1656
1783
|
var keyPairSigner = await loadKeyPairSigner({
|
|
1657
1784
|
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1658
1785
|
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1659
1786
|
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1660
1787
|
});
|
|
1788
|
+
var agentSecretKey = loadSecretKeyBytes({
|
|
1789
|
+
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1790
|
+
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1791
|
+
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1792
|
+
});
|
|
1661
1793
|
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
1662
|
-
var rpc = createRpc(
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
headers: await walletAuthHeaders({ signer, method: "GET", url })
|
|
1670
|
-
});
|
|
1671
|
-
if (response.status !== 200) {
|
|
1672
|
-
return null;
|
|
1673
|
-
}
|
|
1674
|
-
const body = await response.json();
|
|
1675
|
-
if (body.budget === void 0) {
|
|
1676
|
-
return null;
|
|
1677
|
-
}
|
|
1678
|
-
return {
|
|
1679
|
-
positionValueUsdc: formatRawUsdcAmount(body.budget.positionValueRawUsdc),
|
|
1680
|
-
spendableYieldUsdc: formatRawUsdcAmount(
|
|
1681
|
-
body.budget.spendableYieldRawUsdc
|
|
1682
|
-
)
|
|
1683
|
-
};
|
|
1684
|
-
} catch {
|
|
1685
|
-
return null;
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
async function paymentStatusFor(paymentId) {
|
|
1689
|
-
try {
|
|
1690
|
-
const url = `${facilitatorBaseUrl}/v1/payments/${paymentId}`;
|
|
1691
|
-
const response = await fetch(url, {
|
|
1692
|
-
headers: await walletAuthHeaders({ signer, method: "GET", url })
|
|
1693
|
-
});
|
|
1694
|
-
if (response.status !== 200) {
|
|
1695
|
-
return "indeterminate";
|
|
1696
|
-
}
|
|
1697
|
-
const body = await response.json();
|
|
1698
|
-
if (body.status === "settled") {
|
|
1699
|
-
return "settled";
|
|
1700
|
-
}
|
|
1701
|
-
if (body.status === "expired" || body.status === "failed" || body.status === "failed_not_submitted") {
|
|
1702
|
-
return "not_settled";
|
|
1703
|
-
}
|
|
1704
|
-
return "indeterminate";
|
|
1705
|
-
} catch {
|
|
1706
|
-
return "indeterminate";
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
function fileStateStore(path) {
|
|
1710
|
-
return {
|
|
1711
|
-
load() {
|
|
1712
|
-
try {
|
|
1713
|
-
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1714
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
1715
|
-
} catch {
|
|
1716
|
-
return [];
|
|
1717
|
-
}
|
|
1718
|
-
},
|
|
1719
|
-
save(records) {
|
|
1720
|
-
try {
|
|
1721
|
-
writeFileSync(path, JSON.stringify(records));
|
|
1722
|
-
} catch (error) {
|
|
1723
|
-
console.error(
|
|
1724
|
-
`[subly-mcp] failed to persist pending payments: ${error instanceof Error ? error.message : String(error)}`
|
|
1725
|
-
);
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? "demo/env/mcp-pending-payments.json";
|
|
1731
|
-
var paidFetchService = new PaidFetchService({
|
|
1732
|
-
signatureBuilder: new SublyX402Client({
|
|
1733
|
-
facilitatorBaseUrl,
|
|
1734
|
-
signer,
|
|
1735
|
-
lookupTablesFor: (serializedTransaction) => fetchLookupTablesForTransaction(rpc, serializedTransaction)
|
|
1736
|
-
}),
|
|
1737
|
-
defaultMaxAmountRawUsdc,
|
|
1738
|
-
fetchBudget,
|
|
1739
|
-
paymentStatusFor,
|
|
1740
|
-
stateStore: fileStateStore(pendingStatePath)
|
|
1794
|
+
var rpc = createRpc(rpcUrl);
|
|
1795
|
+
var payer = createRelayerX402Payer({
|
|
1796
|
+
facilitatorBaseUrl,
|
|
1797
|
+
signer,
|
|
1798
|
+
rpc,
|
|
1799
|
+
x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
|
|
1800
|
+
defaultMaxAmountRawUsdc
|
|
1741
1801
|
});
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
3. Waited for yield to accrue; a payment needs the price plus a fixed overhead (~0.0024 USDC) of spendable yield.
|
|
1748
|
-
|
|
1749
|
-
Then use fetch_with_subly_payment(url) to GET a paid resource: it pays the 402 from yield and returns the body plus an on-chain receipt. If it returns insufficient_yield, that is expected \u2014 wait for yield, do not loop. If it returns delivery_failed_payment_pending, call the SAME url again (it retries the same payment, never double-pays).`;
|
|
1750
|
-
var server = new Server(
|
|
1751
|
-
{ name: "subly-payments", version: "0.1.1" },
|
|
1752
|
-
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
1753
|
-
);
|
|
1754
|
-
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1755
|
-
tools: [
|
|
1756
|
-
{
|
|
1757
|
-
name: TOOL_NAME,
|
|
1758
|
-
description: `GET a URL, automatically paying a Subly x402 (subly-yield-exact) 402 challenge from the agent wallet's Kamino vault yield. Returns the response body; when a payment was made, also returns the settlement receipt (amount, payee, paymentId, Solscan link). Challenges above maxAmountRawUsdc (default ${defaultMaxAmountRawUsdc} raw = ${formatRawUsdcAmount(
|
|
1759
|
-
defaultMaxAmountRawUsdc
|
|
1760
|
-
)} USDC) are refused without paying. If delivery fails after the payment was signed, the signature is kept and calling again with the same URL retries the same payment instead of paying twice. Payments are refused by the facilitator when the spendable yield budget cannot cover them \u2014 the principal is never spent. Use only for URLs you intend to purchase access to.`,
|
|
1761
|
-
inputSchema: {
|
|
1762
|
-
type: "object",
|
|
1763
|
-
properties: {
|
|
1764
|
-
url: {
|
|
1765
|
-
type: "string",
|
|
1766
|
-
description: "URL to fetch (GET). Must match the seller's resource URL exactly."
|
|
1767
|
-
},
|
|
1768
|
-
maxAmountRawUsdc: {
|
|
1769
|
-
type: "string",
|
|
1770
|
-
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.'
|
|
1771
|
-
},
|
|
1772
|
-
forceNewPayment: {
|
|
1773
|
-
type: "boolean",
|
|
1774
|
-
description: "Pay again even though a previous payment for this URL settled or has an unknown outcome. Only set deliberately: combined with payment_already_settled this means paying twice for the same resource. Ignored while the previous payment is still retryable (the same signature is retried instead)."
|
|
1775
|
-
}
|
|
1776
|
-
},
|
|
1777
|
-
required: ["url"]
|
|
1778
|
-
},
|
|
1779
|
-
annotations: {
|
|
1780
|
-
title: "Fetch with Subly payment",
|
|
1781
|
-
readOnlyHint: false,
|
|
1782
|
-
destructiveHint: true,
|
|
1783
|
-
idempotentHint: false,
|
|
1784
|
-
openWorldHint: true
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
]
|
|
1788
|
-
}));
|
|
1789
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1790
|
-
if (request.params.name !== TOOL_NAME) {
|
|
1791
|
-
return {
|
|
1792
|
-
content: [
|
|
1793
|
-
{ type: "text", text: `unknown tool: ${request.params.name}` }
|
|
1794
|
-
],
|
|
1795
|
-
isError: true
|
|
1796
|
-
};
|
|
1797
|
-
}
|
|
1798
|
-
const args = request.params.arguments ?? {};
|
|
1799
|
-
const url = args.url;
|
|
1800
|
-
if (typeof url !== "string" || url.length === 0) {
|
|
1801
|
-
return {
|
|
1802
|
-
content: [{ type: "text", text: "missing required argument: url" }],
|
|
1803
|
-
isError: true
|
|
1804
|
-
};
|
|
1805
|
-
}
|
|
1806
|
-
let maxAmountRawUsdc;
|
|
1807
|
-
if (args.maxAmountRawUsdc !== void 0) {
|
|
1808
|
-
const raw = args.maxAmountRawUsdc;
|
|
1809
|
-
try {
|
|
1810
|
-
if (typeof raw !== "string" && typeof raw !== "number") {
|
|
1811
|
-
throw new TypeError("not a string or number");
|
|
1812
|
-
}
|
|
1813
|
-
maxAmountRawUsdc = BigInt(raw);
|
|
1814
|
-
} catch {
|
|
1815
|
-
return {
|
|
1816
|
-
content: [
|
|
1817
|
-
{
|
|
1818
|
-
type: "text",
|
|
1819
|
-
text: "maxAmountRawUsdc must be an integer raw USDC amount"
|
|
1820
|
-
}
|
|
1821
|
-
],
|
|
1822
|
-
isError: true
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
try {
|
|
1827
|
-
const result = await paidFetchService.paidFetch({
|
|
1828
|
-
url,
|
|
1829
|
-
...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc },
|
|
1830
|
-
forceNewPayment: args.forceNewPayment === true
|
|
1831
|
-
});
|
|
1832
|
-
return {
|
|
1833
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1834
|
-
};
|
|
1835
|
-
} catch (error) {
|
|
1836
|
-
if (error instanceof PaidFetchError) {
|
|
1837
|
-
return {
|
|
1838
|
-
content: [
|
|
1839
|
-
{
|
|
1840
|
-
type: "text",
|
|
1841
|
-
text: JSON.stringify(
|
|
1842
|
-
{
|
|
1843
|
-
paid: false,
|
|
1844
|
-
refused: true,
|
|
1845
|
-
reason: error.reason,
|
|
1846
|
-
message: error.message,
|
|
1847
|
-
detail: error.detail
|
|
1848
|
-
},
|
|
1849
|
-
null,
|
|
1850
|
-
2
|
|
1851
|
-
)
|
|
1852
|
-
}
|
|
1853
|
-
],
|
|
1854
|
-
isError: true
|
|
1855
|
-
};
|
|
1856
|
-
}
|
|
1857
|
-
if (error instanceof X402ClientError) {
|
|
1858
|
-
return {
|
|
1859
|
-
content: [
|
|
1860
|
-
{
|
|
1861
|
-
type: "text",
|
|
1862
|
-
text: JSON.stringify(
|
|
1863
|
-
{
|
|
1864
|
-
paid: false,
|
|
1865
|
-
refused: true,
|
|
1866
|
-
reason: error.reason,
|
|
1867
|
-
detail: error.detail ?? null
|
|
1868
|
-
},
|
|
1869
|
-
null,
|
|
1870
|
-
2
|
|
1871
|
-
)
|
|
1872
|
-
}
|
|
1873
|
-
],
|
|
1874
|
-
isError: true
|
|
1875
|
-
};
|
|
1876
|
-
}
|
|
1877
|
-
return {
|
|
1878
|
-
content: [
|
|
1879
|
-
{
|
|
1880
|
-
type: "text",
|
|
1881
|
-
text: error instanceof Error ? error.message : String(error)
|
|
1882
|
-
}
|
|
1883
|
-
],
|
|
1884
|
-
isError: true
|
|
1885
|
-
};
|
|
1886
|
-
}
|
|
1802
|
+
await runMcpPaymentServer({
|
|
1803
|
+
payer,
|
|
1804
|
+
signer,
|
|
1805
|
+
facilitatorBaseUrl,
|
|
1806
|
+
defaultMaxAmountRawUsdc
|
|
1887
1807
|
});
|
|
1888
|
-
try {
|
|
1889
|
-
await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
|
|
1890
|
-
console.error("[subly-mcp] wallet registered and synced at the facilitator");
|
|
1891
|
-
} catch (error) {
|
|
1892
|
-
console.error(
|
|
1893
|
-
`[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
|
|
1894
|
-
);
|
|
1895
|
-
}
|
|
1896
|
-
var transport = new StdioServerTransport();
|
|
1897
|
-
await server.connect(transport);
|
|
1898
|
-
console.error(
|
|
1899
|
-
`[subly-mcp] ready: agent wallet ${signer.walletAddress}, facilitator ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc)} USDC`
|
|
1900
|
-
);
|