@subly_fi/pay 0.1.0 → 0.2.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/deposit.js +23 -33
- package/dist/mcp-server.js +490 -663
- package/dist/pay.js +22 -22
- package/dist/withdraw.js +22 -32
- package/package.json +4 -2
package/dist/mcp-server.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
1
|
+
// demo/mcp-server.ts
|
|
3
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
4
|
import {
|
|
@@ -7,11 +6,11 @@ import {
|
|
|
7
6
|
ListToolsRequestSchema
|
|
8
7
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
8
|
|
|
10
|
-
//
|
|
9
|
+
// src/client/agent-wallet-signer.ts
|
|
11
10
|
import { signBytes } from "@solana/kit";
|
|
12
11
|
import bs584 from "bs58";
|
|
13
12
|
|
|
14
|
-
//
|
|
13
|
+
// src/solana/tx.ts
|
|
15
14
|
import bs58 from "bs58";
|
|
16
15
|
import {
|
|
17
16
|
appendTransactionMessageInstructions,
|
|
@@ -26,9 +25,8 @@ import {
|
|
|
26
25
|
setTransactionMessageLifetimeUsingBlockhash
|
|
27
26
|
} from "@solana/kit";
|
|
28
27
|
|
|
29
|
-
//
|
|
28
|
+
// src/lib/hash.ts
|
|
30
29
|
import { createHash } from "node:crypto";
|
|
31
|
-
var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
32
30
|
function sha256TaggedHex(data) {
|
|
33
31
|
return `sha256-${createHash("sha256").update(data).digest("hex")}`;
|
|
34
32
|
}
|
|
@@ -52,7 +50,7 @@ function hashStableJson(value) {
|
|
|
52
50
|
return sha256TaggedHex(stableStringify(value));
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
//
|
|
53
|
+
// src/solana/tx.ts
|
|
56
54
|
function decodeSerializedTransaction(serializedBase64) {
|
|
57
55
|
return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
|
|
58
56
|
}
|
|
@@ -72,11 +70,11 @@ function signatureBase58ForSigner(transaction, signer2) {
|
|
|
72
70
|
return bs58.encode(signature);
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
//
|
|
73
|
+
// src/client/transaction-intent-validator.ts
|
|
76
74
|
import bs583 from "bs58";
|
|
77
75
|
import { getCompiledTransactionMessageDecoder } from "@solana/kit";
|
|
78
76
|
|
|
79
|
-
//
|
|
77
|
+
// src/config/constants.ts
|
|
80
78
|
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
81
79
|
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
82
80
|
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
@@ -92,7 +90,7 @@ var SUBLY_VAULT = {
|
|
|
92
90
|
};
|
|
93
91
|
var USDC_DECIMALS = 6;
|
|
94
92
|
|
|
95
|
-
//
|
|
93
|
+
// src/domain/request-binding.ts
|
|
96
94
|
function computeRequestBindingHash(fields) {
|
|
97
95
|
return hashStableJson({
|
|
98
96
|
sellerRequestId: fields.sellerRequestId,
|
|
@@ -107,7 +105,7 @@ function computeRequestBindingHash(fields) {
|
|
|
107
105
|
});
|
|
108
106
|
}
|
|
109
107
|
|
|
110
|
-
//
|
|
108
|
+
// src/lib/associated-token-account.ts
|
|
111
109
|
import { createHash as createHash2 } from "node:crypto";
|
|
112
110
|
import bs582 from "bs58";
|
|
113
111
|
var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
|
|
@@ -125,12 +123,12 @@ function deriveAssociatedTokenAddress(params) {
|
|
|
125
123
|
"associatedTokenProgramId"
|
|
126
124
|
);
|
|
127
125
|
for (let bump = 255; bump >= 0; bump -= 1) {
|
|
128
|
-
const
|
|
126
|
+
const address3 = createProgramAddress(
|
|
129
127
|
[owner, tokenProgramId, mint, Uint8Array.of(bump)],
|
|
130
128
|
associatedTokenProgramId
|
|
131
129
|
);
|
|
132
|
-
if (
|
|
133
|
-
return bs582.encode(
|
|
130
|
+
if (address3 !== null) {
|
|
131
|
+
return bs582.encode(address3);
|
|
134
132
|
}
|
|
135
133
|
}
|
|
136
134
|
throw new Error("Unable to derive associated token account address");
|
|
@@ -199,7 +197,7 @@ function modPow(base, exponent, modulus) {
|
|
|
199
197
|
return result;
|
|
200
198
|
}
|
|
201
199
|
|
|
202
|
-
//
|
|
200
|
+
// src/client/transaction-intent-validator.ts
|
|
203
201
|
var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
|
|
204
202
|
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
|
|
205
203
|
var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
@@ -885,7 +883,7 @@ function readShortVec(bytes, startOffset) {
|
|
|
885
883
|
return null;
|
|
886
884
|
}
|
|
887
885
|
|
|
888
|
-
//
|
|
886
|
+
// src/client/agent-wallet-signer.ts
|
|
889
887
|
var LocalKeypairAgentWalletSigner = class {
|
|
890
888
|
validationMode = "structured_intent_transaction";
|
|
891
889
|
keyPairSigner;
|
|
@@ -955,7 +953,104 @@ var LocalKeypairAgentWalletSigner = class {
|
|
|
955
953
|
}
|
|
956
954
|
};
|
|
957
955
|
|
|
958
|
-
//
|
|
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");
|
|
965
|
+
}
|
|
966
|
+
function walletAuthMessage(params) {
|
|
967
|
+
return new TextEncoder().encode(
|
|
968
|
+
`subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
|
|
969
|
+
params.rawBody
|
|
970
|
+
)}:${params.signedAtMs}`
|
|
971
|
+
);
|
|
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";
|
|
998
|
+
}
|
|
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" });
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/client/standard-x402-factory.ts
|
|
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
|
|
959
1054
|
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
960
1055
|
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
961
1056
|
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
@@ -995,12 +1090,143 @@ async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
|
995
1090
|
return result;
|
|
996
1091
|
}
|
|
997
1092
|
|
|
998
|
-
//
|
|
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
|
|
999
1228
|
import { z } from "zod";
|
|
1000
1229
|
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
1230
|
var MAX_HEADER_JSON_BYTES = 16384;
|
|
1005
1231
|
var X402HeaderError = class extends Error {
|
|
1006
1232
|
reason;
|
|
@@ -1047,16 +1273,6 @@ var sublyPaymentPayloadSchema = z.object({
|
|
|
1047
1273
|
temporarySettlementSignature: z.string().min(1).max(128)
|
|
1048
1274
|
})
|
|
1049
1275
|
}).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
1276
|
function decodeX402Header(headerValue) {
|
|
1061
1277
|
if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
|
|
1062
1278
|
throw new X402HeaderError(
|
|
@@ -1074,375 +1290,249 @@ function decodeX402Header(headerValue) {
|
|
|
1074
1290
|
);
|
|
1075
1291
|
}
|
|
1076
1292
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1293
|
+
|
|
1294
|
+
// src/x402/standard-requirements.ts
|
|
1295
|
+
import { z as z2 } from "zod";
|
|
1296
|
+
var STANDARD_EXACT_SCHEME = "exact";
|
|
1297
|
+
var standardExactRequirementSchema = z2.object({
|
|
1298
|
+
scheme: z2.literal(STANDARD_EXACT_SCHEME),
|
|
1299
|
+
/** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
|
|
1300
|
+
network: z2.string().min(1),
|
|
1301
|
+
/** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
|
|
1302
|
+
asset: z2.string().min(1),
|
|
1303
|
+
/** Exact price in the asset's atomic units, as a decimal string. */
|
|
1304
|
+
amount: z2.string().regex(/^[1-9]\d*$/),
|
|
1305
|
+
/** Recipient wallet; the transfer destination ATA is derived from it. */
|
|
1306
|
+
payTo: z2.string().min(1),
|
|
1307
|
+
maxTimeoutSeconds: z2.number().int().positive().optional(),
|
|
1308
|
+
extra: z2.object({
|
|
1309
|
+
/** Facilitator address that pays the tx fee (gas sponsorship). */
|
|
1310
|
+
feePayer: z2.string().min(1).optional()
|
|
1311
|
+
}).loose().optional()
|
|
1312
|
+
}).loose();
|
|
1313
|
+
var standardPaymentRequiredSchema = z2.object({
|
|
1314
|
+
x402Version: z2.number().int(),
|
|
1315
|
+
accepts: z2.array(z2.unknown()),
|
|
1316
|
+
error: z2.string().optional(),
|
|
1317
|
+
resource: z2.object({ url: z2.string().optional() }).loose().optional()
|
|
1318
|
+
}).loose();
|
|
1319
|
+
var StandardX402ChallengeError = class extends Error {
|
|
1320
|
+
reason;
|
|
1321
|
+
constructor(reason, message) {
|
|
1322
|
+
super(message);
|
|
1323
|
+
this.name = "StandardX402ChallengeError";
|
|
1324
|
+
this.reason = reason;
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
function parseStandardChallenge(challenge) {
|
|
1328
|
+
const parsed = standardPaymentRequiredSchema.safeParse(challenge);
|
|
1079
1329
|
if (!parsed.success) {
|
|
1080
|
-
throw new
|
|
1330
|
+
throw new StandardX402ChallengeError(
|
|
1081
1331
|
"invalid_payment_required",
|
|
1082
|
-
"
|
|
1332
|
+
"Response is not a valid x402 PaymentRequired object"
|
|
1083
1333
|
);
|
|
1084
1334
|
}
|
|
1085
|
-
const
|
|
1086
|
-
const requirement =
|
|
1087
|
-
|
|
1335
|
+
const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
|
|
1336
|
+
const requirement = standardExactRequirementSchema.safeParse(candidate);
|
|
1337
|
+
if (!requirement.success) {
|
|
1338
|
+
return [];
|
|
1339
|
+
}
|
|
1340
|
+
return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
|
|
1088
1341
|
});
|
|
1089
|
-
return { paymentRequired: parsed.data,
|
|
1342
|
+
return { paymentRequired: parsed.data, solanaExactRequirements };
|
|
1090
1343
|
}
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
|
|
1344
|
+
function decodeStandardPaymentRequiredHeader(headerValue) {
|
|
1345
|
+
let decoded;
|
|
1346
|
+
try {
|
|
1347
|
+
decoded = decodeX402Header(headerValue);
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
throw new StandardX402ChallengeError(
|
|
1350
|
+
error instanceof X402HeaderError ? error.reason : "invalid_header",
|
|
1351
|
+
"Cannot decode the payment-required header"
|
|
1352
|
+
);
|
|
1094
1353
|
}
|
|
1095
|
-
return
|
|
1096
|
-
|
|
1097
|
-
|
|
1354
|
+
return parseStandardChallenge(decoded);
|
|
1355
|
+
}
|
|
1356
|
+
function selectPayableSolanaRequirement(requirements, options) {
|
|
1357
|
+
const network = options?.network ?? SOLANA_MAINNET_NETWORK;
|
|
1358
|
+
const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1359
|
+
const requirement = requirements.find(
|
|
1360
|
+
(candidate) => candidate.network === network && candidate.asset === usdcMint
|
|
1361
|
+
) ?? null;
|
|
1362
|
+
if (requirement === null) {
|
|
1363
|
+
throw new StandardX402ChallengeError(
|
|
1364
|
+
"no_payable_requirement",
|
|
1365
|
+
`The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
return {
|
|
1369
|
+
requirement,
|
|
1370
|
+
amountRawUsdc: BigInt(requirement.amount),
|
|
1371
|
+
payTo: requirement.payTo,
|
|
1372
|
+
feePayer: requirement.extra?.feePayer ?? null
|
|
1373
|
+
};
|
|
1098
1374
|
}
|
|
1099
1375
|
|
|
1100
|
-
//
|
|
1101
|
-
var
|
|
1376
|
+
// src/client/standard-x402-payer.ts
|
|
1377
|
+
var StandardX402PayError = class extends Error {
|
|
1102
1378
|
constructor(reason, message, detail = null) {
|
|
1103
1379
|
super(message);
|
|
1104
1380
|
this.reason = reason;
|
|
1105
1381
|
this.detail = detail;
|
|
1106
|
-
this.name = "
|
|
1382
|
+
this.name = "StandardX402PayError";
|
|
1107
1383
|
}
|
|
1108
1384
|
reason;
|
|
1109
1385
|
detail;
|
|
1110
1386
|
};
|
|
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;
|
|
1387
|
+
var StandardX402Payer = class {
|
|
1388
|
+
realizer;
|
|
1389
|
+
x402Fetch;
|
|
1390
|
+
probeFetch;
|
|
1127
1391
|
defaultMaxAmountRawUsdc;
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
maxBodyChars;
|
|
1131
|
-
nowMs;
|
|
1132
|
-
stateStore;
|
|
1133
|
-
pending = /* @__PURE__ */ new Map();
|
|
1134
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
1392
|
+
network;
|
|
1393
|
+
usdcMint;
|
|
1135
1394
|
constructor(config) {
|
|
1136
|
-
this.
|
|
1137
|
-
this.
|
|
1138
|
-
this.
|
|
1139
|
-
this.paymentStatusFor = config.paymentStatusFor ?? (async () => "indeterminate");
|
|
1395
|
+
this.realizer = config.realizer;
|
|
1396
|
+
this.x402Fetch = config.x402Fetch;
|
|
1397
|
+
this.probeFetch = config.probeFetch ?? fetch;
|
|
1140
1398
|
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
|
-
};
|
|
1210
|
-
}
|
|
1211
|
-
const challengeHeader = first.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1212
|
-
if (challengeHeader === null) {
|
|
1213
|
-
throw new PaidFetchError(
|
|
1214
|
-
"invalid_challenge",
|
|
1215
|
-
"402 response is missing the PAYMENT-REQUIRED header"
|
|
1216
|
-
);
|
|
1217
|
-
}
|
|
1218
|
-
const requirement = decodePaymentRequiredHeader(challengeHeader).sublyRequirements[0];
|
|
1219
|
-
if (requirement === void 0) {
|
|
1220
|
-
throw new PaidFetchError(
|
|
1221
|
-
"invalid_challenge",
|
|
1222
|
-
"402 challenge contains no subly-yield-exact requirement"
|
|
1223
|
-
);
|
|
1399
|
+
this.network = config.network ?? SOLANA_MAINNET_NETWORK;
|
|
1400
|
+
this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1401
|
+
}
|
|
1402
|
+
async pay(input) {
|
|
1403
|
+
const init = {
|
|
1404
|
+
method: input.method ?? "GET",
|
|
1405
|
+
...input.body === void 0 ? {} : { body: input.body },
|
|
1406
|
+
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
1407
|
+
};
|
|
1408
|
+
const probe = await this.probeFetch(input.url, init);
|
|
1409
|
+
if (probe.status !== 402) {
|
|
1410
|
+
return { paid: false, status: probe.status, body: await probe.text() };
|
|
1224
1411
|
}
|
|
1225
|
-
const
|
|
1226
|
-
const
|
|
1227
|
-
if (amountRawUsdc >
|
|
1228
|
-
throw new
|
|
1412
|
+
const selected = await this.selectRequirement(probe);
|
|
1413
|
+
const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
|
|
1414
|
+
if (selected.amountRawUsdc > cap) {
|
|
1415
|
+
throw new StandardX402PayError(
|
|
1229
1416
|
"amount_exceeds_client_cap",
|
|
1230
|
-
`the challenge demands ${
|
|
1231
|
-
{
|
|
1232
|
-
amountRawUsdc: requirement.amountRawUsdc,
|
|
1233
|
-
maxAmountRawUsdc: maxAmountRawUsdc.toString(),
|
|
1234
|
-
payTo: requirement.payTo
|
|
1235
|
-
}
|
|
1417
|
+
`the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
|
|
1418
|
+
{ amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
|
|
1236
1419
|
);
|
|
1237
1420
|
}
|
|
1238
|
-
|
|
1239
|
-
const { headerValue, paymentId } = await this.signatureBuilder.buildPaymentSignatureHeader({
|
|
1240
|
-
paymentRequiredHeader: challengeHeader,
|
|
1241
|
-
httpMethod: "GET",
|
|
1242
|
-
url
|
|
1243
|
-
});
|
|
1244
|
-
const entry = {
|
|
1245
|
-
headerValue,
|
|
1246
|
-
paymentId,
|
|
1247
|
-
amountUsdc: formatRawUsdcAmount(amountRawUsdc),
|
|
1248
|
-
payTo: requirement.payTo,
|
|
1249
|
-
challengeAtMs,
|
|
1250
|
-
unresolved: false
|
|
1251
|
-
};
|
|
1252
|
-
this.track(url, entry);
|
|
1253
|
-
return this.deliver(url, entry, { retried: false, budgetBefore });
|
|
1254
|
-
}
|
|
1255
|
-
/**
|
|
1256
|
-
* Sends the signed PAYMENT-SIGNATURE retry. The tracked entry is removed
|
|
1257
|
-
* only on confirmed delivery (200). A fresh 402 proves the signature can no
|
|
1258
|
-
* longer settle, so the entry is kept as an unresolved marker; every other
|
|
1259
|
-
* failure keeps it retryable so the next call reuses the same signature.
|
|
1260
|
-
*/
|
|
1261
|
-
async deliver(url, entry, params) {
|
|
1262
|
-
let second;
|
|
1263
|
-
let secondText;
|
|
1421
|
+
let realized;
|
|
1264
1422
|
try {
|
|
1265
|
-
|
|
1266
|
-
|
|
1423
|
+
realized = await this.realizer.ensureUsdcAvailable({
|
|
1424
|
+
amountRawUsdc: selected.amountRawUsdc
|
|
1267
1425
|
});
|
|
1268
|
-
secondText = await second.text();
|
|
1269
1426
|
} catch (error) {
|
|
1270
|
-
throw new
|
|
1271
|
-
"
|
|
1272
|
-
`
|
|
1273
|
-
|
|
1427
|
+
throw new StandardX402PayError(
|
|
1428
|
+
"realize_failed",
|
|
1429
|
+
`could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
|
|
1430
|
+
error
|
|
1274
1431
|
);
|
|
1275
1432
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
status
|
|
1282
|
-
|
|
1283
|
-
...params.retried ? { retriedPendingPayment: true } : {},
|
|
1284
|
-
payment: {
|
|
1285
|
-
amountUsdc: entry.amountUsdc,
|
|
1286
|
-
payTo: entry.payTo,
|
|
1287
|
-
paymentId: entry.paymentId,
|
|
1288
|
-
transaction,
|
|
1289
|
-
solscanUrl: transaction === null ? null : `https://solscan.io/tx/${transaction}`,
|
|
1290
|
-
budgetBefore: params.budgetBefore,
|
|
1291
|
-
budgetAfter: await this.fetchBudget()
|
|
1292
|
-
}
|
|
1293
|
-
};
|
|
1294
|
-
}
|
|
1295
|
-
if (second.status === 402) {
|
|
1296
|
-
entry.unresolved = true;
|
|
1297
|
-
this.persist();
|
|
1298
|
-
throw new PaidFetchError(
|
|
1299
|
-
"payment_outcome_unknown",
|
|
1300
|
-
`the seller no longer accepts the signed payment (paymentId=${entry.paymentId}); it may or may not have settled. Verify the payment before purchasing again; to pay again anyway, call this tool with forceNewPayment=true.`,
|
|
1301
|
-
{ paymentId: entry.paymentId }
|
|
1433
|
+
const response = await this.x402Fetch(input.url, init);
|
|
1434
|
+
const bodyText = await response.text();
|
|
1435
|
+
if (response.status !== 200) {
|
|
1436
|
+
throw new StandardX402PayError(
|
|
1437
|
+
"delivery_failed",
|
|
1438
|
+
`the x402 payment did not deliver (status ${response.status})`,
|
|
1439
|
+
{ status: response.status, body: bodyText }
|
|
1302
1440
|
);
|
|
1303
1441
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1442
|
+
return {
|
|
1443
|
+
paid: true,
|
|
1444
|
+
status: response.status,
|
|
1445
|
+
body: bodyText,
|
|
1446
|
+
payment: {
|
|
1447
|
+
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1448
|
+
payTo: selected.payTo,
|
|
1449
|
+
feePayer: selected.feePayer,
|
|
1450
|
+
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1451
|
+
realizeTxSignature: realized.txSignature
|
|
1311
1452
|
}
|
|
1312
|
-
|
|
1453
|
+
};
|
|
1313
1454
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1455
|
+
/** Reads the challenge from the header (preferred) or the JSON body. */
|
|
1456
|
+
async selectRequirement(probe) {
|
|
1457
|
+
const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1458
|
+
let requirements;
|
|
1459
|
+
try {
|
|
1460
|
+
if (header !== null) {
|
|
1461
|
+
requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
|
|
1462
|
+
} else {
|
|
1463
|
+
requirements = parseStandardChallenge(
|
|
1464
|
+
await probe.json()
|
|
1465
|
+
).solanaExactRequirements;
|
|
1319
1466
|
}
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
throw new StandardX402PayError(
|
|
1469
|
+
error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
|
|
1470
|
+
"could not parse the x402 402 challenge",
|
|
1471
|
+
error
|
|
1472
|
+
);
|
|
1320
1473
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1474
|
+
try {
|
|
1475
|
+
return selectPayableSolanaRequirement(requirements, {
|
|
1476
|
+
network: this.network,
|
|
1477
|
+
usdcMint: this.usdcMint
|
|
1478
|
+
});
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
throw new StandardX402PayError(
|
|
1481
|
+
"no_payable_requirement",
|
|
1482
|
+
error instanceof Error ? error.message : String(error),
|
|
1483
|
+
error
|
|
1484
|
+
);
|
|
1331
1485
|
}
|
|
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
1486
|
}
|
|
1340
1487
|
};
|
|
1341
|
-
function receiptTransaction(headers) {
|
|
1342
|
-
try {
|
|
1343
|
-
const receiptHeader = headers.get(PAYMENT_RESPONSE_HEADER);
|
|
1344
|
-
if (receiptHeader === null) {
|
|
1345
|
-
return null;
|
|
1346
|
-
}
|
|
1347
|
-
const receipt = decodeX402Header(receiptHeader);
|
|
1348
|
-
return typeof receipt.transaction === "string" && receipt.transaction.length > 0 ? receipt.transaction : null;
|
|
1349
|
-
} catch {
|
|
1350
|
-
return null;
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
1488
|
|
|
1354
|
-
//
|
|
1355
|
-
import {
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
function sha256Hex(data) {
|
|
1362
|
-
return createHash3("sha256").update(data, "utf8").digest("hex");
|
|
1363
|
-
}
|
|
1364
|
-
function walletAuthMessage(params) {
|
|
1365
|
-
return new TextEncoder().encode(
|
|
1366
|
-
`subly-api:${params.method.toUpperCase()}:${params.path}:${sha256Hex(
|
|
1367
|
-
params.rawBody
|
|
1368
|
-
)}:${params.signedAtMs}`
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// ../../src/client/wallet-auth-headers.ts
|
|
1373
|
-
async function walletAuthHeaders(params) {
|
|
1374
|
-
const signedAtMs = String(Date.now());
|
|
1375
|
-
const message = walletAuthMessage({
|
|
1376
|
-
method: params.method,
|
|
1377
|
-
path: new URL(params.url).pathname,
|
|
1378
|
-
rawBody: params.body ?? "",
|
|
1379
|
-
signedAtMs
|
|
1380
|
-
});
|
|
1489
|
+
// src/client/web3-wallet-adapter.ts
|
|
1490
|
+
import { Keypair } from "@solana/web3.js";
|
|
1491
|
+
function createX402WalletAdapter(secretKey) {
|
|
1492
|
+
if (secretKey.length !== 64) {
|
|
1493
|
+
throw new Error("agent secret key must be 64 bytes");
|
|
1494
|
+
}
|
|
1495
|
+
const keypair = Keypair.fromSecretKey(secretKey);
|
|
1381
1496
|
return {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1497
|
+
address: keypair.publicKey.toBase58(),
|
|
1498
|
+
signTransaction: async (tx) => {
|
|
1499
|
+
tx.sign([keypair]);
|
|
1500
|
+
return tx;
|
|
1501
|
+
}
|
|
1385
1502
|
};
|
|
1386
1503
|
}
|
|
1387
1504
|
|
|
1388
|
-
//
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
};
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
const serialized = JSON.stringify(body);
|
|
1406
|
-
const response = await fetchImpl(url, {
|
|
1407
|
-
method: "POST",
|
|
1408
|
-
headers: {
|
|
1409
|
-
...await walletAuthHeaders({
|
|
1410
|
-
signer: params.signer,
|
|
1411
|
-
method: "POST",
|
|
1412
|
-
url,
|
|
1413
|
-
body: serialized
|
|
1414
|
-
}),
|
|
1415
|
-
"content-type": "application/json"
|
|
1416
|
-
},
|
|
1417
|
-
body: serialized
|
|
1418
|
-
});
|
|
1419
|
-
if (response.status !== 200) {
|
|
1420
|
-
let detail = null;
|
|
1421
|
-
try {
|
|
1422
|
-
detail = await response.json();
|
|
1423
|
-
} catch {
|
|
1424
|
-
detail = null;
|
|
1425
|
-
}
|
|
1426
|
-
throw new OnboardingError(
|
|
1427
|
-
step,
|
|
1428
|
-
`wallet onboarding ${step} failed with ${response.status}`,
|
|
1429
|
-
detail
|
|
1430
|
-
);
|
|
1431
|
-
}
|
|
1432
|
-
};
|
|
1433
|
-
const wallet = params.signer.walletAddress;
|
|
1434
|
-
await post("register", "/v1/wallets/agent", {
|
|
1435
|
-
wallet,
|
|
1436
|
-
signingPolicyId: SELF_SERVE_POLICY_ID,
|
|
1437
|
-
signingMode: "non_interactive",
|
|
1438
|
-
signerValidationMode: params.signer.validationMode,
|
|
1439
|
-
signerProvider: "local-keypair",
|
|
1440
|
-
activateForPayments: true
|
|
1505
|
+
// src/client/standard-x402-factory.ts
|
|
1506
|
+
function createStandardX402Payer(config) {
|
|
1507
|
+
const realizer = new RelayerYieldRealizer({
|
|
1508
|
+
facilitatorBaseUrl: config.facilitatorBaseUrl,
|
|
1509
|
+
signer: config.signer,
|
|
1510
|
+
rpc: config.rpc
|
|
1511
|
+
});
|
|
1512
|
+
const client = createX402Client({
|
|
1513
|
+
wallet: createX402WalletAdapter(config.agentSecretKey),
|
|
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
|
|
1441
1522
|
});
|
|
1442
|
-
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
|
|
1443
1523
|
}
|
|
1444
1524
|
|
|
1445
|
-
//
|
|
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
|
|
1446
1536
|
import { readFileSync } from "node:fs";
|
|
1447
1537
|
import bs586 from "bs58";
|
|
1448
1538
|
import {
|
|
@@ -1466,314 +1556,76 @@ async function loadKeyPairSigner(params) {
|
|
|
1466
1556
|
}
|
|
1467
1557
|
throw new Error(`${label} keypair is not configured`);
|
|
1468
1558
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
throw new Error("SOLANA_RPC_URL must be configured");
|
|
1476
|
-
}
|
|
1477
|
-
return createSolanaRpc(url);
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
// ../../src/x402/client.ts
|
|
1481
|
-
var X402ClientError = class extends Error {
|
|
1482
|
-
reason;
|
|
1483
|
-
detail;
|
|
1484
|
-
constructor(reason, message, detail) {
|
|
1485
|
-
super(message);
|
|
1486
|
-
this.name = "X402ClientError";
|
|
1487
|
-
this.reason = reason;
|
|
1488
|
-
this.detail = detail;
|
|
1489
|
-
}
|
|
1490
|
-
};
|
|
1491
|
-
var SublyX402Client = class {
|
|
1492
|
-
config;
|
|
1493
|
-
signer;
|
|
1494
|
-
lookupTablesFor;
|
|
1495
|
-
fetchImpl;
|
|
1496
|
-
constructor(config) {
|
|
1497
|
-
this.config = {
|
|
1498
|
-
facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
|
|
1499
|
-
network: config.network ?? SOLANA_MAINNET_NETWORK
|
|
1500
|
-
};
|
|
1501
|
-
this.signer = config.signer;
|
|
1502
|
-
this.lookupTablesFor = config.lookupTablesFor ?? null;
|
|
1503
|
-
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1504
|
-
}
|
|
1505
|
-
/**
|
|
1506
|
-
* Builds the PAYMENT-SIGNATURE header for a 402 challenge. The request
|
|
1507
|
-
* method, URL, and body must be exactly the request being retried; they are
|
|
1508
|
-
* bound into the payment and verified again by the seller and facilitator.
|
|
1509
|
-
*/
|
|
1510
|
-
async buildPaymentSignatureHeader(input) {
|
|
1511
|
-
const requirement = this.selectRequirement(input.paymentRequiredHeader);
|
|
1512
|
-
if (requirement.resource !== input.url) {
|
|
1513
|
-
throw new X402ClientError(
|
|
1514
|
-
"resource_mismatch",
|
|
1515
|
-
"PAYMENT-REQUIRED resource does not match the request URL"
|
|
1516
|
-
);
|
|
1517
|
-
}
|
|
1518
|
-
const requestBodyHash = requestBodyHashFor(input.body);
|
|
1519
|
-
const prepared = await this.preparePayment({
|
|
1520
|
-
requirement,
|
|
1521
|
-
httpMethod: input.httpMethod.toUpperCase(),
|
|
1522
|
-
requestBodyHash
|
|
1523
|
-
});
|
|
1524
|
-
const lookupTables = this.lookupTablesFor === null ? void 0 : await this.lookupTablesFor(prepared.intentJson.serializedTransaction);
|
|
1525
|
-
const signed = await this.signer.signPayment({
|
|
1526
|
-
intent: prepared.intentJson.signingIntent,
|
|
1527
|
-
serializedTransaction: prepared.intentJson.serializedTransaction,
|
|
1528
|
-
lookupTables
|
|
1529
|
-
});
|
|
1530
|
-
const payload = {
|
|
1531
|
-
x402Version: X402_VERSION,
|
|
1532
|
-
scheme: requirement.scheme,
|
|
1533
|
-
network: requirement.network,
|
|
1534
|
-
payload: {
|
|
1535
|
-
paymentId: prepared.paymentId,
|
|
1536
|
-
requestBindingHash: prepared.requestBindingHash,
|
|
1537
|
-
preparedMessageHash: prepared.preparedMessageHash,
|
|
1538
|
-
serializedTransaction: signed.serializedTransaction,
|
|
1539
|
-
agentSignature: signed.agentSignature,
|
|
1540
|
-
temporarySettlementSignature: prepared.temporarySettlementSignature
|
|
1541
|
-
}
|
|
1542
|
-
};
|
|
1543
|
-
return {
|
|
1544
|
-
headerValue: encodeX402Header(payload),
|
|
1545
|
-
paymentId: prepared.paymentId
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
/**
|
|
1549
|
-
* Convenience wrapper: performs the request, and on a 402 with a Subly
|
|
1550
|
-
* requirement pays from vault yield and retries once.
|
|
1551
|
-
*/
|
|
1552
|
-
async fetchWithPayment(url, init) {
|
|
1553
|
-
const first = await this.fetchImpl(url, init);
|
|
1554
|
-
if (first.status !== 402) {
|
|
1555
|
-
return first;
|
|
1556
|
-
}
|
|
1557
|
-
const challenge = first.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1558
|
-
if (challenge === null) {
|
|
1559
|
-
return first;
|
|
1559
|
+
function loadSecretKeyBytes(params) {
|
|
1560
|
+
const { base58Secret, jsonFilePath, label } = params;
|
|
1561
|
+
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
1562
|
+
const bytes = bs586.decode(base58Secret);
|
|
1563
|
+
if (bytes.length !== 64) {
|
|
1564
|
+
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
1560
1565
|
}
|
|
1561
|
-
|
|
1562
|
-
paymentRequiredHeader: challenge,
|
|
1563
|
-
httpMethod: init?.method ?? "GET",
|
|
1564
|
-
url,
|
|
1565
|
-
body: init?.body ?? null
|
|
1566
|
-
});
|
|
1567
|
-
return this.fetchImpl(url, {
|
|
1568
|
-
...init,
|
|
1569
|
-
headers: {
|
|
1570
|
-
...init?.headers,
|
|
1571
|
-
[PAYMENT_SIGNATURE_HEADER]: headerValue
|
|
1572
|
-
}
|
|
1573
|
-
});
|
|
1566
|
+
return bytes;
|
|
1574
1567
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
paymentRequiredHeader
|
|
1580
|
-
).sublyRequirements;
|
|
1581
|
-
} catch (error) {
|
|
1582
|
-
throw new X402ClientError(
|
|
1583
|
-
error instanceof X402HeaderError ? error.reason : "invalid_challenge",
|
|
1584
|
-
"Cannot decode the PAYMENT-REQUIRED header"
|
|
1585
|
-
);
|
|
1586
|
-
}
|
|
1587
|
-
const selected = sublyRequirements.find(
|
|
1588
|
-
(candidate) => candidate.network === this.config.network && candidate.extra.vault === SUBLY_VAULT.address && candidate.extra.shareMint === SUBLY_VAULT.shareMint && candidate.asset === SUBLY_VAULT.usdcMint
|
|
1589
|
-
) ?? null;
|
|
1590
|
-
if (selected === null) {
|
|
1591
|
-
throw new X402ClientError(
|
|
1592
|
-
"no_supported_requirement",
|
|
1593
|
-
"The 402 challenge contains no supported subly-yield-exact requirement"
|
|
1594
|
-
);
|
|
1595
|
-
}
|
|
1596
|
-
return selected;
|
|
1597
|
-
}
|
|
1598
|
-
async preparePayment(input) {
|
|
1599
|
-
const { requirement } = input;
|
|
1600
|
-
const wallet = this.signer.walletAddress;
|
|
1601
|
-
const prepareUrl = `${this.config.facilitatorBaseUrl}/v1/payments/prepare`;
|
|
1602
|
-
const prepareBody = JSON.stringify({
|
|
1603
|
-
wallet,
|
|
1604
|
-
scheme: requirement.scheme,
|
|
1605
|
-
network: requirement.network,
|
|
1606
|
-
vault: requirement.extra.vault,
|
|
1607
|
-
shareMint: requirement.extra.shareMint,
|
|
1608
|
-
asset: requirement.asset,
|
|
1609
|
-
seller: requirement.extra.seller,
|
|
1610
|
-
sellerRequestId: requirement.extra.sellerRequestId,
|
|
1611
|
-
httpMethod: input.httpMethod,
|
|
1612
|
-
canonicalResourceUrl: requirement.resource,
|
|
1613
|
-
requestBodyHash: input.requestBodyHash,
|
|
1614
|
-
amountRawUsdc: requirement.amountRawUsdc,
|
|
1615
|
-
payTo: requirement.payTo,
|
|
1616
|
-
sellerUsdcAta: requirement.extra.sellerUsdcAta,
|
|
1617
|
-
dustRecipientUsdcAta: deriveAssociatedTokenAddress({
|
|
1618
|
-
owner: wallet,
|
|
1619
|
-
mint: SUBLY_VAULT.usdcMint
|
|
1620
|
-
})
|
|
1621
|
-
});
|
|
1622
|
-
const response = await this.fetchImpl(prepareUrl, {
|
|
1623
|
-
method: "POST",
|
|
1624
|
-
headers: {
|
|
1625
|
-
...await walletAuthHeaders({
|
|
1626
|
-
signer: this.signer,
|
|
1627
|
-
method: "POST",
|
|
1628
|
-
url: prepareUrl,
|
|
1629
|
-
body: prepareBody
|
|
1630
|
-
}),
|
|
1631
|
-
"content-type": "application/json"
|
|
1632
|
-
},
|
|
1633
|
-
body: prepareBody
|
|
1634
|
-
});
|
|
1635
|
-
const body = await response.json();
|
|
1636
|
-
if (response.status !== 200) {
|
|
1637
|
-
const error = body.error;
|
|
1638
|
-
throw new X402ClientError(
|
|
1639
|
-
typeof error?.code === "string" ? error.code : "prepare_failed",
|
|
1640
|
-
"Payment preparation failed at the facilitator",
|
|
1641
|
-
body
|
|
1642
|
-
);
|
|
1643
|
-
}
|
|
1644
|
-
const prepared = body;
|
|
1645
|
-
if (typeof prepared.paymentId !== "string" || typeof prepared.preparedMessageHash !== "string" || typeof prepared.intentJson?.serializedTransaction !== "string" || typeof prepared.intentJson?.signingIntent !== "object") {
|
|
1646
|
-
throw new X402ClientError(
|
|
1647
|
-
"invalid_prepare_response",
|
|
1648
|
-
"Facilitator prepare response is missing required fields"
|
|
1649
|
-
);
|
|
1568
|
+
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1569
|
+
const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
|
|
1570
|
+
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1571
|
+
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1650
1572
|
}
|
|
1651
|
-
return
|
|
1573
|
+
return Uint8Array.from(raw);
|
|
1652
1574
|
}
|
|
1653
|
-
};
|
|
1654
|
-
|
|
1655
|
-
// ../../demo/shared.ts
|
|
1656
|
-
function fail(message) {
|
|
1657
|
-
console.error(message);
|
|
1658
|
-
process.exit(1);
|
|
1575
|
+
throw new Error(`${label} keypair is not configured`);
|
|
1659
1576
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
return value;
|
|
1577
|
+
|
|
1578
|
+
// src/solana/rpc.ts
|
|
1579
|
+
import { createSolanaRpc } from "@solana/kit";
|
|
1580
|
+
function createRpc(url) {
|
|
1581
|
+
return createSolanaRpc(url);
|
|
1666
1582
|
}
|
|
1667
1583
|
|
|
1668
|
-
//
|
|
1584
|
+
// demo/mcp-server.ts
|
|
1669
1585
|
var TOOL_NAME = "fetch_with_subly_payment";
|
|
1670
1586
|
var DEFAULT_MAX_AMOUNT_RAW_USDC = 10000n;
|
|
1671
|
-
|
|
1672
|
-
var
|
|
1587
|
+
var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
|
|
1588
|
+
var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
|
|
1673
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);
|
|
1674
1590
|
var keyPairSigner = await loadKeyPairSigner({
|
|
1675
1591
|
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1676
1592
|
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1677
1593
|
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1678
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
|
+
});
|
|
1679
1600
|
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
1680
|
-
var rpc =
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
return null;
|
|
1689
|
-
}
|
|
1690
|
-
const body = await response.json();
|
|
1691
|
-
if (body.budget === void 0) {
|
|
1692
|
-
return null;
|
|
1693
|
-
}
|
|
1694
|
-
return {
|
|
1695
|
-
positionValueUsdc: formatRawUsdcAmount(body.budget.positionValueRawUsdc),
|
|
1696
|
-
spendableYieldUsdc: formatRawUsdcAmount(
|
|
1697
|
-
body.budget.spendableYieldRawUsdc
|
|
1698
|
-
)
|
|
1699
|
-
};
|
|
1700
|
-
} catch {
|
|
1701
|
-
return null;
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
async function paymentStatusFor(paymentId) {
|
|
1705
|
-
try {
|
|
1706
|
-
const url = `${facilitatorBaseUrl}/v1/payments/${paymentId}`;
|
|
1707
|
-
const response = await fetch(url, {
|
|
1708
|
-
headers: await walletAuthHeaders({ signer, method: "GET", url })
|
|
1709
|
-
});
|
|
1710
|
-
if (response.status !== 200) {
|
|
1711
|
-
return "indeterminate";
|
|
1712
|
-
}
|
|
1713
|
-
const body = await response.json();
|
|
1714
|
-
if (body.status === "settled") {
|
|
1715
|
-
return "settled";
|
|
1716
|
-
}
|
|
1717
|
-
if (body.status === "expired" || body.status === "failed" || body.status === "failed_not_submitted") {
|
|
1718
|
-
return "not_settled";
|
|
1719
|
-
}
|
|
1720
|
-
return "indeterminate";
|
|
1721
|
-
} catch {
|
|
1722
|
-
return "indeterminate";
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
function fileStateStore(path) {
|
|
1726
|
-
return {
|
|
1727
|
-
load() {
|
|
1728
|
-
try {
|
|
1729
|
-
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
1730
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
1731
|
-
} catch {
|
|
1732
|
-
return [];
|
|
1733
|
-
}
|
|
1734
|
-
},
|
|
1735
|
-
save(records) {
|
|
1736
|
-
try {
|
|
1737
|
-
writeFileSync(path, JSON.stringify(records));
|
|
1738
|
-
} catch (error) {
|
|
1739
|
-
console.error(
|
|
1740
|
-
`[subly-mcp] failed to persist pending payments: ${error instanceof Error ? error.message : String(error)}`
|
|
1741
|
-
);
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? "demo/env/mcp-pending-payments.json";
|
|
1747
|
-
var paidFetchService = new PaidFetchService({
|
|
1748
|
-
signatureBuilder: new SublyX402Client({
|
|
1749
|
-
facilitatorBaseUrl,
|
|
1750
|
-
signer,
|
|
1751
|
-
lookupTablesFor: (serializedTransaction) => fetchLookupTablesForTransaction(rpc, serializedTransaction)
|
|
1752
|
-
}),
|
|
1753
|
-
defaultMaxAmountRawUsdc,
|
|
1754
|
-
fetchBudget,
|
|
1755
|
-
paymentStatusFor,
|
|
1756
|
-
stateStore: fileStateStore(pendingStatePath)
|
|
1601
|
+
var rpc = createRpc(rpcUrl);
|
|
1602
|
+
var payer = createStandardX402Payer({
|
|
1603
|
+
facilitatorBaseUrl,
|
|
1604
|
+
signer,
|
|
1605
|
+
agentSecretKey,
|
|
1606
|
+
rpc,
|
|
1607
|
+
rpcUrl,
|
|
1608
|
+
defaultMaxAmountRawUsdc
|
|
1757
1609
|
});
|
|
1758
|
-
var SERVER_INSTRUCTIONS = `Subly lets an agent pay for HTTP 402
|
|
1610
|
+
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.
|
|
1759
1611
|
|
|
1760
1612
|
Before payments can succeed the operator of this server must have, once:
|
|
1761
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.
|
|
1762
|
-
2. Funded that wallet with USDC on Solana mainnet (no SOL needed \u2014 fees are sponsored) and deposited into the vault (see the project's deposit command). The vault minimum deposit is 1 USDC.
|
|
1763
|
-
3. Waited for yield to accrue; a payment needs the price
|
|
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.
|
|
1764
1616
|
|
|
1765
|
-
Then use fetch_with_subly_payment(url) to GET a paid resource: it
|
|
1617
|
+
Then use fetch_with_subly_payment(url) to GET 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.`;
|
|
1766
1618
|
var server = new Server(
|
|
1767
|
-
{ name: "subly-payments", version: "0.
|
|
1619
|
+
{ name: "subly-payments", version: "0.2.0" },
|
|
1768
1620
|
{ capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
1769
1621
|
);
|
|
1770
1622
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1771
1623
|
tools: [
|
|
1772
1624
|
{
|
|
1773
1625
|
name: TOOL_NAME,
|
|
1774
|
-
description: `GET a URL, automatically paying a
|
|
1626
|
+
description: `GET a URL, 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 ${defaultMaxAmountRawUsdc} raw = ${formatRawUsdcAmount(
|
|
1775
1627
|
defaultMaxAmountRawUsdc
|
|
1776
|
-
)} USDC) are refused without paying.
|
|
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.`,
|
|
1777
1629
|
inputSchema: {
|
|
1778
1630
|
type: "object",
|
|
1779
1631
|
properties: {
|
|
@@ -1784,10 +1636,6 @@ server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
|
1784
1636
|
maxAmountRawUsdc: {
|
|
1785
1637
|
type: "string",
|
|
1786
1638
|
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.'
|
|
1787
|
-
},
|
|
1788
|
-
forceNewPayment: {
|
|
1789
|
-
type: "boolean",
|
|
1790
|
-
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)."
|
|
1791
1639
|
}
|
|
1792
1640
|
},
|
|
1793
1641
|
required: ["url"]
|
|
@@ -1840,16 +1688,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1840
1688
|
}
|
|
1841
1689
|
}
|
|
1842
1690
|
try {
|
|
1843
|
-
const result = await
|
|
1691
|
+
const result = await payer.pay({
|
|
1844
1692
|
url,
|
|
1845
|
-
...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc }
|
|
1846
|
-
forceNewPayment: args.forceNewPayment === true
|
|
1693
|
+
...maxAmountRawUsdc === void 0 ? {} : { maxAmountRawUsdc }
|
|
1847
1694
|
});
|
|
1848
1695
|
return {
|
|
1849
1696
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1850
1697
|
};
|
|
1851
1698
|
} catch (error) {
|
|
1852
|
-
if (error instanceof
|
|
1699
|
+
if (error instanceof StandardX402PayError) {
|
|
1853
1700
|
return {
|
|
1854
1701
|
content: [
|
|
1855
1702
|
{
|
|
@@ -1870,26 +1717,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1870
1717
|
isError: true
|
|
1871
1718
|
};
|
|
1872
1719
|
}
|
|
1873
|
-
if (error instanceof X402ClientError) {
|
|
1874
|
-
return {
|
|
1875
|
-
content: [
|
|
1876
|
-
{
|
|
1877
|
-
type: "text",
|
|
1878
|
-
text: JSON.stringify(
|
|
1879
|
-
{
|
|
1880
|
-
paid: false,
|
|
1881
|
-
refused: true,
|
|
1882
|
-
reason: error.reason,
|
|
1883
|
-
detail: error.detail ?? null
|
|
1884
|
-
},
|
|
1885
|
-
null,
|
|
1886
|
-
2
|
|
1887
|
-
)
|
|
1888
|
-
}
|
|
1889
|
-
],
|
|
1890
|
-
isError: true
|
|
1891
|
-
};
|
|
1892
|
-
}
|
|
1893
1720
|
return {
|
|
1894
1721
|
content: [
|
|
1895
1722
|
{
|
|
@@ -1903,7 +1730,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1903
1730
|
});
|
|
1904
1731
|
try {
|
|
1905
1732
|
await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
|
|
1906
|
-
console.error("[subly-mcp] wallet registered and synced at the
|
|
1733
|
+
console.error("[subly-mcp] wallet registered and synced at the relayer");
|
|
1907
1734
|
} catch (error) {
|
|
1908
1735
|
console.error(
|
|
1909
1736
|
`[subly-mcp] wallet onboarding failed (will still serve tools): ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -1912,5 +1739,5 @@ try {
|
|
|
1912
1739
|
var transport = new StdioServerTransport();
|
|
1913
1740
|
await server.connect(transport);
|
|
1914
1741
|
console.error(
|
|
1915
|
-
`[subly-mcp] ready: agent wallet ${signer.walletAddress},
|
|
1742
|
+
`[subly-mcp] ready: agent wallet ${signer.walletAddress}, relayer ${facilitatorBaseUrl}, default cap ${formatRawUsdcAmount(defaultMaxAmountRawUsdc)} USDC`
|
|
1916
1743
|
);
|