@subly_fi/pay 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/dist/deposit.js +262 -71
- package/dist/mcp-server.js +1076 -411
- package/dist/pay.js +861 -543
- package/dist/withdraw.js +295 -102
- package/package.json +9 -7
package/dist/pay.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
import {
|
|
1
|
+
// src/pay.ts
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join as join2 } from "node:path";
|
|
3
4
|
|
|
4
|
-
// src/client/agent-wallet-signer.ts
|
|
5
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
5
6
|
import { signBytes } from "@solana/kit";
|
|
6
7
|
import bs584 from "bs58";
|
|
7
8
|
|
|
8
|
-
// src/solana/tx.ts
|
|
9
|
+
// ../../src/solana/tx.ts
|
|
9
10
|
import bs58 from "bs58";
|
|
10
11
|
import {
|
|
11
12
|
appendTransactionMessageInstructions,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
setTransactionMessageLifetimeUsingBlockhash
|
|
21
22
|
} from "@solana/kit";
|
|
22
23
|
|
|
23
|
-
// src/lib/hash.ts
|
|
24
|
+
// ../../src/lib/hash.ts
|
|
24
25
|
import { createHash } from "node:crypto";
|
|
25
26
|
var EMPTY_BODY_HASH = "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
26
27
|
function sha256TaggedHex(data) {
|
|
@@ -46,7 +47,7 @@ function hashStableJson(value) {
|
|
|
46
47
|
return sha256TaggedHex(stableStringify(value));
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
// src/solana/tx.ts
|
|
50
|
+
// ../../src/solana/tx.ts
|
|
50
51
|
function decodeSerializedTransaction(serializedBase64) {
|
|
51
52
|
return getTransactionDecoder().decode(Buffer.from(serializedBase64, "base64"));
|
|
52
53
|
}
|
|
@@ -66,11 +67,11 @@ function signatureBase58ForSigner(transaction, signer2) {
|
|
|
66
67
|
return bs58.encode(signature);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
// src/client/transaction-intent-validator.ts
|
|
70
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
70
71
|
import bs583 from "bs58";
|
|
71
72
|
import { getCompiledTransactionMessageDecoder } from "@solana/kit";
|
|
72
73
|
|
|
73
|
-
// src/config/constants.ts
|
|
74
|
+
// ../../src/config/constants.ts
|
|
74
75
|
var PAYMENT_SCHEME = "subly-yield-exact";
|
|
75
76
|
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
76
77
|
var SPL_TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
@@ -86,7 +87,7 @@ var SUBLY_VAULT = {
|
|
|
86
87
|
};
|
|
87
88
|
var USDC_DECIMALS = 6;
|
|
88
89
|
|
|
89
|
-
// src/domain/request-binding.ts
|
|
90
|
+
// ../../src/domain/request-binding.ts
|
|
90
91
|
function computeRequestBindingHash(fields) {
|
|
91
92
|
return hashStableJson({
|
|
92
93
|
sellerRequestId: fields.sellerRequestId,
|
|
@@ -101,7 +102,7 @@ function computeRequestBindingHash(fields) {
|
|
|
101
102
|
});
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
// src/lib/associated-token-account.ts
|
|
105
|
+
// ../../src/lib/associated-token-account.ts
|
|
105
106
|
import { createHash as createHash2 } from "node:crypto";
|
|
106
107
|
import bs582 from "bs58";
|
|
107
108
|
var PDA_MARKER = Buffer.from("ProgramDerivedAddress", "utf8");
|
|
@@ -193,7 +194,7 @@ function modPow(base, exponent, modulus) {
|
|
|
193
194
|
return result;
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
// src/client/transaction-intent-validator.ts
|
|
197
|
+
// ../../src/client/transaction-intent-validator.ts
|
|
197
198
|
var COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111";
|
|
198
199
|
var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
|
|
199
200
|
var ASSOCIATED_TOKEN_PROGRAM_ID2 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
@@ -879,7 +880,7 @@ function readShortVec(bytes, startOffset) {
|
|
|
879
880
|
return null;
|
|
880
881
|
}
|
|
881
882
|
|
|
882
|
-
// src/client/agent-wallet-signer.ts
|
|
883
|
+
// ../../src/client/agent-wallet-signer.ts
|
|
883
884
|
var LocalKeypairAgentWalletSigner = class {
|
|
884
885
|
validationMode = "structured_intent_transaction";
|
|
885
886
|
keyPairSigner;
|
|
@@ -949,47 +950,7 @@ var LocalKeypairAgentWalletSigner = class {
|
|
|
949
950
|
}
|
|
950
951
|
};
|
|
951
952
|
|
|
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
|
|
953
|
+
// ../../src/api/wallet-auth.ts
|
|
993
954
|
import { createHash as createHash3 } from "node:crypto";
|
|
994
955
|
import bs585 from "bs58";
|
|
995
956
|
import nacl from "tweetnacl";
|
|
@@ -1007,7 +968,7 @@ function walletAuthMessage(params) {
|
|
|
1007
968
|
);
|
|
1008
969
|
}
|
|
1009
970
|
|
|
1010
|
-
// src/client/wallet-auth-headers.ts
|
|
971
|
+
// ../../src/client/wallet-auth-headers.ts
|
|
1011
972
|
async function walletAuthHeaders(params) {
|
|
1012
973
|
const signedAtMs = String(Date.now());
|
|
1013
974
|
const message = walletAuthMessage({
|
|
@@ -1023,7 +984,7 @@ async function walletAuthHeaders(params) {
|
|
|
1023
984
|
};
|
|
1024
985
|
}
|
|
1025
986
|
|
|
1026
|
-
// src/client/onboarding.ts
|
|
987
|
+
// ../../src/client/onboarding.ts
|
|
1027
988
|
var SELF_SERVE_POLICY_ID = "self-serve";
|
|
1028
989
|
var OnboardingError = class extends Error {
|
|
1029
990
|
constructor(step, message, detail = null) {
|
|
@@ -1038,9 +999,9 @@ var OnboardingError = class extends Error {
|
|
|
1038
999
|
async function ensureWalletOnboarded(params) {
|
|
1039
1000
|
const fetchImpl = params.fetchImpl ?? fetch;
|
|
1040
1001
|
const baseUrl = params.facilitatorBaseUrl.replace(/\/$/, "");
|
|
1041
|
-
const post = async (step, path,
|
|
1002
|
+
const post = async (step, path, body2) => {
|
|
1042
1003
|
const url2 = `${baseUrl}${path}`;
|
|
1043
|
-
const serialized = JSON.stringify(
|
|
1004
|
+
const serialized = JSON.stringify(body2);
|
|
1044
1005
|
const response = await fetchImpl(url2, {
|
|
1045
1006
|
method: "POST",
|
|
1046
1007
|
headers: {
|
|
@@ -1080,12 +1041,375 @@ async function ensureWalletOnboarded(params) {
|
|
|
1080
1041
|
await post("sync", `/v1/wallets/${wallet}/sync`, { source: "chain" });
|
|
1081
1042
|
}
|
|
1082
1043
|
|
|
1083
|
-
// src/
|
|
1044
|
+
// ../../src/client/lookup-tables.ts
|
|
1045
|
+
import { fetchAllMaybeAddressLookupTable } from "@solana-program/address-lookup-table";
|
|
1046
|
+
import { address, getCompiledTransactionMessageDecoder as getCompiledTransactionMessageDecoder2 } from "@solana/kit";
|
|
1047
|
+
function lookupTableAddressesForTransaction(serializedTransaction) {
|
|
1048
|
+
const wire = Buffer.from(serializedTransaction, "base64");
|
|
1049
|
+
let offset = 0;
|
|
1050
|
+
let signatureCount = 0;
|
|
1051
|
+
let shift = 0;
|
|
1052
|
+
while (offset < wire.length) {
|
|
1053
|
+
const byte = wire[offset];
|
|
1054
|
+
signatureCount |= (byte & 127) << shift;
|
|
1055
|
+
offset += 1;
|
|
1056
|
+
if ((byte & 128) === 0) {
|
|
1057
|
+
break;
|
|
1058
|
+
}
|
|
1059
|
+
shift += 7;
|
|
1060
|
+
}
|
|
1061
|
+
const messageBytes = wire.subarray(offset + signatureCount * 64);
|
|
1062
|
+
const compiled = getCompiledTransactionMessageDecoder2().decode(messageBytes);
|
|
1063
|
+
const lookups = compiled.addressTableLookups ?? [];
|
|
1064
|
+
return lookups.map((lookup) => String(lookup.lookupTableAddress));
|
|
1065
|
+
}
|
|
1066
|
+
async function fetchLookupTablesForTransaction(rpc2, serializedTransaction) {
|
|
1067
|
+
const addresses = lookupTableAddressesForTransaction(serializedTransaction);
|
|
1068
|
+
if (addresses.length === 0) {
|
|
1069
|
+
return {};
|
|
1070
|
+
}
|
|
1071
|
+
const tables = await fetchAllMaybeAddressLookupTable(
|
|
1072
|
+
rpc2,
|
|
1073
|
+
addresses.map((value) => address(value))
|
|
1074
|
+
);
|
|
1075
|
+
const result = {};
|
|
1076
|
+
for (const table of tables) {
|
|
1077
|
+
if (table.exists) {
|
|
1078
|
+
result[table.address] = table.data.addresses.map(String);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ../../src/client/vault-flows.ts
|
|
1085
|
+
var VaultFlowClientError = class extends Error {
|
|
1086
|
+
constructor(step, message, detail = null) {
|
|
1087
|
+
super(message);
|
|
1088
|
+
this.step = step;
|
|
1089
|
+
this.detail = detail;
|
|
1090
|
+
this.name = "VaultFlowClientError";
|
|
1091
|
+
}
|
|
1092
|
+
step;
|
|
1093
|
+
detail;
|
|
1094
|
+
};
|
|
1095
|
+
var VaultFlowClient = class {
|
|
1096
|
+
baseUrl;
|
|
1097
|
+
signer;
|
|
1098
|
+
fetchImpl;
|
|
1099
|
+
lookupTablesFor;
|
|
1100
|
+
pollTimeoutMs;
|
|
1101
|
+
pollIntervalMs;
|
|
1102
|
+
constructor(config) {
|
|
1103
|
+
this.baseUrl = config.facilitatorBaseUrl.replace(/\/$/, "");
|
|
1104
|
+
this.signer = config.signer;
|
|
1105
|
+
this.fetchImpl = config.fetchImpl ?? fetch;
|
|
1106
|
+
this.lookupTablesFor = config.lookupTablesFor ?? ((serializedTransaction) => fetchLookupTablesForTransaction(config.rpc, serializedTransaction));
|
|
1107
|
+
this.pollTimeoutMs = config.pollTimeoutMs ?? 9e4;
|
|
1108
|
+
this.pollIntervalMs = config.pollIntervalMs ?? 2500;
|
|
1109
|
+
}
|
|
1110
|
+
/** Moves USDC from the agent wallet into the vault (fee sponsored). */
|
|
1111
|
+
async deposit(input) {
|
|
1112
|
+
const prepared = await this.postJson("prepare", "/v1/deposits/prepare", {
|
|
1113
|
+
wallet: this.signer.walletAddress,
|
|
1114
|
+
amountRawUsdc: input.amountRawUsdc.toString()
|
|
1115
|
+
});
|
|
1116
|
+
const signed = await this.signer.signDeposit({
|
|
1117
|
+
intent: prepared.signingIntent,
|
|
1118
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1119
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1120
|
+
});
|
|
1121
|
+
let outcome = await this.postJson("submit", "/v1/deposits/submit", {
|
|
1122
|
+
depositId: prepared.depositId,
|
|
1123
|
+
serializedTransaction: signed.serializedTransaction,
|
|
1124
|
+
agentSignature: signed.agentSignature
|
|
1125
|
+
});
|
|
1126
|
+
if (outcome.status === "submitted") {
|
|
1127
|
+
outcome = await this.pollUntilTerminal(
|
|
1128
|
+
`/v1/deposits/${prepared.depositId}`,
|
|
1129
|
+
outcome
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
depositId: prepared.depositId,
|
|
1134
|
+
status: outcome.status,
|
|
1135
|
+
txSignature: outcome.txSignature ?? null,
|
|
1136
|
+
actualDepositRawUsdc: outcome.actualDepositRawUsdc ?? null,
|
|
1137
|
+
sharesMintedRaw: outcome.sharesMintedRaw ?? null,
|
|
1138
|
+
errorCode: outcome.errorCode ?? null
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Moves USDC from the vault back to the agent wallet's USDC ATA (fee
|
|
1143
|
+
* sponsored). A plain withdrawal is the exit path and MAY spend principal;
|
|
1144
|
+
* with purpose "yield_realize" the relayer refuses anything beyond the
|
|
1145
|
+
* spendable yield (the payment path, via RelayerYieldRealizer).
|
|
1146
|
+
*/
|
|
1147
|
+
async withdraw(input) {
|
|
1148
|
+
const prepared = await this.postJson(
|
|
1149
|
+
"prepare",
|
|
1150
|
+
"/v1/withdrawals/prepare",
|
|
1151
|
+
{
|
|
1152
|
+
wallet: this.signer.walletAddress,
|
|
1153
|
+
amountRawUsdc: input.amountRawUsdc.toString(),
|
|
1154
|
+
...input.purpose === void 0 ? {} : { purpose: input.purpose }
|
|
1155
|
+
}
|
|
1156
|
+
);
|
|
1157
|
+
const signed = await this.signer.signWithdrawal({
|
|
1158
|
+
intent: prepared.signingIntent,
|
|
1159
|
+
serializedTransaction: prepared.serializedTransaction,
|
|
1160
|
+
lookupTables: await this.lookupTablesFor(prepared.serializedTransaction)
|
|
1161
|
+
});
|
|
1162
|
+
let outcome = await this.postJson("submit", "/v1/withdrawals/submit", {
|
|
1163
|
+
withdrawalId: prepared.withdrawalId,
|
|
1164
|
+
serializedTransaction: signed.serializedTransaction,
|
|
1165
|
+
agentSignature: signed.agentSignature
|
|
1166
|
+
});
|
|
1167
|
+
if (outcome.status === "submitted") {
|
|
1168
|
+
outcome = await this.pollUntilTerminal(
|
|
1169
|
+
`/v1/withdrawals/${prepared.withdrawalId}`,
|
|
1170
|
+
outcome
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
withdrawalId: prepared.withdrawalId,
|
|
1175
|
+
status: outcome.status,
|
|
1176
|
+
txSignature: outcome.txSignature ?? null,
|
|
1177
|
+
destinationUsdcAta: prepared.destinationUsdcAta,
|
|
1178
|
+
actualWithdrawRawUsdc: outcome.actualWithdrawRawUsdc ?? null,
|
|
1179
|
+
actualSharesBurnedRaw: outcome.actualSharesBurnedRaw ?? null,
|
|
1180
|
+
errorCode: outcome.errorCode ?? null
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Reads the yield budget. Syncs the relayer's ledger from chain first (so
|
|
1185
|
+
* yield accrued since the last sync shows up); the sync is best-effort and
|
|
1186
|
+
* on failure the last-synced view is returned.
|
|
1187
|
+
*/
|
|
1188
|
+
async getBudget(options = {}) {
|
|
1189
|
+
if (options.refreshFromChain !== false) {
|
|
1190
|
+
try {
|
|
1191
|
+
await this.postJson(
|
|
1192
|
+
"sync",
|
|
1193
|
+
`/v1/wallets/${this.signer.walletAddress}/sync`,
|
|
1194
|
+
{ source: "chain" }
|
|
1195
|
+
);
|
|
1196
|
+
} catch {
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const url2 = `${this.baseUrl}/v1/wallets/${this.signer.walletAddress}/budget`;
|
|
1200
|
+
const response = await this.fetchImpl(url2, {
|
|
1201
|
+
headers: await walletAuthHeaders({
|
|
1202
|
+
signer: this.signer,
|
|
1203
|
+
method: "GET",
|
|
1204
|
+
url: url2
|
|
1205
|
+
})
|
|
1206
|
+
});
|
|
1207
|
+
const text = await response.text();
|
|
1208
|
+
if (response.status !== 200) {
|
|
1209
|
+
throw new VaultFlowClientError(
|
|
1210
|
+
"budget",
|
|
1211
|
+
`budget endpoint returned ${response.status}: ${text}`
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
let parsed;
|
|
1215
|
+
try {
|
|
1216
|
+
parsed = JSON.parse(text);
|
|
1217
|
+
} catch {
|
|
1218
|
+
throw new VaultFlowClientError(
|
|
1219
|
+
"budget",
|
|
1220
|
+
"budget endpoint returned 200 with a non-JSON body",
|
|
1221
|
+
text
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
const body2 = parsed;
|
|
1225
|
+
return {
|
|
1226
|
+
wallet: this.signer.walletAddress,
|
|
1227
|
+
principalBasisRawUsdc: body2.position?.principalBasisRawUsdc ?? "0",
|
|
1228
|
+
positionValueRawUsdc: body2.budget?.positionValueRawUsdc ?? "0",
|
|
1229
|
+
grossYieldRawUsdc: body2.budget?.grossYieldRawUsdc ?? "0",
|
|
1230
|
+
spendableYieldRawUsdc: body2.budget?.spendableYieldRawUsdc ?? "0"
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Polls the reconciling GET endpoint until the intent leaves "submitted"
|
|
1235
|
+
* (each read looks the tx up on-chain) or the timeout elapses.
|
|
1236
|
+
*/
|
|
1237
|
+
async pollUntilTerminal(path, last) {
|
|
1238
|
+
const deadline = Date.now() + this.pollTimeoutMs;
|
|
1239
|
+
let latest = last;
|
|
1240
|
+
while (Date.now() < deadline) {
|
|
1241
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
1242
|
+
const url2 = `${this.baseUrl}${path}`;
|
|
1243
|
+
const response = await this.fetchImpl(url2, {
|
|
1244
|
+
headers: await walletAuthHeaders({
|
|
1245
|
+
signer: this.signer,
|
|
1246
|
+
method: "GET",
|
|
1247
|
+
url: url2
|
|
1248
|
+
})
|
|
1249
|
+
});
|
|
1250
|
+
if (response.status !== 200) {
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
try {
|
|
1254
|
+
latest = await response.json();
|
|
1255
|
+
} catch {
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (latest.status !== "submitted") {
|
|
1259
|
+
return latest;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return latest;
|
|
1263
|
+
}
|
|
1264
|
+
async postJson(step, path, body2) {
|
|
1265
|
+
const url2 = `${this.baseUrl}${path}`;
|
|
1266
|
+
const serialized = JSON.stringify(body2);
|
|
1267
|
+
const response = await this.fetchImpl(url2, {
|
|
1268
|
+
method: "POST",
|
|
1269
|
+
headers: {
|
|
1270
|
+
...await walletAuthHeaders({
|
|
1271
|
+
signer: this.signer,
|
|
1272
|
+
method: "POST",
|
|
1273
|
+
url: url2,
|
|
1274
|
+
body: serialized
|
|
1275
|
+
}),
|
|
1276
|
+
"content-type": "application/json"
|
|
1277
|
+
},
|
|
1278
|
+
body: serialized
|
|
1279
|
+
});
|
|
1280
|
+
const text = await response.text();
|
|
1281
|
+
if (response.status !== 200) {
|
|
1282
|
+
throw new VaultFlowClientError(
|
|
1283
|
+
step,
|
|
1284
|
+
`${path} failed with ${response.status}: ${text}`,
|
|
1285
|
+
text
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
try {
|
|
1289
|
+
return JSON.parse(text);
|
|
1290
|
+
} catch {
|
|
1291
|
+
throw new VaultFlowClientError(
|
|
1292
|
+
step,
|
|
1293
|
+
`${path} returned 200 with a non-JSON body`,
|
|
1294
|
+
text
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
// ../../src/client/relayer-yield-realizer.ts
|
|
1301
|
+
var REALIZE_OVERHEAD_RAW_USDC = 2500n;
|
|
1302
|
+
var RelayerRealizeError = class extends Error {
|
|
1303
|
+
constructor(code, message, detail = null) {
|
|
1304
|
+
super(message);
|
|
1305
|
+
this.code = code;
|
|
1306
|
+
this.detail = detail;
|
|
1307
|
+
this.name = "RelayerRealizeError";
|
|
1308
|
+
}
|
|
1309
|
+
code;
|
|
1310
|
+
detail;
|
|
1311
|
+
};
|
|
1312
|
+
var RelayerYieldRealizer = class {
|
|
1313
|
+
vaultFlows;
|
|
1314
|
+
constructor(config) {
|
|
1315
|
+
this.vaultFlows = new VaultFlowClient({
|
|
1316
|
+
facilitatorBaseUrl: config.facilitatorBaseUrl,
|
|
1317
|
+
signer: config.signer,
|
|
1318
|
+
rpc: config.rpc,
|
|
1319
|
+
...config.fetchImpl === void 0 ? {} : { fetchImpl: config.fetchImpl },
|
|
1320
|
+
...config.lookupTablesFor === void 0 ? {} : { lookupTablesFor: config.lookupTablesFor }
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
async ensureUsdcAvailable(input) {
|
|
1324
|
+
const shortfallRawUsdc = input.amountRawUsdc;
|
|
1325
|
+
await this.assertSpendableYield(shortfallRawUsdc);
|
|
1326
|
+
let outcome;
|
|
1327
|
+
try {
|
|
1328
|
+
outcome = await this.vaultFlows.withdraw({
|
|
1329
|
+
amountRawUsdc: shortfallRawUsdc,
|
|
1330
|
+
// The relayer refuses to prepare this withdrawal beyond the spendable
|
|
1331
|
+
// yield — the principal-protection guard the client cannot bypass.
|
|
1332
|
+
purpose: "yield_realize"
|
|
1333
|
+
});
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
throw this.mapWithdrawError(error);
|
|
1336
|
+
}
|
|
1337
|
+
if (outcome.status !== "confirmed" || outcome.txSignature === null) {
|
|
1338
|
+
throw new RelayerRealizeError(
|
|
1339
|
+
"realize_not_confirmed",
|
|
1340
|
+
`yield realize withdrawal did not confirm (status=${outcome.status})`,
|
|
1341
|
+
outcome
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
realizedRawUsdc: BigInt(outcome.actualWithdrawRawUsdc ?? "0"),
|
|
1346
|
+
txSignature: outcome.txSignature
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Refuses to realize more than the ledger's spendable yield (principal).
|
|
1351
|
+
* getBudget syncs the relayer's ledger from chain first (best-effort), so a
|
|
1352
|
+
* long-running client sees yield as it accrues instead of a frozen view.
|
|
1353
|
+
*/
|
|
1354
|
+
async assertSpendableYield(shortfallRawUsdc) {
|
|
1355
|
+
let spendable;
|
|
1356
|
+
try {
|
|
1357
|
+
const budget = await this.vaultFlows.getBudget();
|
|
1358
|
+
spendable = BigInt(budget.spendableYieldRawUsdc);
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
throw new RelayerRealizeError(
|
|
1361
|
+
"budget_unavailable",
|
|
1362
|
+
"could not read the spendable-yield budget",
|
|
1363
|
+
error
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
const requiredRawUsdc = shortfallRawUsdc + REALIZE_OVERHEAD_RAW_USDC;
|
|
1367
|
+
if (spendable < requiredRawUsdc) {
|
|
1368
|
+
throw new RelayerRealizeError(
|
|
1369
|
+
"insufficient_yield",
|
|
1370
|
+
`spendable yield ${spendable} cannot cover ${shortfallRawUsdc} raw USDC plus the ${REALIZE_OVERHEAD_RAW_USDC} raw fee headroom; the principal is never spent \u2014 wait for more yield`,
|
|
1371
|
+
{ spendableYieldRawUsdc: spendable.toString() }
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
mapWithdrawError(error) {
|
|
1376
|
+
if (!(error instanceof VaultFlowClientError)) {
|
|
1377
|
+
return new RelayerRealizeError(
|
|
1378
|
+
"prepare_failed",
|
|
1379
|
+
`yield realize failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1380
|
+
error
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
const serverCode = errorCodeFrom(error.detail);
|
|
1384
|
+
if (serverCode === "insufficient_yield" || serverCode === "post_state_principal_invariant_failed") {
|
|
1385
|
+
return new RelayerRealizeError(
|
|
1386
|
+
"insufficient_yield",
|
|
1387
|
+
"the relayer refused to realize beyond the spendable yield; the principal is never spent \u2014 wait for more yield",
|
|
1388
|
+
error.detail
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
return new RelayerRealizeError(
|
|
1392
|
+
error.step === "submit" ? "submit_failed" : "prepare_failed",
|
|
1393
|
+
error.message,
|
|
1394
|
+
error.detail
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
function errorCodeFrom(detail) {
|
|
1399
|
+
if (typeof detail !== "string") {
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
try {
|
|
1403
|
+
const parsed = JSON.parse(detail);
|
|
1404
|
+
return typeof parsed.error?.code === "string" ? parsed.error.code : null;
|
|
1405
|
+
} catch {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// ../../src/x402/headers.ts
|
|
1084
1411
|
import { z } from "zod";
|
|
1085
1412
|
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
1413
|
var MAX_HEADER_JSON_BYTES = 16384;
|
|
1090
1414
|
var X402HeaderError = class extends Error {
|
|
1091
1415
|
reason;
|
|
@@ -1132,16 +1456,6 @@ var sublyPaymentPayloadSchema = z.object({
|
|
|
1132
1456
|
temporarySettlementSignature: z.string().min(1).max(128)
|
|
1133
1457
|
})
|
|
1134
1458
|
}).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
1459
|
function decodeX402Header(headerValue) {
|
|
1146
1460
|
if (headerValue.length > Math.ceil(MAX_HEADER_JSON_BYTES * 4 / 3) + 4) {
|
|
1147
1461
|
throw new X402HeaderError(
|
|
@@ -1159,285 +1473,466 @@ function decodeX402Header(headerValue) {
|
|
|
1159
1473
|
);
|
|
1160
1474
|
}
|
|
1161
1475
|
}
|
|
1162
|
-
function
|
|
1163
|
-
|
|
1476
|
+
function requestBodyHashFor(body2) {
|
|
1477
|
+
if (body2 === null || body2 === void 0 || body2.length === 0) {
|
|
1478
|
+
return EMPTY_BODY_HASH;
|
|
1479
|
+
}
|
|
1480
|
+
return sha256TaggedHex(
|
|
1481
|
+
typeof body2 === "string" ? Buffer.from(body2, "utf8") : Buffer.from(body2)
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// ../../src/x402/standard-requirements.ts
|
|
1486
|
+
import { z as z2 } from "zod";
|
|
1487
|
+
var STANDARD_EXACT_SCHEME = "exact";
|
|
1488
|
+
var standardExactRequirementSchema = z2.object({
|
|
1489
|
+
scheme: z2.literal(STANDARD_EXACT_SCHEME),
|
|
1490
|
+
/** CAIP-2 chain id, e.g. "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp". */
|
|
1491
|
+
network: z2.string().min(1),
|
|
1492
|
+
/** SPL mint (Solana) or token contract (EVM); Subly only pays USDC/Solana. */
|
|
1493
|
+
asset: z2.string().min(1),
|
|
1494
|
+
/** Exact price in the asset's atomic units, as a decimal string. */
|
|
1495
|
+
amount: z2.string().regex(/^[1-9]\d*$/),
|
|
1496
|
+
/** Recipient wallet; the transfer destination ATA is derived from it. */
|
|
1497
|
+
payTo: z2.string().min(1),
|
|
1498
|
+
maxTimeoutSeconds: z2.number().int().positive().optional(),
|
|
1499
|
+
extra: z2.object({
|
|
1500
|
+
/** Facilitator address that pays the tx fee (gas sponsorship). */
|
|
1501
|
+
feePayer: z2.string().min(1).optional()
|
|
1502
|
+
}).loose().optional()
|
|
1503
|
+
}).loose();
|
|
1504
|
+
var standardPaymentRequiredSchema = z2.object({
|
|
1505
|
+
x402Version: z2.number().int(),
|
|
1506
|
+
accepts: z2.array(z2.unknown()),
|
|
1507
|
+
error: z2.string().optional(),
|
|
1508
|
+
resource: z2.object({ url: z2.string().optional() }).loose().optional()
|
|
1509
|
+
}).loose();
|
|
1510
|
+
var StandardX402ChallengeError = class extends Error {
|
|
1511
|
+
reason;
|
|
1512
|
+
constructor(reason, message) {
|
|
1513
|
+
super(message);
|
|
1514
|
+
this.name = "StandardX402ChallengeError";
|
|
1515
|
+
this.reason = reason;
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
function parseStandardChallenge(challenge) {
|
|
1519
|
+
const parsed = standardPaymentRequiredSchema.safeParse(challenge);
|
|
1164
1520
|
if (!parsed.success) {
|
|
1165
|
-
throw new
|
|
1521
|
+
throw new StandardX402ChallengeError(
|
|
1166
1522
|
"invalid_payment_required",
|
|
1167
|
-
"
|
|
1523
|
+
"Response is not a valid x402 PaymentRequired object"
|
|
1168
1524
|
);
|
|
1169
1525
|
}
|
|
1170
|
-
const
|
|
1171
|
-
const requirement =
|
|
1172
|
-
|
|
1526
|
+
const solanaExactRequirements = parsed.data.accepts.flatMap((candidate) => {
|
|
1527
|
+
const requirement = standardExactRequirementSchema.safeParse(candidate);
|
|
1528
|
+
if (!requirement.success) {
|
|
1529
|
+
return [];
|
|
1530
|
+
}
|
|
1531
|
+
return requirement.data.network.startsWith("solana:") ? [requirement.data] : [];
|
|
1173
1532
|
});
|
|
1174
|
-
return { paymentRequired: parsed.data,
|
|
1533
|
+
return { paymentRequired: parsed.data, solanaExactRequirements };
|
|
1175
1534
|
}
|
|
1176
|
-
function
|
|
1177
|
-
|
|
1178
|
-
|
|
1535
|
+
function decodeStandardPaymentRequiredHeader(headerValue) {
|
|
1536
|
+
let decoded;
|
|
1537
|
+
try {
|
|
1538
|
+
decoded = decodeX402Header(headerValue);
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
throw new StandardX402ChallengeError(
|
|
1541
|
+
error instanceof X402HeaderError ? error.reason : "invalid_header",
|
|
1542
|
+
"Cannot decode the payment-required header"
|
|
1543
|
+
);
|
|
1179
1544
|
}
|
|
1180
|
-
return
|
|
1181
|
-
|
|
1182
|
-
|
|
1545
|
+
return parseStandardChallenge(decoded);
|
|
1546
|
+
}
|
|
1547
|
+
function selectPayableSolanaRequirement(requirements, options) {
|
|
1548
|
+
const network = options?.network ?? SOLANA_MAINNET_NETWORK;
|
|
1549
|
+
const usdcMint = options?.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1550
|
+
const requirement = requirements.find(
|
|
1551
|
+
(candidate) => candidate.network === network && candidate.asset === usdcMint
|
|
1552
|
+
) ?? null;
|
|
1553
|
+
if (requirement === null) {
|
|
1554
|
+
throw new StandardX402ChallengeError(
|
|
1555
|
+
"no_payable_requirement",
|
|
1556
|
+
`The challenge has no Solana exact requirement on ${network} paying ${usdcMint}`
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
return {
|
|
1560
|
+
requirement,
|
|
1561
|
+
amountRawUsdc: BigInt(requirement.amount),
|
|
1562
|
+
payTo: requirement.payTo,
|
|
1563
|
+
feePayer: requirement.extra?.feePayer ?? null
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function standardRequirementMatchesSelected(candidate, selected) {
|
|
1567
|
+
const parsed = standardExactRequirementSchema.safeParse(candidate);
|
|
1568
|
+
return parsed.success && stableJson(parsed.data) === stableJson(selected.requirement);
|
|
1569
|
+
}
|
|
1570
|
+
function stableJson(value) {
|
|
1571
|
+
return JSON.stringify(sortJson(value));
|
|
1572
|
+
}
|
|
1573
|
+
function sortJson(value) {
|
|
1574
|
+
if (Array.isArray(value)) {
|
|
1575
|
+
return value.map(sortJson);
|
|
1576
|
+
}
|
|
1577
|
+
if (value !== null && typeof value === "object") {
|
|
1578
|
+
return Object.fromEntries(
|
|
1579
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
return value;
|
|
1183
1583
|
}
|
|
1184
1584
|
|
|
1185
|
-
// src/client/
|
|
1186
|
-
var
|
|
1585
|
+
// ../../src/client/standard-x402-payer.ts
|
|
1586
|
+
var StandardX402PayError = class extends Error {
|
|
1187
1587
|
constructor(reason, message, detail = null) {
|
|
1188
1588
|
super(message);
|
|
1189
1589
|
this.reason = reason;
|
|
1190
1590
|
this.detail = detail;
|
|
1191
|
-
this.name = "
|
|
1591
|
+
this.name = "StandardX402PayError";
|
|
1192
1592
|
}
|
|
1193
1593
|
reason;
|
|
1194
1594
|
detail;
|
|
1195
1595
|
};
|
|
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;
|
|
1596
|
+
var StandardX402Payer = class {
|
|
1597
|
+
realizer;
|
|
1598
|
+
x402Fetch;
|
|
1599
|
+
probeFetch;
|
|
1212
1600
|
defaultMaxAmountRawUsdc;
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
maxBodyChars;
|
|
1216
|
-
nowMs;
|
|
1601
|
+
network;
|
|
1602
|
+
usdcMint;
|
|
1217
1603
|
stateStore;
|
|
1218
1604
|
pending = /* @__PURE__ */ new Map();
|
|
1219
1605
|
inFlight = /* @__PURE__ */ new Map();
|
|
1606
|
+
nowMs;
|
|
1220
1607
|
constructor(config) {
|
|
1221
|
-
this.
|
|
1222
|
-
this.
|
|
1223
|
-
this.
|
|
1224
|
-
this.paymentStatusFor = config.paymentStatusFor ?? (async () => "indeterminate");
|
|
1608
|
+
this.realizer = config.realizer;
|
|
1609
|
+
this.x402Fetch = config.x402Fetch;
|
|
1610
|
+
this.probeFetch = config.probeFetch ?? fetch;
|
|
1225
1611
|
this.defaultMaxAmountRawUsdc = config.defaultMaxAmountRawUsdc;
|
|
1226
|
-
this.
|
|
1227
|
-
this.
|
|
1228
|
-
this.maxBodyChars = config.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS;
|
|
1229
|
-
this.nowMs = config.nowMs ?? (() => Date.now());
|
|
1612
|
+
this.network = config.network ?? SOLANA_MAINNET_NETWORK;
|
|
1613
|
+
this.usdcMint = config.usdcMint ?? SUBLY_VAULT.usdcMint;
|
|
1230
1614
|
this.stateStore = config.stateStore ?? null;
|
|
1615
|
+
this.nowMs = config.nowMs ?? (() => Date.now());
|
|
1231
1616
|
if (this.stateStore !== null) {
|
|
1232
1617
|
for (const record of this.stateStore.load()) {
|
|
1233
|
-
|
|
1234
|
-
this.pending.set(url2, entry);
|
|
1618
|
+
this.pending.set(record.key, record);
|
|
1235
1619
|
}
|
|
1236
1620
|
}
|
|
1237
1621
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1622
|
+
pay(input) {
|
|
1623
|
+
const method2 = (input.method ?? "GET").toUpperCase();
|
|
1624
|
+
const requestBodyHash = requestBodyHashFor(input.body ?? null);
|
|
1625
|
+
const pendingKey = pendingPaymentKey({
|
|
1626
|
+
url: input.url,
|
|
1627
|
+
method: method2,
|
|
1628
|
+
requestBodyHash
|
|
1629
|
+
});
|
|
1630
|
+
const existingFlow = this.inFlight.get(pendingKey);
|
|
1631
|
+
if (existingFlow !== void 0) {
|
|
1632
|
+
return existingFlow;
|
|
1246
1633
|
}
|
|
1247
|
-
const flow = this.run(
|
|
1248
|
-
|
|
1634
|
+
const flow = this.run(input, {
|
|
1635
|
+
method: method2,
|
|
1636
|
+
requestBodyHash,
|
|
1637
|
+
pendingKey
|
|
1638
|
+
}).finally(() => {
|
|
1639
|
+
this.inFlight.delete(pendingKey);
|
|
1249
1640
|
});
|
|
1250
|
-
this.inFlight.set(
|
|
1641
|
+
this.inFlight.set(pendingKey, flow);
|
|
1251
1642
|
return flow;
|
|
1252
1643
|
}
|
|
1253
|
-
async run(
|
|
1254
|
-
const {
|
|
1255
|
-
const
|
|
1256
|
-
if (
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
-
});
|
|
1644
|
+
async run(input, computed) {
|
|
1645
|
+
const { method: method2, requestBodyHash, pendingKey } = computed;
|
|
1646
|
+
const existingPending = this.pending.get(pendingKey);
|
|
1647
|
+
if (existingPending !== void 0 && input.forceNewPayment !== true) {
|
|
1648
|
+
throw new StandardX402PayError(
|
|
1649
|
+
"payment_outcome_unknown",
|
|
1650
|
+
"a previous external x402 payment for this request has an unknown outcome. Verify whether it settled before purchasing again; to pay again anyway, call with forceNewPayment=true.",
|
|
1651
|
+
existingPending
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
if (existingPending !== void 0 && input.forceNewPayment === true) {
|
|
1655
|
+
try {
|
|
1656
|
+
this.untrack(pendingKey);
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
throw new StandardX402PayError(
|
|
1659
|
+
"state_persist_failed",
|
|
1660
|
+
"could not clear the previous pending x402 marker before forcing a new payment",
|
|
1661
|
+
error
|
|
1662
|
+
);
|
|
1284
1663
|
}
|
|
1285
1664
|
}
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
};
|
|
1665
|
+
const init = {
|
|
1666
|
+
method: method2,
|
|
1667
|
+
...input.body === void 0 ? {} : { body: input.body },
|
|
1668
|
+
...input.headers === void 0 ? {} : { headers: input.headers }
|
|
1669
|
+
};
|
|
1670
|
+
const probe = await this.probeFetch(input.url, init);
|
|
1671
|
+
if (probe.status !== 402) {
|
|
1672
|
+
return { paid: false, status: probe.status, body: await probe.text() };
|
|
1295
1673
|
}
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
"
|
|
1674
|
+
const selected = await this.selectRequirement(probe);
|
|
1675
|
+
const cap = input.maxAmountRawUsdc ?? this.defaultMaxAmountRawUsdc;
|
|
1676
|
+
if (selected.amountRawUsdc > cap) {
|
|
1677
|
+
throw new StandardX402PayError(
|
|
1678
|
+
"amount_exceeds_client_cap",
|
|
1679
|
+
`the challenge demands ${selected.amountRawUsdc} raw USDC, above the client cap of ${cap}; nothing was paid`,
|
|
1680
|
+
{ amountRawUsdc: selected.amountRawUsdc.toString(), payTo: selected.payTo }
|
|
1301
1681
|
);
|
|
1302
1682
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1683
|
+
let realized;
|
|
1684
|
+
try {
|
|
1685
|
+
realized = await this.realizer.ensureUsdcAvailable({
|
|
1686
|
+
amountRawUsdc: selected.amountRawUsdc
|
|
1687
|
+
});
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
throw new StandardX402PayError(
|
|
1690
|
+
"realize_failed",
|
|
1691
|
+
`could not realize yield to cover ${selected.amountRawUsdc} raw USDC: ${error instanceof Error ? error.message : String(error)}`,
|
|
1692
|
+
error
|
|
1308
1693
|
);
|
|
1309
1694
|
}
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1695
|
+
const pendingRecord = {
|
|
1696
|
+
key: pendingKey,
|
|
1697
|
+
url: input.url,
|
|
1698
|
+
method: method2,
|
|
1699
|
+
requestBodyHash,
|
|
1700
|
+
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1701
|
+
payTo: selected.payTo,
|
|
1702
|
+
feePayer: selected.feePayer,
|
|
1703
|
+
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1704
|
+
realizeTxSignature: realized.txSignature,
|
|
1705
|
+
status: "realized",
|
|
1706
|
+
createdAtMs: this.nowMs(),
|
|
1707
|
+
updatedAtMs: this.nowMs()
|
|
1708
|
+
};
|
|
1709
|
+
try {
|
|
1710
|
+
this.track(pendingRecord);
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
throw new StandardX402PayError(
|
|
1713
|
+
"state_persist_failed",
|
|
1714
|
+
"could not persist the pending x402 marker; refusing to attempt the external payment because a restart would not be double-payment safe",
|
|
1715
|
+
{ error, pendingPayment: pendingRecord }
|
|
1321
1716
|
);
|
|
1322
1717
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1718
|
+
let response;
|
|
1719
|
+
try {
|
|
1720
|
+
response = await this.x402Fetch(input.url, init, selected);
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1723
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1724
|
+
});
|
|
1725
|
+
throw new StandardX402PayError(
|
|
1726
|
+
"payment_outcome_unknown",
|
|
1727
|
+
`the x402 payment attempt failed after yield was realized; verify whether it settled before paying again: ${error instanceof Error ? error.message : String(error)}`,
|
|
1728
|
+
{ error, persistError }
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
const bodyText = await response.text();
|
|
1732
|
+
if (response.status !== 200) {
|
|
1733
|
+
const persistError = this.tryMarkUnknown(pendingKey, {
|
|
1734
|
+
status: response.status,
|
|
1735
|
+
body: bodyText
|
|
1736
|
+
});
|
|
1737
|
+
throw new StandardX402PayError(
|
|
1738
|
+
"payment_outcome_unknown",
|
|
1739
|
+
`the x402 payment attempt returned ${response.status} after yield was realized; verify whether it settled before paying again`,
|
|
1740
|
+
{ status: response.status, body: bodyText, persistError }
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
this.clearDelivered(pendingKey);
|
|
1744
|
+
return {
|
|
1745
|
+
paid: true,
|
|
1746
|
+
status: response.status,
|
|
1747
|
+
body: bodyText,
|
|
1748
|
+
payment: {
|
|
1749
|
+
amountRawUsdc: selected.amountRawUsdc.toString(),
|
|
1750
|
+
payTo: selected.payTo,
|
|
1751
|
+
feePayer: selected.feePayer,
|
|
1752
|
+
realizedRawUsdc: realized.realizedRawUsdc.toString(),
|
|
1753
|
+
realizeTxSignature: realized.txSignature
|
|
1754
|
+
}
|
|
1336
1755
|
};
|
|
1337
|
-
this.track(url2, entry);
|
|
1338
|
-
return this.deliver(url2, entry, { retried: false, budgetBefore });
|
|
1339
1756
|
}
|
|
1340
|
-
/**
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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;
|
|
1757
|
+
/** Reads the challenge from the header (preferred) or the JSON body. */
|
|
1758
|
+
async selectRequirement(probe) {
|
|
1759
|
+
const header = probe.headers.get(PAYMENT_REQUIRED_HEADER);
|
|
1760
|
+
let requirements;
|
|
1349
1761
|
try {
|
|
1350
|
-
|
|
1351
|
-
|
|
1762
|
+
if (header !== null) {
|
|
1763
|
+
requirements = decodeStandardPaymentRequiredHeader(header).solanaExactRequirements;
|
|
1764
|
+
} else {
|
|
1765
|
+
requirements = parseStandardChallenge(
|
|
1766
|
+
await probe.json()
|
|
1767
|
+
).solanaExactRequirements;
|
|
1768
|
+
}
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
throw new StandardX402PayError(
|
|
1771
|
+
error instanceof StandardX402ChallengeError ? error.reason : "invalid_challenge",
|
|
1772
|
+
"could not parse the x402 402 challenge",
|
|
1773
|
+
error
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
try {
|
|
1777
|
+
return selectPayableSolanaRequirement(requirements, {
|
|
1778
|
+
network: this.network,
|
|
1779
|
+
usdcMint: this.usdcMint
|
|
1352
1780
|
});
|
|
1353
|
-
secondText = await second.text();
|
|
1354
1781
|
} catch (error) {
|
|
1355
|
-
throw new
|
|
1356
|
-
"
|
|
1357
|
-
|
|
1358
|
-
|
|
1782
|
+
throw new StandardX402PayError(
|
|
1783
|
+
"no_payable_requirement",
|
|
1784
|
+
error instanceof Error ? error.message : String(error),
|
|
1785
|
+
error
|
|
1359
1786
|
);
|
|
1360
1787
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1788
|
+
}
|
|
1789
|
+
track(record) {
|
|
1790
|
+
const previous = this.pending.get(record.key);
|
|
1791
|
+
this.pending.set(record.key, record);
|
|
1792
|
+
try {
|
|
1793
|
+
this.persist();
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
if (previous === void 0) {
|
|
1796
|
+
this.pending.delete(record.key);
|
|
1797
|
+
} else {
|
|
1798
|
+
this.pending.set(record.key, previous);
|
|
1799
|
+
}
|
|
1800
|
+
throw error;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
markUnknown(key, detail) {
|
|
1804
|
+
const current = this.pending.get(key);
|
|
1805
|
+
if (current === void 0) {
|
|
1806
|
+
return;
|
|
1379
1807
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1808
|
+
const next = {
|
|
1809
|
+
...current,
|
|
1810
|
+
status: "external_outcome_unknown",
|
|
1811
|
+
updatedAtMs: this.nowMs(),
|
|
1812
|
+
detail
|
|
1813
|
+
};
|
|
1814
|
+
this.pending.set(key, next);
|
|
1815
|
+
try {
|
|
1382
1816
|
this.persist();
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
this.pending.set(key, current);
|
|
1819
|
+
throw error;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
tryMarkUnknown(key, detail) {
|
|
1823
|
+
try {
|
|
1824
|
+
this.markUnknown(key, detail);
|
|
1825
|
+
return null;
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
return error;
|
|
1388
1828
|
}
|
|
1389
|
-
throw new PaidFetchError(
|
|
1390
|
-
"delivery_failed_payment_pending",
|
|
1391
|
-
`paid delivery failed with ${second.status} (paymentId=${entry.paymentId}); the payment may already have settled. Call this tool again with the same URL to retry delivery with the same payment signature \u2014 do NOT treat this as unpaid.`,
|
|
1392
|
-
{
|
|
1393
|
-
paymentId: entry.paymentId,
|
|
1394
|
-
status: second.status,
|
|
1395
|
-
body: this.truncated(secondText)
|
|
1396
|
-
}
|
|
1397
|
-
);
|
|
1398
1829
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1830
|
+
untrack(key) {
|
|
1831
|
+
const previous = this.pending.get(key);
|
|
1832
|
+
const existed = previous !== void 0;
|
|
1833
|
+
this.pending.delete(key);
|
|
1834
|
+
try {
|
|
1835
|
+
this.persist();
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
if (existed) {
|
|
1838
|
+
this.pending.set(key, previous);
|
|
1404
1839
|
}
|
|
1840
|
+
throw error;
|
|
1405
1841
|
}
|
|
1406
|
-
this.pending.set(url2, entry);
|
|
1407
|
-
this.persist();
|
|
1408
1842
|
}
|
|
1409
|
-
|
|
1410
|
-
this.pending.
|
|
1411
|
-
this.
|
|
1843
|
+
clearDelivered(key) {
|
|
1844
|
+
const previous = this.pending.get(key);
|
|
1845
|
+
this.pending.delete(key);
|
|
1846
|
+
try {
|
|
1847
|
+
this.persist();
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
if (previous !== void 0) {
|
|
1850
|
+
this.pending.set(key, previous);
|
|
1851
|
+
}
|
|
1852
|
+
console.error(
|
|
1853
|
+
`[subly-x402] payment delivered but pending marker could not be cleared: ${error instanceof Error ? error.message : String(error)}`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1412
1856
|
}
|
|
1413
1857
|
persist() {
|
|
1414
1858
|
if (this.stateStore === null) {
|
|
1415
1859
|
return;
|
|
1416
1860
|
}
|
|
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;
|
|
1861
|
+
this.stateStore.save([...this.pending.values()]);
|
|
1424
1862
|
}
|
|
1425
1863
|
};
|
|
1426
|
-
function
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1864
|
+
function pendingPaymentKey(input) {
|
|
1865
|
+
return `${input.method}:${input.url}:${input.requestBodyHash}`;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// ../../src/client/relayer-payer.ts
|
|
1869
|
+
function createRelayerX402Payer(config) {
|
|
1870
|
+
const realizer = new RelayerYieldRealizer({
|
|
1871
|
+
facilitatorBaseUrl: config.facilitatorBaseUrl,
|
|
1872
|
+
signer: config.signer,
|
|
1873
|
+
rpc: config.rpc
|
|
1874
|
+
});
|
|
1875
|
+
return new StandardX402Payer({
|
|
1876
|
+
realizer,
|
|
1877
|
+
x402Fetch: config.x402Fetch,
|
|
1878
|
+
defaultMaxAmountRawUsdc: config.defaultMaxAmountRawUsdc,
|
|
1879
|
+
...config.stateStore === void 0 ? {} : { stateStore: config.stateStore }
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
// ../../src/client/standard-x402-state-store.ts
|
|
1884
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
1885
|
+
import { basename, dirname, join } from "node:path";
|
|
1886
|
+
function fileStandardX402StateStore(path) {
|
|
1887
|
+
return {
|
|
1888
|
+
load() {
|
|
1889
|
+
let text;
|
|
1890
|
+
try {
|
|
1891
|
+
text = readFileSync(path, "utf8");
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
if (isMissingFileError(error)) {
|
|
1894
|
+
return [];
|
|
1895
|
+
}
|
|
1896
|
+
throw error;
|
|
1897
|
+
}
|
|
1898
|
+
const parsed = JSON.parse(text);
|
|
1899
|
+
if (!Array.isArray(parsed)) {
|
|
1900
|
+
throw new Error(`pending payment state is not an array: ${path}`);
|
|
1901
|
+
}
|
|
1902
|
+
for (const [index, record] of parsed.entries()) {
|
|
1903
|
+
if (!isPendingPaymentRecord(record)) {
|
|
1904
|
+
throw new Error(
|
|
1905
|
+
`pending payment state has an invalid record at index ${index}: ${path}`
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
return parsed;
|
|
1910
|
+
},
|
|
1911
|
+
save(records) {
|
|
1912
|
+
const directory = dirname(path);
|
|
1913
|
+
mkdirSync(directory, { recursive: true });
|
|
1914
|
+
const tempPath = join(
|
|
1915
|
+
directory,
|
|
1916
|
+
`.${basename(path)}.${process.pid}.${Date.now()}.tmp`
|
|
1917
|
+
);
|
|
1918
|
+
writeFileSync(tempPath, JSON.stringify(records, null, 2));
|
|
1919
|
+
renameSync(tempPath, path);
|
|
1431
1920
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
function isMissingFileError(error) {
|
|
1924
|
+
return error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
1925
|
+
}
|
|
1926
|
+
function isPendingPaymentRecord(value) {
|
|
1927
|
+
if (value === null || typeof value !== "object") {
|
|
1928
|
+
return false;
|
|
1436
1929
|
}
|
|
1930
|
+
const record = value;
|
|
1931
|
+
return typeof record.key === "string" && typeof record.url === "string" && typeof record.method === "string" && typeof record.requestBodyHash === "string" && typeof record.amountRawUsdc === "string" && typeof record.payTo === "string" && (record.feePayer === null || typeof record.feePayer === "string") && typeof record.realizedRawUsdc === "string" && (record.realizeTxSignature === null || typeof record.realizeTxSignature === "string") && (record.status === "realized" || record.status === "external_outcome_unknown") && typeof record.createdAtMs === "number" && typeof record.updatedAtMs === "number";
|
|
1437
1932
|
}
|
|
1438
1933
|
|
|
1439
|
-
// src/solana/keys.ts
|
|
1440
|
-
import { readFileSync } from "node:fs";
|
|
1934
|
+
// ../../src/solana/keys.ts
|
|
1935
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1441
1936
|
import bs586 from "bs58";
|
|
1442
1937
|
import {
|
|
1443
1938
|
createKeyPairSignerFromBytes
|
|
@@ -1452,7 +1947,7 @@ async function loadKeyPairSigner(params) {
|
|
|
1452
1947
|
return createKeyPairSignerFromBytes(bytes);
|
|
1453
1948
|
}
|
|
1454
1949
|
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1455
|
-
const raw = JSON.parse(
|
|
1950
|
+
const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
|
|
1456
1951
|
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1457
1952
|
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1458
1953
|
}
|
|
@@ -1460,304 +1955,127 @@ async function loadKeyPairSigner(params) {
|
|
|
1460
1955
|
}
|
|
1461
1956
|
throw new Error(`${label} keypair is not configured`);
|
|
1462
1957
|
}
|
|
1958
|
+
function loadSecretKeyBytes(params) {
|
|
1959
|
+
const { base58Secret, jsonFilePath, label } = params;
|
|
1960
|
+
if (base58Secret !== void 0 && base58Secret.length > 0) {
|
|
1961
|
+
const bytes = bs586.decode(base58Secret);
|
|
1962
|
+
if (bytes.length !== 64) {
|
|
1963
|
+
throw new Error(`${label} base58 secret must decode to 64 bytes`);
|
|
1964
|
+
}
|
|
1965
|
+
return bytes;
|
|
1966
|
+
}
|
|
1967
|
+
if (jsonFilePath !== void 0 && jsonFilePath.length > 0) {
|
|
1968
|
+
const raw = JSON.parse(readFileSync2(jsonFilePath, "utf8"));
|
|
1969
|
+
if (!Array.isArray(raw) || raw.length !== 64) {
|
|
1970
|
+
throw new Error(`${label} keypair file must be a 64-byte JSON array`);
|
|
1971
|
+
}
|
|
1972
|
+
return Uint8Array.from(raw);
|
|
1973
|
+
}
|
|
1974
|
+
throw new Error(`${label} keypair is not configured`);
|
|
1975
|
+
}
|
|
1463
1976
|
|
|
1464
|
-
// src/solana/rpc.ts
|
|
1977
|
+
// ../../src/solana/rpc.ts
|
|
1465
1978
|
import { createSolanaRpc } from "@solana/kit";
|
|
1466
1979
|
function createRpc(url2) {
|
|
1467
1980
|
return createSolanaRpc(url2);
|
|
1468
1981
|
}
|
|
1469
1982
|
|
|
1470
|
-
// src/x402
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
|
1562
|
-
}
|
|
1563
|
-
});
|
|
1564
|
-
}
|
|
1565
|
-
selectRequirement(paymentRequiredHeader) {
|
|
1566
|
-
let sublyRequirements;
|
|
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
|
|
1983
|
+
// src/svm-x402-fetch.ts
|
|
1984
|
+
import { createKeyPairSignerFromBytes as createKeyPairSignerFromBytes2 } from "@solana/kit";
|
|
1985
|
+
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
|
|
1986
|
+
import { ExactSvmScheme, toClientSvmSigner } from "@x402/svm";
|
|
1987
|
+
async function createSvmX402Fetch(params) {
|
|
1988
|
+
const signer2 = toClientSvmSigner(
|
|
1989
|
+
await createKeyPairSignerFromBytes2(params.agentSecretKey)
|
|
1990
|
+
);
|
|
1991
|
+
return (url2, init, expected) => {
|
|
1992
|
+
const wrapped = wrapFetchWithPaymentFromConfig(fetch, {
|
|
1993
|
+
schemes: [
|
|
1994
|
+
{
|
|
1995
|
+
network: expected.requirement.network,
|
|
1996
|
+
client: new ExactSvmScheme(signer2, { rpcUrl: params.rpcUrl })
|
|
1997
|
+
}
|
|
1998
|
+
],
|
|
1999
|
+
paymentRequirementsSelector: (_x402Version, requirements) => selectExpectedRequirement(requirements, expected)
|
|
1624
2000
|
});
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
throw new X402ClientError(
|
|
1637
|
-
"invalid_prepare_response",
|
|
1638
|
-
"Facilitator prepare response is missing required fields"
|
|
1639
|
-
);
|
|
1640
|
-
}
|
|
1641
|
-
return prepared;
|
|
2001
|
+
return wrapped(url2, init);
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
function selectExpectedRequirement(requirements, expected) {
|
|
2005
|
+
const match = requirements.find(
|
|
2006
|
+
(candidate) => standardRequirementMatchesSelected(candidate, expected)
|
|
2007
|
+
);
|
|
2008
|
+
if (match === void 0) {
|
|
2009
|
+
throw new Error(
|
|
2010
|
+
"x402 challenge changed after preflight; refusing to pay an unchecked requirement"
|
|
2011
|
+
);
|
|
1642
2012
|
}
|
|
1643
|
-
|
|
2013
|
+
return match;
|
|
2014
|
+
}
|
|
1644
2015
|
|
|
1645
|
-
//
|
|
2016
|
+
// src/pay.ts
|
|
1646
2017
|
function fail(message) {
|
|
1647
|
-
|
|
2018
|
+
process.stderr.write(`${message}
|
|
2019
|
+
`);
|
|
1648
2020
|
process.exit(1);
|
|
1649
2021
|
}
|
|
1650
|
-
|
|
1651
|
-
// demo/pay.ts
|
|
1652
2022
|
var url = process.argv[2];
|
|
1653
2023
|
if (url === void 0 || !/^https?:\/\//.test(url)) {
|
|
1654
|
-
fail("Usage:
|
|
2024
|
+
fail("Usage: pay fetch <url> [maxAmountRawUsdc]");
|
|
1655
2025
|
}
|
|
1656
2026
|
var maxAmountArg = process.argv[3];
|
|
1657
|
-
var
|
|
2027
|
+
var relayerBaseUrl = process.env.SUBLY_RELAYER_URL ?? process.env.SUBLY_FACILITATOR_URL ?? "https://api.demo.sublyfi.com";
|
|
1658
2028
|
var rpcUrl = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
|
|
1659
2029
|
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 ?? "
|
|
2030
|
+
var pendingStatePath = process.env.SUBLY_MCP_STATE_PATH ?? join2(homedir(), ".subly", "standard-x402-pending.json");
|
|
1661
2031
|
var keyPairSigner = await loadKeyPairSigner({
|
|
1662
2032
|
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
1663
2033
|
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
1664
2034
|
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
1665
2035
|
});
|
|
2036
|
+
var agentSecretKey = loadSecretKeyBytes({
|
|
2037
|
+
base58Secret: process.env.SUBLY_DEMO_AGENT_KEYPAIR,
|
|
2038
|
+
jsonFilePath: process.env.SUBLY_DEMO_AGENT_KEYPAIR_PATH,
|
|
2039
|
+
label: "SUBLY_DEMO_AGENT_KEYPAIR"
|
|
2040
|
+
});
|
|
1666
2041
|
var signer = new LocalKeypairAgentWalletSigner(keyPairSigner);
|
|
1667
2042
|
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
|
-
}),
|
|
2043
|
+
var payer = createRelayerX402Payer({
|
|
2044
|
+
facilitatorBaseUrl: relayerBaseUrl,
|
|
2045
|
+
signer,
|
|
2046
|
+
rpc,
|
|
2047
|
+
x402Fetch: await createSvmX402Fetch({ agentSecretKey, rpcUrl }),
|
|
1736
2048
|
defaultMaxAmountRawUsdc,
|
|
1737
|
-
|
|
1738
|
-
paymentStatusFor,
|
|
1739
|
-
stateStore: fileStateStore(pendingStatePath)
|
|
2049
|
+
stateStore: fileStandardX402StateStore(pendingStatePath)
|
|
1740
2050
|
});
|
|
1741
|
-
|
|
2051
|
+
var method = process.env.SUBLY_PAY_METHOD;
|
|
2052
|
+
var body = process.env.SUBLY_PAY_BODY;
|
|
2053
|
+
console.error(`[pay] agent ${signer.walletAddress} -> ${url}`);
|
|
1742
2054
|
try {
|
|
1743
|
-
await ensureWalletOnboarded({ facilitatorBaseUrl, signer });
|
|
2055
|
+
await ensureWalletOnboarded({ facilitatorBaseUrl: relayerBaseUrl, signer });
|
|
1744
2056
|
} catch (error) {
|
|
1745
2057
|
console.error(
|
|
1746
|
-
`[
|
|
2058
|
+
`[pay] onboarding skipped: ${error instanceof Error ? error.message : String(error)}`
|
|
1747
2059
|
);
|
|
1748
2060
|
}
|
|
1749
2061
|
try {
|
|
1750
|
-
const result = await
|
|
2062
|
+
const result = await payer.pay({
|
|
1751
2063
|
url,
|
|
1752
|
-
...
|
|
2064
|
+
...method === void 0 ? {} : { method },
|
|
2065
|
+
...body === void 0 ? {} : { body, headers: { "content-type": "application/json" } },
|
|
2066
|
+
...maxAmountArg === void 0 ? {} : { maxAmountRawUsdc: BigInt(maxAmountArg) },
|
|
2067
|
+
...process.env.SUBLY_PAY_FORCE_NEW_PAYMENT === "1" ? { forceNewPayment: true } : {}
|
|
1753
2068
|
});
|
|
1754
2069
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1755
2070
|
`);
|
|
2071
|
+
if (!result.paid) {
|
|
2072
|
+
process.exit(1);
|
|
2073
|
+
}
|
|
1756
2074
|
} catch (error) {
|
|
1757
|
-
if (error instanceof
|
|
2075
|
+
if (error instanceof StandardX402PayError) {
|
|
1758
2076
|
process.stdout.write(
|
|
1759
2077
|
`${JSON.stringify(
|
|
1760
|
-
{ paid: false,
|
|
2078
|
+
{ paid: false, reason: error.reason, message: error.message },
|
|
1761
2079
|
null,
|
|
1762
2080
|
2
|
|
1763
2081
|
)}
|
|
@@ -1765,5 +2083,5 @@ try {
|
|
|
1765
2083
|
);
|
|
1766
2084
|
process.exit(1);
|
|
1767
2085
|
}
|
|
1768
|
-
fail(`[
|
|
2086
|
+
fail(`[pay] ${error instanceof Error ? error.message : String(error)}`);
|
|
1769
2087
|
}
|