@subly_fi/pay 0.2.0 → 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/deposit.js +18 -18
- package/dist/mcp-server.js +442 -378
- package/dist/pay.js +472 -572
- package/dist/withdraw.js +17 -17
- package/package.json +9 -7
package/dist/pay.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
3
|
-
|
|
4
|
-
// src/client/agent-wallet-signer.ts
|
|
1
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
5
2
|
import { signBytes } from "@solana/kit";
|
|
6
3
|
import bs584 from "bs58";
|
|
7
4
|
|
|
8
|
-
// src/solana/tx.ts
|
|
5
|
+
// ../../src/solana/tx.ts
|
|
9
6
|
import bs58 from "bs58";
|
|
10
7
|
import {
|
|
11
8
|
appendTransactionMessageInstructions,
|
|
@@ -20,9 +17,8 @@ import {
|
|
|
20
17
|
setTransactionMessageLifetimeUsingBlockhash
|
|
21
18
|
} from "@solana/kit";
|
|
22
19
|
|
|
23
|
-
// src/lib/hash.ts
|
|
20
|
+
// ../../src/lib/hash.ts
|
|
24
21
|
import { createHash } from "node:crypto";
|
|
25
|
-
var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
26
22
|
function sha256TaggedHex(data) {
|
|
27
23
|
return `sha256-${createHash("sha256").update(data).digest("hex")}`;
|
|
28
24
|
}
|
|
@@ -46,7 +42,7 @@ function hashStableJson(value) {
|
|
|
46
42
|
return sha256TaggedHex(stableStringify(value));
|
|
47
43
|
}
|
|
48
44
|
|
|
49
|
-
// src/solana/tx.ts
|
|
45
|
+
// ../../src/solana/tx.ts
|
|
50
46
|
function decodeSerializedTransaction(serializedBase64) {
|
|
51
47
|
return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
|
|
52
48
|
}
|
|
@@ -66,11 +62,11 @@ function signatureBase58ForSigner(transaction, signer2) {
|
|
|
66
62
|
return bs58.encode(signature);
|
|
67
63
|
}
|
|
68
64
|
|
|
69
|
-
// src/client/transaction-intent-validator.ts
|
|
65
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
70
66
|
import bs583 from "bs58";
|
|
71
67
|
import { getCompiledTransactionMessageDecoder } from "@solana/kit";
|
|
72
68
|
|
|
73
|
-
// src/config/constants.ts
|
|
69
|
+
// ../../src/config/constants.ts
|
|
74
70
|
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
75
71
|
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
76
72
|
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
@@ -86,7 +82,7 @@ var SUBLY_VAULT = {
|
|
|
86
82
|
};
|
|
87
83
|
var USDC_DECIMALS = 6;
|
|
88
84
|
|
|
89
|
-
// src/domain/request-binding.ts
|
|
85
|
+
// ../../src/domain/request-binding.ts
|
|
90
86
|
function computeRequestBindingHash(fields) {
|
|
91
87
|
return hashStableJson({
|
|
92
88
|
sellerRequestId: fields.sellerRequestId,
|
|
@@ -101,7 +97,7 @@ function computeRequestBindingHash(fields) {
|
|
|
101
97
|
});
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
// src/lib/associated-token-account.ts
|
|
100
|
+
// ../../src/lib/associated-token-account.ts
|
|
105
101
|
import { createHash as createHash2 } from "node:crypto";
|
|
106
102
|
import bs582 from "bs58";
|
|
107
103
|
var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
|
|
@@ -119,12 +115,12 @@ function deriveAssociatedTokenAddress(params) {
|
|
|
119
115
|
"associatedTokenProgramId"
|
|
120
116
|
);
|
|
121
117
|
for (let bump = 255; bump >= 0; bump -= 1) {
|
|
122
|
-
const
|
|
118
|
+
const address3 = createProgramAddress(
|
|
123
119
|
[owner, tokenProgramId, mint, Uint8Array.of(bump)],
|
|
124
120
|
associatedTokenProgramId
|
|
125
121
|
);
|
|
126
|
-
if (
|
|
127
|
-
return bs582.encode(
|
|
122
|
+
if (address3 !== null) {
|
|
123
|
+
return bs582.encode(address3);
|
|
128
124
|
}
|
|
129
125
|
}
|
|
130
126
|
throw new Error("Unable to derive associated token account address");
|
|
@@ -193,7 +189,7 @@ function modPow(base, exponent, modulus) {
|
|
|
193
189
|
return result;
|
|
194
190
|
}
|
|
195
191
|
|
|
196
|
-
// src/client/transaction-intent-validator.ts
|
|
192
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
197
193
|
var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
|
|
198
194
|
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
|
|
199
195
|
var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
@@ -879,7 +875,7 @@ function readShortVec(bytes, startOffset) {
|
|
|
879
875
|
return null;
|
|
880
876
|
}
|
|
881
877
|
|
|
882
|
-
// src/client/agent-wallet-signer.ts
|
|
878
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
883
879
|
var LocalKeypairAgentWalletSigner = class {
|
|
884
880
|
validationMode = "structured_intent_transaction";
|
|
885
881
|
keyPairSigner;
|
|
@@ -949,47 +945,7 @@ var LocalKeypairAgentWalletSigner = class {
|
|
|
949
945
|
}
|
|
950
946
|
};
|
|
951
947
|
|
|
952
|
-
// src/
|
|
953
|
-
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
954
|
-
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
955
|
-
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
956
|
-
const wire = Buffer.from(serializedTransaction, "base64");
|
|
957
|
-
let offset = 0;
|
|
958
|
-
let signatureCount = 0;
|
|
959
|
-
let shift = 0;
|
|
960
|
-
while (offset < wire.length) {
|
|
961
|
-
const byte = wire[offset];
|
|
962
|
-
signatureCount |= (byte & 127) << shift;
|
|
963
|
-
offset += 1;
|
|
964
|
-
if ((byte & 128) === 0) {
|
|
965
|
-
break;
|
|
966
|
-
}
|
|
967
|
-
shift += 7;
|
|
968
|
-
}
|
|
969
|
-
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
970
|
-
const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
|
|
971
|
-
const lookups = compiled.addressTableLookups ?? [];
|
|
972
|
-
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
973
|
-
}
|
|
974
|
-
async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
975
|
-
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
976
|
-
if (addresses.length === 0) {
|
|
977
|
-
return {};
|
|
978
|
-
}
|
|
979
|
-
const tables = await fetchAllMaybeAddressLookupTable(
|
|
980
|
-
rpc2,
|
|
981
|
-
addresses.map((value) => address(value))
|
|
982
|
-
);
|
|
983
|
-
const result = {};
|
|
984
|
-
for (const table of tables) {
|
|
985
|
-
if (table.exists) {
|
|
986
|
-
result[table.address] = table.data.addresses.map(String);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
return result;
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
// src/api/wallet-auth.ts
|
|
948
|
+
// ../../src/api/wallet-auth.ts
|
|
993
949
|
import { createHash as createHash3 } from "node:crypto";
|
|
994
950
|
import bs585 from "bs58";
|
|
995
951
|
import nacl from "tweetnacl";
|
|
@@ -1007,7 +963,7 @@ function walletAuthMessage(params) {
|
|
|
1007
963
|
);
|
|
1008
964
|
}
|
|
1009
965
|
|
|
1010
|
-
// src/client/wallet-auth-headers.ts
|
|
966
|
+
// ../../src/client/wallet-auth-headers.ts
|
|
1011
967
|
async function walletAuthHeaders(params) {
|
|
1012
968
|
const signedAtMs = String(Date.now());
|
|
1013
969
|
const message = walletAuthMessage({
|
|
@@ -1023,7 +979,7 @@ async function walletAuthHeaders(params) {
|
|
|
1023
979
|
};
|
|
1024
980
|
}
|
|
1025
981
|
|
|
1026
|
-
// src/client/onboarding.ts
|
|
982
|
+
// ../../src/client/onboarding.ts
|
|
1027
983
|
var SELF_SERVE_POLICY_ID = "self-serve";
|
|
1028
984
|
var OnboardingError = class extends Error {
|
|
1029
985
|
constructor(step, message, detail = null) {
|
|
@@ -1038,9 +994,9 @@ var OnboardingError = class extends Error {
|
|
|
1038
994
|
async function ensureWalletOnboarded(params) {
|
|
1039
995
|
const fetchImpl = params.fetchImpl ?? fetch;
|
|
1040
996
|
const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
|
|
1041
|
-
const post = async (step, path,
|
|
997
|
+
const post = async (step, path, body2) => {
|
|
1042
998
|
const url2 = `${baseUrl}${path}`;
|
|
1043
|
-
const serialized = JSON.stringify(
|
|
999
|
+
const serialized = JSON.stringify(body2);
|
|
1044
1000
|
const response = await fetchImpl(url2, {
|
|
1045
1001
|
method: "POST",
|
|
1046
1002
|
headers: {
|
|
@@ -1080,12 +1036,226 @@ async function ensureWalletOnboarded(params) {
|
|
|
1080
1036
|
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
|
|
1081
1037
|
}
|
|
1082
1038
|
|
|
1083
|
-
// src/
|
|
1039
|
+
// ../../src/client/relayer-yield-realizer.ts
|
|
1040
|
+
import { address as address2 } from "@solana/kit";
|
|
1041
|
+
|
|
1042
|
+
// ../../src/client/lookup-tables.ts
|
|
1043
|
+
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
1044
|
+
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
1045
|
+
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
1046
|
+
const wire = Buffer.from(serializedTransaction, "base64");
|
|
1047
|
+
let offset = 0;
|
|
1048
|
+
let signatureCount = 0;
|
|
1049
|
+
let shift = 0;
|
|
1050
|
+
while (offset < wire.length) {
|
|
1051
|
+
const byte = wire[offset];
|
|
1052
|
+
signatureCount |= (byte & 127) << shift;
|
|
1053
|
+
offset += 1;
|
|
1054
|
+
if ((byte & 128) === 0) {
|
|
1055
|
+
break;
|
|
1056
|
+
}
|
|
1057
|
+
shift += 7;
|
|
1058
|
+
}
|
|
1059
|
+
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
1060
|
+
const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
|
|
1061
|
+
const lookups = compiled.addressTableLookups ?? [];
|
|
1062
|
+
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
1063
|
+
}
|
|
1064
|
+
async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
1065
|
+
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
1066
|
+
if (addresses.length === 0) {
|
|
1067
|
+
return {};
|
|
1068
|
+
}
|
|
1069
|
+
const tables = await fetchAllMaybeAddressLookupTable(
|
|
1070
|
+
rpc2,
|
|
1071
|
+
addresses.map((value) => address(value))
|
|
1072
|
+
);
|
|
1073
|
+
const result = {};
|
|
1074
|
+
for (const table of tables) {
|
|
1075
|
+
if (table.exists) {
|
|
1076
|
+
result[table.address] = table.data.addresses.map(String);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return result;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ../../src/client/relayer-yield-realizer.ts
|
|
1083
|
+
var RelayerRealizeError = class extends Error {
|
|
1084
|
+
constructor(code, message, detail = null) {
|
|
1085
|
+
super(message);
|
|
1086
|
+
this.code = code;
|
|
1087
|
+
this.detail = detail;
|
|
1088
|
+
this.name = "RelayerRealizeError";
|
|
1089
|
+
}
|
|
1090
|
+
code;
|
|
1091
|
+
detail;
|
|
1092
|
+
};
|
|
1093
|
+
var RelayerYieldRealizer = class {
|
|
1094
|
+
config;
|
|
1095
|
+
signer;
|
|
1096
|
+
rpc;
|
|
1097
|
+
fetchImpl;
|
|
1098
|
+
lookupTablesFor;
|
|
1099
|
+
constructor(config) {
|
|
1100
|
+
this.config = {
|
|
1101
|
+
facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
|
|
1102
|
+
usdcMint: config.usdcMint ?? SUBLY_VAULT.usdcMint,
|
|
1103
|
+
forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
|
|
1104
|
+
};
|
|
1105
|
+
this.signer = config.signer;
|
|
1106
|
+
this.rpc = config.rpc;
|
|
1107
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1108
|
+
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(this.rpc, serializedTransaction));
|
|
1109
|
+
}
|
|
1110
|
+
async ensureUsdcAvailable(input) {
|
|
1111
|
+
const agentAta = deriveAssociatedTokenAddress({
|
|
1112
|
+
owner: this.signer.walletAddress,
|
|
1113
|
+
mint: this.config.usdcMint
|
|
1114
|
+
});
|
|
1115
|
+
let shortfallRawUsdc;
|
|
1116
|
+
if (this.config.forceRealizeFullAmount) {
|
|
1117
|
+
shortfallRawUsdc = input.amountRawUsdc;
|
|
1118
|
+
} else {
|
|
1119
|
+
const currentBalance = await this.readTokenBalance(agentAta);
|
|
1120
|
+
if (currentBalance >= input.amountRawUsdc) {
|
|
1121
|
+
return { realizedRawUsdc: 0n, txSignature: null };
|
|
1122
|
+
}
|
|
1123
|
+
shortfallRawUsdc = input.amountRawUsdc - currentBalance;
|
|
1124
|
+
}
|
|
1125
|
+
await this.assertSpendableYield(shortfallRawUsdc);
|
|
1126
|
+
const prepared = await this.postJson("/v1/withdrawals/prepare", {
|
|
1127
|
+
wallet: this.signer.walletAddress,
|
|
1128
|
+
amountRawUsdc: shortfallRawUsdc.toString()
|
|
1129
|
+
});
|
|
1130
|
+
const signed = await this.signer.signWithdrawal({
|
|
1131
|
+
intent: prepared.signingIntent,
|
|
1132
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1133
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1134
|
+
});
|
|
1135
|
+
let settled = await this.postJson("/v1/withdrawals/submit", {
|
|
1136
|
+
withdrawalId: prepared.withdrawalId,
|
|
1137
|
+
serializedTransaction: signed.serializedTransaction,
|
|
1138
|
+
agentSignature: signed.agentSignature
|
|
1139
|
+
});
|
|
1140
|
+
if (settled.status === "submitted") {
|
|
1141
|
+
settled = await this.pollUntilSettled(prepared.withdrawalId);
|
|
1142
|
+
}
|
|
1143
|
+
if (settled.status !== "confirmed" || settled.txSignature === null) {
|
|
1144
|
+
throw new RelayerRealizeError(
|
|
1145
|
+
"realize_not_confirmed",
|
|
1146
|
+
`yield realize withdrawal did not confirm (status=${settled.status})`,
|
|
1147
|
+
settled
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
return {
|
|
1151
|
+
realizedRawUsdc: BigInt(settled.actualWithdrawRawUsdc ?? "0"),
|
|
1152
|
+
txSignature: settled.txSignature
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
/** Refuses to realize more than the ledger's spendable yield (principal). */
|
|
1156
|
+
async assertSpendableYield(shortfallRawUsdc) {
|
|
1157
|
+
const url2 = `${this.config.facilitatorBaseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
1158
|
+
let response;
|
|
1159
|
+
try {
|
|
1160
|
+
response = await this.fetchImpl(url2, {
|
|
1161
|
+
headers: await walletAuthHeaders({
|
|
1162
|
+
signer: this.signer,
|
|
1163
|
+
method: "GET",
|
|
1164
|
+
url: url2
|
|
1165
|
+
})
|
|
1166
|
+
});
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
throw new RelayerRealizeError(
|
|
1169
|
+
"budget_unavailable",
|
|
1170
|
+
"could not read the spendable-yield budget",
|
|
1171
|
+
error
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
if (response.status !== 200) {
|
|
1175
|
+
throw new RelayerRealizeError(
|
|
1176
|
+
"budget_unavailable",
|
|
1177
|
+
`budget endpoint returned ${response.status}`
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
const body2 = await response.json();
|
|
1181
|
+
const spendable = BigInt(body2.budget?.spendableYieldRawUsdc ?? "0");
|
|
1182
|
+
if (spendable < shortfallRawUsdc) {
|
|
1183
|
+
throw new RelayerRealizeError(
|
|
1184
|
+
"insufficient_yield",
|
|
1185
|
+
`spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC; the principal is never spent \u2014 wait for more yield`,
|
|
1186
|
+
{ spendableYieldRawUsdc: spendable.toString() }
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Polls GET /v1/withdrawals/:id (which reconciles a submitted intent against
|
|
1192
|
+
* the chain) until it leaves the "submitted" state or the timeout elapses.
|
|
1193
|
+
*/
|
|
1194
|
+
async pollUntilSettled(withdrawalId, { timeoutMs = 9e4, intervalMs = 2500 } = {}) {
|
|
1195
|
+
const deadline = Date.now() + timeoutMs;
|
|
1196
|
+
let latest = null;
|
|
1197
|
+
while (Date.now() < deadline) {
|
|
1198
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1199
|
+
const url2 = `${this.config.facilitatorBaseUrl}/v1/withdrawals/${withdrawalId}`;
|
|
1200
|
+
const response = await this.fetchImpl(url2, {
|
|
1201
|
+
headers: await walletAuthHeaders({
|
|
1202
|
+
signer: this.signer,
|
|
1203
|
+
method: "GET",
|
|
1204
|
+
url: url2
|
|
1205
|
+
})
|
|
1206
|
+
});
|
|
1207
|
+
if (response.status !== 200) {
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
latest = await response.json();
|
|
1211
|
+
if (latest.status !== "submitted") {
|
|
1212
|
+
return latest;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return latest ?? {
|
|
1216
|
+
status: "submitted",
|
|
1217
|
+
txSignature: null,
|
|
1218
|
+
actualWithdrawRawUsdc: null
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
async postJson(path, body2) {
|
|
1222
|
+
const url2 = `${this.config.facilitatorBaseUrl}${path}`;
|
|
1223
|
+
const serialized = JSON.stringify(body2);
|
|
1224
|
+
const response = await this.fetchImpl(url2, {
|
|
1225
|
+
method: "POST",
|
|
1226
|
+
headers: {
|
|
1227
|
+
...await walletAuthHeaders({
|
|
1228
|
+
signer: this.signer,
|
|
1229
|
+
method: "POST",
|
|
1230
|
+
url: url2,
|
|
1231
|
+
body: serialized
|
|
1232
|
+
}),
|
|
1233
|
+
"content-type": "application/json"
|
|
1234
|
+
},
|
|
1235
|
+
body: serialized
|
|
1236
|
+
});
|
|
1237
|
+
const text = await response.text();
|
|
1238
|
+
if (response.status !== 200) {
|
|
1239
|
+
throw new RelayerRealizeError(
|
|
1240
|
+
path.endsWith("/submit") ? "submit_failed" : "prepare_failed",
|
|
1241
|
+
`${path} failed with ${response.status}: ${text}`
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
return JSON.parse(text);
|
|
1245
|
+
}
|
|
1246
|
+
async readTokenBalance(ata) {
|
|
1247
|
+
try {
|
|
1248
|
+
const response = await this.rpc.getTokenAccountBalance(address2(ata), { commitment: "confirmed" }).send();
|
|
1249
|
+
return BigInt(response.value.amount);
|
|
1250
|
+
} catch {
|
|
1251
|
+
return 0n;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
// ../../src/x402/headers.ts
|
|
1084
1257
|
import { z } from "zod";
|
|
1085
1258
|
var PAYMENT_REQUIRED_HEADER = "payment-required";
|
|
1086
|
-
var PAYMENT_SIGNATURE_HEADER = "payment-signature";
|
|
1087
|
-
var PAYMENT_RESPONSE_HEADER = "payment-response";
|
|
1088
|
-
var X402_VERSION = 2;
|
|
1089
1259
|
var MAX_HEADER_JSON_BYTES = 16384;
|
|
1090
1260
|
var X402HeaderError = class extends Error {
|
|
1091
1261
|
reason;
|
|
@@ -1132,16 +1302,6 @@ var sublyPaymentPayloadSchema = z.object({
|
|
|
1132
1302
|
temporarySettlementSignature: z.string().min(1).max(128)
|
|
1133
1303
|
})
|
|
1134
1304
|
}).loose();
|
|
1135
|
-
function encodeX402Header(value) {
|
|
1136
|
-
const json = JSON.stringify(value);
|
|
1137
|
-
if (Buffer.byteLength(json, "utf8") > MAX_HEADER_JSON_BYTES) {
|
|
1138
|
-
throw new X402HeaderError(
|
|
1139
|
-
"header_too_large",
|
|
1140
|
-
`x402 header JSON exceeds ${MAX_HEADER_JSON_BYTES} bytes`
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
return Buffer.from(json, "utf8").toString("base64");
|
|
1144
|
-
}
|
|
1145
1305
|
function decodeX402Header(headerValue) {
|
|
1146
1306
|
if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
|
|
1147
1307
|
throw new X402HeaderError(
|
|
@@ -1159,284 +1319,218 @@ function decodeX402Header(headerValue) {
|
|
|
1159
1319
|
);
|
|
1160
1320
|
}
|
|
1161
1321
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1322
|
+
|
|
1323
|
+
// ../../src/x402/standard-requirements.ts
|
|
1324
|
+
import { z as z2 } from "zod";
|
|
1325
|
+
var STANDARD_EXACT_SCHEME = "exact";
|
|
1326
|
+
var standardExactRequirementSchema = z2.object({
|
|
1327
|
+
scheme: z2.literal(STANDARD_EXACT_SCHEME),
|
|
1328
|
+
/** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
|
|
1329
|
+
network: z2.string().min(1),
|
|
1330
|
+
/** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
|
|
1331
|
+
asset: z2.string().min(1),
|
|
1332
|
+
/** Exact price in the asset's atomic units, as a decimal string. */
|
|
1333
|
+
amount: z2.string().regex(/^[1-9]\d*$/),
|
|
1334
|
+
/** Recipient wallet; the transfer destination ATA is derived from it. */
|
|
1335
|
+
payTo: z2.string().min(1),
|
|
1336
|
+
maxTimeoutSeconds: z2.number().int().positive().optional(),
|
|
1337
|
+
extra: z2.object({
|
|
1338
|
+
/** Facilitator address that pays the tx fee (gas sponsorship). */
|
|
1339
|
+
feePayer: z2.string().min(1).optional()
|
|
1340
|
+
}).loose().optional()
|
|
1341
|
+
}).loose();
|
|
1342
|
+
var standardPaymentRequiredSchema = z2.object({
|
|
1343
|
+
x402Version: z2.number().int(),
|
|
1344
|
+
accepts: z2.array(z2.unknown()),
|
|
1345
|
+
error: z2.string().optional(),
|
|
1346
|
+
resource: z2.object({ url: z2.string().optional() }).loose().optional()
|
|
1347
|
+
}).loose();
|
|
1348
|
+
var StandardX402ChallengeError = class extends Error {
|
|
1349
|
+
reason;
|
|
1350
|
+
constructor(reason, message) {
|
|
1351
|
+
super(message);
|
|
1352
|
+
this.name = "StandardX402ChallengeError";
|
|
1353
|
+
this.reason = reason;
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
function parseStandardChallenge(challenge) {
|
|
1357
|
+
const parsed = standardPaymentRequiredSchema.safeParse(challenge);
|
|
1164
1358
|
if (!parsed.success) {
|
|
1165
|
-
throw new
|
|
1359
|
+
throw new StandardX402ChallengeError(
|
|
1166
1360
|
"invalid_payment_required",
|
|
1167
|
-
"
|
|
1361
|
+
"Response is not a valid x402 PaymentRequired object"
|
|
1168
1362
|
);
|
|
1169
1363
|
}
|
|
1170
|
-
const
|
|
1171
|
-
const requirement =
|
|
1172
|
-
|
|
1364
|
+
const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
|
|
1365
|
+
const requirement = standardExactRequirementSchema.safeParse(candidate);
|
|
1366
|
+
if (!requirement.success) {
|
|
1367
|
+
return [];
|
|
1368
|
+
}
|
|
1369
|
+
return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
|
|
1173
1370
|
});
|
|
1174
|
-
return { paymentRequired: parsed.data,
|
|
1371
|
+
return { paymentRequired: parsed.data, solanaExactRequirements };
|
|
1175
1372
|
}
|
|
1176
|
-
function
|
|
1177
|
-
|
|
1178
|
-
|
|
1373
|
+
function decodeStandardPaymentRequiredHeader(headerValue) {
|
|
1374
|
+
let decoded;
|
|
1375
|
+
try {
|
|
1376
|
+
decoded = decodeX402Header(headerValue);
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
throw new StandardX402ChallengeError(
|
|
1379
|
+
error instanceof X402HeaderError ? error.reason : "invalid_header",
|
|
1380
|
+
"Cannot decode the payment-required header"
|
|
1381
|
+
);
|
|
1179
1382
|
}
|
|
1180
|
-
return
|
|
1181
|
-
|
|
1182
|
-
|
|
1383
|
+
return parseStandardChallenge(decoded);
|
|
1384
|
+
}
|
|
1385
|
+
function selectPayableSolanaRequirement(requirements, options) {
|
|
1386
|
+
const network = options?.network ?? SOLANA_MAINNET_NETWORK;
|
|
1387
|
+
const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1388
|
+
const requirement = requirements.find(
|
|
1389
|
+
(candidate) => candidate.network === network && candidate.asset === usdcMint
|
|
1390
|
+
) ?? null;
|
|
1391
|
+
if (requirement === null) {
|
|
1392
|
+
throw new StandardX402ChallengeError(
|
|
1393
|
+
"no_payable_requirement",
|
|
1394
|
+
`The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
requirement,
|
|
1399
|
+
amountRawUsdc: BigInt(requirement.amount),
|
|
1400
|
+
payTo: requirement.payTo,
|
|
1401
|
+
feePayer: requirement.extra?.feePayer ?? null
|
|
1402
|
+
};
|
|
1183
1403
|
}
|
|
1184
1404
|
|
|
1185
|
-
// src/client/
|
|
1186
|
-
var
|
|
1405
|
+
// ../../src/client/standard-x402-payer.ts
|
|
1406
|
+
var StandardX402PayError = class extends Error {
|
|
1187
1407
|
constructor(reason, message, detail = null) {
|
|
1188
1408
|
super(message);
|
|
1189
1409
|
this.reason = reason;
|
|
1190
1410
|
this.detail = detail;
|
|
1191
|
-
this.name = "
|
|
1411
|
+
this.name = "StandardX402PayError";
|
|
1192
1412
|
}
|
|
1193
1413
|
reason;
|
|
1194
1414
|
detail;
|
|
1195
1415
|
};
|
|
1196
|
-
var
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
const value = BigInt(raw);
|
|
1201
|
-
const negative = value < 0n;
|
|
1202
|
-
const abs = negative ? -value : value;
|
|
1203
|
-
const whole = abs / 1000000n;
|
|
1204
|
-
const frac = (abs % 1000000n).toString().padStart(6, "0");
|
|
1205
|
-
return `${negative ? "-" : ""}${whole}.${frac}`;
|
|
1206
|
-
}
|
|
1207
|
-
var PaidFetchService = class {
|
|
1208
|
-
signatureBuilder;
|
|
1209
|
-
fetchImpl;
|
|
1210
|
-
fetchBudget;
|
|
1211
|
-
paymentStatusFor;
|
|
1416
|
+
var StandardX402Payer = class {
|
|
1417
|
+
realizer;
|
|
1418
|
+
x402Fetch;
|
|
1419
|
+
probeFetch;
|
|
1212
1420
|
defaultMaxAmountRawUsdc;
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
maxBodyChars;
|
|
1216
|
-
nowMs;
|
|
1217
|
-
stateStore;
|
|
1218
|
-
pending = /* @__PURE__ */ new Map();
|
|
1219
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
1421
|
+
network;
|
|
1422
|
+
usdcMint;
|
|
1220
1423
|
constructor(config) {
|
|
1221
|
-
this.
|
|
1222
|
-
this.
|
|
1223
|
-
this.
|
|
1224
|
-
this.paymentStatusFor = config.paymentStatusFor ?? (async () => "indeterminate");
|
|
1424
|
+
this.realizer = config.realizer;
|
|
1425
|
+
this.x402Fetch = config.x402Fetch;
|
|
1426
|
+
this.probeFetch = config.probeFetch ?? fetch;
|
|
1225
1427
|
this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
|
|
1226
|
-
this.
|
|
1227
|
-
this.
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
/**
|
|
1239
|
-
* GET the URL, paying a 402 challenge when needed. Concurrent calls for the
|
|
1240
|
-
* same URL share one flow and one result.
|
|
1241
|
-
*/
|
|
1242
|
-
paidFetch(params) {
|
|
1243
|
-
const existing = this.inFlight.get(params.url);
|
|
1244
|
-
if (existing !== void 0) {
|
|
1245
|
-
return existing;
|
|
1246
|
-
}
|
|
1247
|
-
const flow = this.run(params).finally(() => {
|
|
1248
|
-
this.inFlight.delete(params.url);
|
|
1249
|
-
});
|
|
1250
|
-
this.inFlight.set(params.url, flow);
|
|
1251
|
-
return flow;
|
|
1252
|
-
}
|
|
1253
|
-
async run(params) {
|
|
1254
|
-
const { url: url2 } = params;
|
|
1255
|
-
const pending = this.pending.get(url2);
|
|
1256
|
-
if (pending !== void 0) {
|
|
1257
|
-
const expired = this.nowMs() - pending.challengeAtMs > this.pendingTtlMs;
|
|
1258
|
-
if (pending.unresolved || expired) {
|
|
1259
|
-
if (params.forceNewPayment === true) {
|
|
1260
|
-
this.untrack(url2);
|
|
1261
|
-
} else {
|
|
1262
|
-
const outcome = await this.paymentStatusFor(pending.paymentId);
|
|
1263
|
-
if (outcome === "not_settled") {
|
|
1264
|
-
this.untrack(url2);
|
|
1265
|
-
} else if (outcome === "settled") {
|
|
1266
|
-
throw new PaidFetchError(
|
|
1267
|
-
"payment_already_settled",
|
|
1268
|
-
`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.`,
|
|
1269
|
-
{ paymentId: pending.paymentId }
|
|
1270
|
-
);
|
|
1271
|
-
} else {
|
|
1272
|
-
throw new PaidFetchError(
|
|
1273
|
-
"payment_outcome_unknown",
|
|
1274
|
-
`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.`,
|
|
1275
|
-
{ paymentId: pending.paymentId }
|
|
1276
|
-
);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
} else {
|
|
1280
|
-
return this.deliver(url2, pending, {
|
|
1281
|
-
retried: true,
|
|
1282
|
-
budgetBefore: null
|
|
1283
|
-
});
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
const challengeAtMs = this.nowMs();
|
|
1287
|
-
const first = await this.fetchImpl(url2);
|
|
1288
|
-
const firstText = await first.text();
|
|
1289
|
-
if (first.status !== 402) {
|
|
1290
|
-
return {
|
|
1291
|
-
paid: false,
|
|
1292
|
-
status: first.status,
|
|
1293
|
-
body: this.truncated(firstText)
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
const challengeHeader = first.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1297
|
-
if (challengeHeader === null) {
|
|
1298
|
-
throw new PaidFetchError(
|
|
1299
|
-
"invalid_challenge",
|
|
1300
|
-
"402 response is missing the PAYMENT-REQUIRED header"
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1303
|
-
const requirement = decodePaymentRequiredHeader(challengeHeader).sublyRequirements[0];
|
|
1304
|
-
if (requirement === void 0) {
|
|
1305
|
-
throw new PaidFetchError(
|
|
1306
|
-
"invalid_challenge",
|
|
1307
|
-
"402 challenge contains no subly-yield-exact requirement"
|
|
1308
|
-
);
|
|
1428
|
+
this.network = config.network ?? SOLANA_MAINNET_NETWORK;
|
|
1429
|
+
this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1430
|
+
}
|
|
1431
|
+
async pay(input) {
|
|
1432
|
+
const init = {
|
|
1433
|
+
method: input.method ?? "GET",
|
|
1434
|
+
...input.body === void 0 ? {} : { body: input.body },
|
|
1435
|
+
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
1436
|
+
};
|
|
1437
|
+
const probe = await this.probeFetch(input.url, init);
|
|
1438
|
+
if (probe.status !== 402) {
|
|
1439
|
+
return { paid: false, status: probe.status, body: await probe.text() };
|
|
1309
1440
|
}
|
|
1310
|
-
const
|
|
1311
|
-
const
|
|
1312
|
-
if (amountRawUsdc >
|
|
1313
|
-
throw new
|
|
1441
|
+
const selected = await this.selectRequirement(probe);
|
|
1442
|
+
const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
|
|
1443
|
+
if (selected.amountRawUsdc > cap) {
|
|
1444
|
+
throw new StandardX402PayError(
|
|
1314
1445
|
"amount_exceeds_client_cap",
|
|
1315
|
-
`the challenge demands ${
|
|
1316
|
-
{
|
|
1317
|
-
amountRawUsdc: requirement.amountRawUsdc,
|
|
1318
|
-
maxAmountRawUsdc: maxAmountRawUsdc.toString(),
|
|
1319
|
-
payTo: requirement.payTo
|
|
1320
|
-
}
|
|
1446
|
+
`the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
|
|
1447
|
+
{ amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
|
|
1321
1448
|
);
|
|
1322
1449
|
}
|
|
1323
|
-
|
|
1324
|
-
const { headerValue, paymentId } = await this.signatureBuilder.buildPaymentSignatureHeader({
|
|
1325
|
-
paymentRequiredHeader: challengeHeader,
|
|
1326
|
-
httpMethod: "GET",
|
|
1327
|
-
url: url2
|
|
1328
|
-
});
|
|
1329
|
-
const entry = {
|
|
1330
|
-
headerValue,
|
|
1331
|
-
paymentId,
|
|
1332
|
-
amountUsdc: formatRawUsdcAmount(amountRawUsdc),
|
|
1333
|
-
payTo: requirement.payTo,
|
|
1334
|
-
challengeAtMs,
|
|
1335
|
-
unresolved: false
|
|
1336
|
-
};
|
|
1337
|
-
this.track(url2, entry);
|
|
1338
|
-
return this.deliver(url2, entry, { retried: false, budgetBefore });
|
|
1339
|
-
}
|
|
1340
|
-
/**
|
|
1341
|
-
* Sends the signed PAYMENT-SIGNATURE retry. The tracked entry is removed
|
|
1342
|
-
* only on confirmed delivery (200). A fresh 402 proves the signature can no
|
|
1343
|
-
* longer settle, so the entry is kept as an unresolved marker; every other
|
|
1344
|
-
* failure keeps it retryable so the next call reuses the same signature.
|
|
1345
|
-
*/
|
|
1346
|
-
async deliver(url2, entry, params) {
|
|
1347
|
-
let second;
|
|
1348
|
-
let secondText;
|
|
1450
|
+
let realized;
|
|
1349
1451
|
try {
|
|
1350
|
-
|
|
1351
|
-
|
|
1452
|
+
realized = await this.realizer.ensureUsdcAvailable({
|
|
1453
|
+
amountRawUsdc: selected.amountRawUsdc
|
|
1352
1454
|
});
|
|
1353
|
-
secondText = await second.text();
|
|
1354
1455
|
} catch (error) {
|
|
1355
|
-
throw new
|
|
1356
|
-
"
|
|
1357
|
-
`
|
|
1358
|
-
|
|
1456
|
+
throw new StandardX402PayError(
|
|
1457
|
+
"realize_failed",
|
|
1458
|
+
`could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
|
|
1459
|
+
error
|
|
1359
1460
|
);
|
|
1360
1461
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
status
|
|
1367
|
-
|
|
1368
|
-
...params.retried ? { retriedPendingPayment: true } : {},
|
|
1369
|
-
payment: {
|
|
1370
|
-
amountUsdc: entry.amountUsdc,
|
|
1371
|
-
payTo: entry.payTo,
|
|
1372
|
-
paymentId: entry.paymentId,
|
|
1373
|
-
transaction,
|
|
1374
|
-
solscanUrl: transaction === null ? null : `https://solscan.io/tx/${transaction}`,
|
|
1375
|
-
budgetBefore: params.budgetBefore,
|
|
1376
|
-
budgetAfter: await this.fetchBudget()
|
|
1377
|
-
}
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
if (second.status === 402) {
|
|
1381
|
-
entry.unresolved = true;
|
|
1382
|
-
this.persist();
|
|
1383
|
-
throw new PaidFetchError(
|
|
1384
|
-
"payment_outcome_unknown",
|
|
1385
|
-
`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.`,
|
|
1386
|
-
{ paymentId: entry.paymentId }
|
|
1462
|
+
const response = await this.x402Fetch(input.url, init);
|
|
1463
|
+
const bodyText = await response.text();
|
|
1464
|
+
if (response.status !== 200) {
|
|
1465
|
+
throw new StandardX402PayError(
|
|
1466
|
+
"delivery_failed",
|
|
1467
|
+
`the x402 payment did not deliver (status ${response.status})`,
|
|
1468
|
+
{ status: response.status, body: bodyText }
|
|
1387
1469
|
);
|
|
1388
1470
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1471
|
+
return {
|
|
1472
|
+
paid: true,
|
|
1473
|
+
status: response.status,
|
|
1474
|
+
body: bodyText,
|
|
1475
|
+
payment: {
|
|
1476
|
+
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1477
|
+
payTo: selected.payTo,
|
|
1478
|
+
feePayer: selected.feePayer,
|
|
1479
|
+
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1480
|
+
realizeTxSignature: realized.txSignature
|
|
1396
1481
|
}
|
|
1397
|
-
|
|
1482
|
+
};
|
|
1398
1483
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1484
|
+
/** Reads the challenge from the header (preferred) or the JSON body. */
|
|
1485
|
+
async selectRequirement(probe) {
|
|
1486
|
+
const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1487
|
+
let requirements;
|
|
1488
|
+
try {
|
|
1489
|
+
if (header !== null) {
|
|
1490
|
+
requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
|
|
1491
|
+
} else {
|
|
1492
|
+
requirements = parseStandardChallenge(
|
|
1493
|
+
await probe.json()
|
|
1494
|
+
).solanaExactRequirements;
|
|
1404
1495
|
}
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
throw new StandardX402PayError(
|
|
1498
|
+
error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
|
|
1499
|
+
"could not parse the x402 402 challenge",
|
|
1500
|
+
error
|
|
1501
|
+
);
|
|
1405
1502
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1503
|
+
try {
|
|
1504
|
+
return selectPayableSolanaRequirement(requirements, {
|
|
1505
|
+
network: this.network,
|
|
1506
|
+
usdcMint: this.usdcMint
|
|
1507
|
+
});
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
throw new StandardX402PayError(
|
|
1510
|
+
"no_payable_requirement",
|
|
1511
|
+
error instanceof Error ? error.message : String(error),
|
|
1512
|
+
error
|
|
1513
|
+
);
|
|
1416
1514
|
}
|
|
1417
|
-
this.stateStore.save(
|
|
1418
|
-
[...this.pending.entries()].map(([url2, entry]) => ({ url: url2, ...entry }))
|
|
1419
|
-
);
|
|
1420
|
-
}
|
|
1421
|
-
truncated(text) {
|
|
1422
|
-
return text.length > this.maxBodyChars ? `${text.slice(0, this.maxBodyChars)}
|
|
1423
|
-
... (truncated)` : text;
|
|
1424
1515
|
}
|
|
1425
1516
|
};
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1517
|
+
|
|
1518
|
+
// ../../src/client/relayer-payer.ts
|
|
1519
|
+
function createRelayerX402Payer(config) {
|
|
1520
|
+
const realizer = new RelayerYieldRealizer({
|
|
1521
|
+
facilitatorBaseUrl: config.facilitatorBaseUrl,
|
|
1522
|
+
signer: config.signer,
|
|
1523
|
+
rpc: config.rpc,
|
|
1524
|
+
forceRealizeFullAmount: config.forceRealizeFullAmount ?? false
|
|
1525
|
+
});
|
|
1526
|
+
return new StandardX402Payer({
|
|
1527
|
+
realizer,
|
|
1528
|
+
x402Fetch: config.x402Fetch,
|
|
1529
|
+
defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc
|
|
1530
|
+
});
|
|
1437
1531
|
}
|
|
1438
1532
|
|
|
1439
|
-
// src/solana/keys.ts
|
|
1533
|
+
// ../../src/solana/keys.ts
|
|
1440
1534
|
import { readFileSync } from "node:fs";
|
|
1441
1535
|
import bs586 from "bs58";
|
|
1442
1536
|
import {
|
|
@@ -1460,304 +1554,110 @@ async function loadKeyPairSigner(params) {
|
|
|
1460
1554
|
}
|
|
1461
1555
|
throw new Error(`${label} keypair is not configured`);
|
|
1462
1556
|
}
|
|
1557
|
+
function loadSecretKeyBytes(params) {
|
|
1558
|
+
const { base58Secret, jsonFilePath, label } = params;
|
|
1559
|
+
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
1560
|
+
const bytes = bs586.decode(base58Secret);
|
|
1561
|
+
if (bytes.length !== 64) {
|
|
1562
|
+
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
1563
|
+
}
|
|
1564
|
+
return bytes;
|
|
1565
|
+
}
|
|
1566
|
+
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1567
|
+
const raw = JSON.parse(readFileSync(jsonFilePath, "utf8"));
|
|
1568
|
+
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1569
|
+
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1570
|
+
}
|
|
1571
|
+
return Uint8Array.from(raw);
|
|
1572
|
+
}
|
|
1573
|
+
throw new Error(`${label} keypair is not configured`);
|
|
1574
|
+
}
|
|
1463
1575
|
|
|
1464
|
-
// src/solana/rpc.ts
|
|
1576
|
+
// ../../src/solana/rpc.ts
|
|
1465
1577
|
import { createSolanaRpc } from "@solana/kit";
|
|
1466
1578
|
function createRpc(url2) {
|
|
1467
1579
|
return createSolanaRpc(url2);
|
|
1468
1580
|
}
|
|
1469
1581
|
|
|
1470
|
-
// src/x402
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
signer;
|
|
1484
|
-
lookupTablesFor;
|
|
1485
|
-
fetchImpl;
|
|
1486
|
-
constructor(config) {
|
|
1487
|
-
this.config = {
|
|
1488
|
-
facilitatorBaseUrl: config.facilitatorBaseUrl.replace(/\/$/, ""),
|
|
1489
|
-
network: config.network ?? SOLANA_MAINNET_NETWORK
|
|
1490
|
-
};
|
|
1491
|
-
this.signer = config.signer;
|
|
1492
|
-
this.lookupTablesFor = config.lookupTablesFor ?? null;
|
|
1493
|
-
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1494
|
-
}
|
|
1495
|
-
/**
|
|
1496
|
-
* Builds the PAYMENT-SIGNATURE header for a 402 challenge. The request
|
|
1497
|
-
* method, URL, and body must be exactly the request being retried; they are
|
|
1498
|
-
* bound into the payment and verified again by the seller and facilitator.
|
|
1499
|
-
*/
|
|
1500
|
-
async buildPaymentSignatureHeader(input) {
|
|
1501
|
-
const requirement = this.selectRequirement(input.paymentRequiredHeader);
|
|
1502
|
-
if (requirement.resource !== input.url) {
|
|
1503
|
-
throw new X402ClientError(
|
|
1504
|
-
"resource_mismatch",
|
|
1505
|
-
"PAYMENT-REQUIRED resource does not match the request URL"
|
|
1506
|
-
);
|
|
1507
|
-
}
|
|
1508
|
-
const requestBodyHash = requestBodyHashFor(input.body);
|
|
1509
|
-
const prepared = await this.preparePayment({
|
|
1510
|
-
requirement,
|
|
1511
|
-
httpMethod: input.httpMethod.toUpperCase(),
|
|
1512
|
-
requestBodyHash
|
|
1513
|
-
});
|
|
1514
|
-
const lookupTables = this.lookupTablesFor === null ? void 0 : await this.lookupTablesFor(prepared.intentJson.serializedTransaction);
|
|
1515
|
-
const signed = await this.signer.signPayment({
|
|
1516
|
-
intent: prepared.intentJson.signingIntent,
|
|
1517
|
-
serializedTransaction: prepared.intentJson.serializedTransaction,
|
|
1518
|
-
lookupTables
|
|
1519
|
-
});
|
|
1520
|
-
const payload = {
|
|
1521
|
-
x402Version: X402_VERSION,
|
|
1522
|
-
scheme: requirement.scheme,
|
|
1523
|
-
network: requirement.network,
|
|
1524
|
-
payload: {
|
|
1525
|
-
paymentId: prepared.paymentId,
|
|
1526
|
-
requestBindingHash: prepared.requestBindingHash,
|
|
1527
|
-
preparedMessageHash: prepared.preparedMessageHash,
|
|
1528
|
-
serializedTransaction: signed.serializedTransaction,
|
|
1529
|
-
agentSignature: signed.agentSignature,
|
|
1530
|
-
temporarySettlementSignature: prepared.temporarySettlementSignature
|
|
1531
|
-
}
|
|
1532
|
-
};
|
|
1533
|
-
return {
|
|
1534
|
-
headerValue: encodeX402Header(payload),
|
|
1535
|
-
paymentId: prepared.paymentId
|
|
1536
|
-
};
|
|
1537
|
-
}
|
|
1538
|
-
/**
|
|
1539
|
-
* Convenience wrapper: performs the request, and on a 402 with a Subly
|
|
1540
|
-
* requirement pays from vault yield and retries once.
|
|
1541
|
-
*/
|
|
1542
|
-
async fetchWithPayment(url2, init) {
|
|
1543
|
-
const first = await this.fetchImpl(url2, init);
|
|
1544
|
-
if (first.status !== 402) {
|
|
1545
|
-
return first;
|
|
1546
|
-
}
|
|
1547
|
-
const challenge = first.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1548
|
-
if (challenge === null) {
|
|
1549
|
-
return first;
|
|
1550
|
-
}
|
|
1551
|
-
const { headerValue } = await this.buildPaymentSignatureHeader({
|
|
1552
|
-
paymentRequiredHeader: challenge,
|
|
1553
|
-
httpMethod: init?.method ?? "GET",
|
|
1554
|
-
url: url2,
|
|
1555
|
-
body: init?.body ?? null
|
|
1556
|
-
});
|
|
1557
|
-
return this.fetchImpl(url2, {
|
|
1558
|
-
...init,
|
|
1559
|
-
headers: {
|
|
1560
|
-
...init?.headers,
|
|
1561
|
-
[PAYMENT_SIGNATURE_HEADER]: headerValue
|
|
1582
|
+
// src/svm-x402-fetch.ts
|
|
1583
|
+
import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
|
|
1584
|
+
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
|
|
1585
|
+
import { ExactSvmScheme, toClientSvmSigner } from "@x402/svm";
|
|
1586
|
+
async function createSvmX402Fetch(params) {
|
|
1587
|
+
const signer2 = toClientSvmSigner(
|
|
1588
|
+
await createKeyPairSignerFromBytes2(params.agentSecretKey)
|
|
1589
|
+
);
|
|
1590
|
+
const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
|
|
1591
|
+
schemes: [
|
|
1592
|
+
{
|
|
1593
|
+
network: "solana:*",
|
|
1594
|
+
client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
|
|
1562
1595
|
}
|
|
1563
|
-
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
try {
|
|
1568
|
-
sublyRequirements = decodePaymentRequiredHeader(
|
|
1569
|
-
paymentRequiredHeader
|
|
1570
|
-
).sublyRequirements;
|
|
1571
|
-
} catch (error) {
|
|
1572
|
-
throw new X402ClientError(
|
|
1573
|
-
error instanceof X402HeaderError ? error.reason : "invalid_challenge",
|
|
1574
|
-
"Cannot decode the PAYMENT-REQUIRED header"
|
|
1575
|
-
);
|
|
1576
|
-
}
|
|
1577
|
-
const selected = sublyRequirements.find(
|
|
1578
|
-
(candidate) => candidate.network === this.config.network && candidate.extra.vault === SUBLY_VAULT.address && candidate.extra.shareMint === SUBLY_VAULT.shareMint && candidate.asset === SUBLY_VAULT.usdcMint
|
|
1579
|
-
) ?? null;
|
|
1580
|
-
if (selected === null) {
|
|
1581
|
-
throw new X402ClientError(
|
|
1582
|
-
"no_supported_requirement",
|
|
1583
|
-
"The 402 challenge contains no supported subly-yield-exact requirement"
|
|
1584
|
-
);
|
|
1585
|
-
}
|
|
1586
|
-
return selected;
|
|
1587
|
-
}
|
|
1588
|
-
async preparePayment(input) {
|
|
1589
|
-
const { requirement } = input;
|
|
1590
|
-
const wallet = this.signer.walletAddress;
|
|
1591
|
-
const prepareUrl = `${this.config.facilitatorBaseUrl}/v1/payments/prepare`;
|
|
1592
|
-
const prepareBody = JSON.stringify({
|
|
1593
|
-
wallet,
|
|
1594
|
-
scheme: requirement.scheme,
|
|
1595
|
-
network: requirement.network,
|
|
1596
|
-
vault: requirement.extra.vault,
|
|
1597
|
-
shareMint: requirement.extra.shareMint,
|
|
1598
|
-
asset: requirement.asset,
|
|
1599
|
-
seller: requirement.extra.seller,
|
|
1600
|
-
sellerRequestId: requirement.extra.sellerRequestId,
|
|
1601
|
-
httpMethod: input.httpMethod,
|
|
1602
|
-
canonicalResourceUrl: requirement.resource,
|
|
1603
|
-
requestBodyHash: input.requestBodyHash,
|
|
1604
|
-
amountRawUsdc: requirement.amountRawUsdc,
|
|
1605
|
-
payTo: requirement.payTo,
|
|
1606
|
-
sellerUsdcAta: requirement.extra.sellerUsdcAta,
|
|
1607
|
-
dustRecipientUsdcAta: deriveAssociatedTokenAddress({
|
|
1608
|
-
owner: wallet,
|
|
1609
|
-
mint: SUBLY_VAULT.usdcMint
|
|
1610
|
-
})
|
|
1611
|
-
});
|
|
1612
|
-
const response = await this.fetchImpl(prepareUrl, {
|
|
1613
|
-
method: "POST",
|
|
1614
|
-
headers: {
|
|
1615
|
-
...await walletAuthHeaders({
|
|
1616
|
-
signer: this.signer,
|
|
1617
|
-
method: "POST",
|
|
1618
|
-
url: prepareUrl,
|
|
1619
|
-
body: prepareBody
|
|
1620
|
-
}),
|
|
1621
|
-
"content-type": "application/json"
|
|
1622
|
-
},
|
|
1623
|
-
body: prepareBody
|
|
1624
|
-
});
|
|
1625
|
-
const body = await response.json();
|
|
1626
|
-
if (response.status !== 200) {
|
|
1627
|
-
const error = body.error;
|
|
1628
|
-
throw new X402ClientError(
|
|
1629
|
-
typeof error?.code === "string" ? error.code : "prepare_failed",
|
|
1630
|
-
"Payment preparation failed at the facilitator",
|
|
1631
|
-
body
|
|
1632
|
-
);
|
|
1633
|
-
}
|
|
1634
|
-
const prepared = body;
|
|
1635
|
-
if (typeof prepared.paymentId !== "string" || typeof prepared.preparedMessageHash !== "string" || typeof prepared.intentJson?.serializedTransaction !== "string" || typeof prepared.intentJson?.signingIntent !== "object") {
|
|
1636
|
-
throw new X402ClientError(
|
|
1637
|
-
"invalid_prepare_response",
|
|
1638
|
-
"Facilitator prepare response is missing required fields"
|
|
1639
|
-
);
|
|
1640
|
-
}
|
|
1641
|
-
return prepared;
|
|
1642
|
-
}
|
|
1643
|
-
};
|
|
1596
|
+
]
|
|
1597
|
+
});
|
|
1598
|
+
return (url2, init) => wrapped(url2, init);
|
|
1599
|
+
}
|
|
1644
1600
|
|
|
1645
|
-
//
|
|
1601
|
+
// src/pay.ts
|
|
1646
1602
|
function fail(message) {
|
|
1647
|
-
|
|
1603
|
+
process.stderr.write(`${message}
|
|
1604
|
+
`);
|
|
1648
1605
|
process.exit(1);
|
|
1649
1606
|
}
|
|
1650
|
-
|
|
1651
|
-
// demo/pay.ts
|
|
1652
1607
|
var url = process.argv[2];
|
|
1653
1608
|
if (url === void 0 || !/^https?:\/\//.test(url)) {
|
|
1654
|
-
fail("Usage:
|
|
1609
|
+
fail("Usage: pay fetch <url> [maxAmountRawUsdc]");
|
|
1655
1610
|
}
|
|
1656
1611
|
var maxAmountArg = process.argv[3];
|
|
1657
1612
|
var facilitatorBaseUrl = process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
|
|
1658
1613
|
var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
|
|
1659
1614
|
var defaultMaxAmountRawUsdc = process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC === void 0 ? 10000n : BigInt(process.env.SUBLY_MCP_MAX_AMOUNT_RAW_USDC);
|
|
1660
|
-
var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? "demo/env/mcp-pending-payments.json";
|
|
1661
1615
|
var keyPairSigner = await loadKeyPairSigner({
|
|
1662
1616
|
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1663
1617
|
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1664
1618
|
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1665
1619
|
});
|
|
1620
|
+
var agentSecretKey = loadSecretKeyBytes({
|
|
1621
|
+
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1622
|
+
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1623
|
+
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1624
|
+
});
|
|
1666
1625
|
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
1667
1626
|
var rpc = createRpc(rpcUrl);
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
if (response.status !== 200) {
|
|
1675
|
-
return null;
|
|
1676
|
-
}
|
|
1677
|
-
const body = await response.json();
|
|
1678
|
-
return body.budget === void 0 ? null : {
|
|
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 statusUrl = `${facilitatorBaseUrl}/v1/payments/${paymentId}`;
|
|
1691
|
-
const response = await fetch(statusUrl, {
|
|
1692
|
-
headers: await walletAuthHeaders({ signer, method: "GET", url: statusUrl })
|
|
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-pay] failed to persist pending payments: ${error instanceof Error ? error.message : String(error)}`
|
|
1725
|
-
);
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
var service = new PaidFetchService({
|
|
1731
|
-
signatureBuilder: new SublyX402Client({
|
|
1732
|
-
facilitatorBaseUrl,
|
|
1733
|
-
signer,
|
|
1734
|
-
lookupTablesFor: (serializedTransaction) => fetchLookupTablesForTransaction(rpc, serializedTransaction)
|
|
1735
|
-
}),
|
|
1736
|
-
defaultMaxAmountRawUsdc,
|
|
1737
|
-
fetchBudget,
|
|
1738
|
-
paymentStatusFor,
|
|
1739
|
-
stateStore: fileStateStore(pendingStatePath)
|
|
1627
|
+
var payer = createRelayerX402Payer({
|
|
1628
|
+
facilitatorBaseUrl,
|
|
1629
|
+
signer,
|
|
1630
|
+
rpc,
|
|
1631
|
+
x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
|
|
1632
|
+
defaultMaxAmountRawUsdc
|
|
1740
1633
|
});
|
|
1741
|
-
|
|
1634
|
+
var method = process.env.SUBLY_PAY_METHOD;
|
|
1635
|
+
var body = process.env.SUBLY_PAY_BODY;
|
|
1636
|
+
console.error(`[pay] agent ${signer.walletAddress} -> ${url}`);
|
|
1742
1637
|
try {
|
|
1743
1638
|
await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
|
|
1744
1639
|
} catch (error) {
|
|
1745
1640
|
console.error(
|
|
1746
|
-
`[
|
|
1641
|
+
`[pay] onboarding skipped: ${error instanceof Error ? error.message : String(error)}`
|
|
1747
1642
|
);
|
|
1748
1643
|
}
|
|
1749
1644
|
try {
|
|
1750
|
-
const result = await
|
|
1645
|
+
const result = await payer.pay({
|
|
1751
1646
|
url,
|
|
1647
|
+
...method === void 0 ? {} : { method },
|
|
1648
|
+
...body === void 0 ? {} : { body, headers: { "content-type": "application/json" } },
|
|
1752
1649
|
...maxAmountArg === void 0 ? {} : { maxAmountRawUsdc: BigInt(maxAmountArg) }
|
|
1753
1650
|
});
|
|
1754
1651
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1755
1652
|
`);
|
|
1653
|
+
if (!result.paid) {
|
|
1654
|
+
process.exit(1);
|
|
1655
|
+
}
|
|
1756
1656
|
} catch (error) {
|
|
1757
|
-
if (error instanceof
|
|
1657
|
+
if (error instanceof StandardX402PayError) {
|
|
1758
1658
|
process.stdout.write(
|
|
1759
1659
|
`${JSON.stringify(
|
|
1760
|
-
{ paid: false,
|
|
1660
|
+
{ paid: false, reason: error.reason, message: error.message },
|
|
1761
1661
|
null,
|
|
1762
1662
|
2
|
|
1763
1663
|
)}
|
|
@@ -1765,5 +1665,5 @@ try {
|
|
|
1765
1665
|
);
|
|
1766
1666
|
process.exit(1);
|
|
1767
1667
|
}
|
|
1768
|
-
fail(`[
|
|
1668
|
+
fail(`[pay] ${error instanceof Error ? error.message : String(error)}`);
|
|
1769
1669
|
}
|