@alleyboss/micropay-solana-x402-paywall 3.3.8 → 3.3.9
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/index.cjs +151 -525
- package/dist/index.d.cts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +153 -517
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4,9 +4,7 @@ var core = require('@x402/core');
|
|
|
4
4
|
var types = require('@x402/core/types');
|
|
5
5
|
var client = require('@x402/core/client');
|
|
6
6
|
var svm = require('@x402/svm');
|
|
7
|
-
var http = require('@x402/core/http');
|
|
8
7
|
var web3_js = require('@solana/web3.js');
|
|
9
|
-
var react = require('react');
|
|
10
8
|
var bs58 = require('bs58');
|
|
11
9
|
var jose = require('jose');
|
|
12
10
|
|
|
@@ -15,519 +13,6 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
15
13
|
var bs58__default = /*#__PURE__*/_interopDefault(bs58);
|
|
16
14
|
|
|
17
15
|
// src/index.ts
|
|
18
|
-
|
|
19
|
-
// src/client/types.ts
|
|
20
|
-
var TOKEN_MINTS = {
|
|
21
|
-
/** USDC on mainnet */
|
|
22
|
-
USDC_MAINNET: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
23
|
-
/** USDC on devnet */
|
|
24
|
-
USDC_DEVNET: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
25
|
-
/** USDT on mainnet */
|
|
26
|
-
USDT_MAINNET: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// src/client/payment.ts
|
|
30
|
-
function buildSolanaPayUrl(params) {
|
|
31
|
-
const { recipient, amount, splToken, reference, label, message } = params;
|
|
32
|
-
const url = new URL(`solana:${recipient}`);
|
|
33
|
-
if (amount !== void 0) {
|
|
34
|
-
url.searchParams.set("amount", amount.toString());
|
|
35
|
-
}
|
|
36
|
-
if (splToken) {
|
|
37
|
-
url.searchParams.set("spl-token", splToken);
|
|
38
|
-
}
|
|
39
|
-
if (reference) {
|
|
40
|
-
url.searchParams.set("reference", reference);
|
|
41
|
-
}
|
|
42
|
-
if (label) {
|
|
43
|
-
url.searchParams.set("label", label);
|
|
44
|
-
}
|
|
45
|
-
if (message) {
|
|
46
|
-
url.searchParams.set("message", message);
|
|
47
|
-
}
|
|
48
|
-
return url.toString();
|
|
49
|
-
}
|
|
50
|
-
function createPaymentFlow(config) {
|
|
51
|
-
const { network, recipientWallet, amount, asset = "native", memo } = config;
|
|
52
|
-
let decimals = 9;
|
|
53
|
-
let mintAddress;
|
|
54
|
-
if (asset === "usdc") {
|
|
55
|
-
decimals = 6;
|
|
56
|
-
mintAddress = network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
57
|
-
} else if (asset === "usdt") {
|
|
58
|
-
decimals = 6;
|
|
59
|
-
mintAddress = TOKEN_MINTS.USDT_MAINNET;
|
|
60
|
-
} else if (typeof asset === "object" && "mint" in asset) {
|
|
61
|
-
decimals = asset.decimals ?? 6;
|
|
62
|
-
mintAddress = asset.mint;
|
|
63
|
-
}
|
|
64
|
-
const naturalAmount = Number(amount) / Math.pow(10, decimals);
|
|
65
|
-
return {
|
|
66
|
-
/** Get the payment configuration */
|
|
67
|
-
getConfig: () => ({ ...config }),
|
|
68
|
-
/** Get amount in natural display units (e.g., 0.01 SOL) */
|
|
69
|
-
getDisplayAmount: () => naturalAmount,
|
|
70
|
-
/** Get amount formatted with symbol */
|
|
71
|
-
getFormattedAmount: () => {
|
|
72
|
-
const symbol = asset === "native" ? "SOL" : asset === "usdc" ? "USDC" : asset === "usdt" ? "USDT" : "tokens";
|
|
73
|
-
return `${naturalAmount.toFixed(decimals > 6 ? 4 : 2)} ${symbol}`;
|
|
74
|
-
},
|
|
75
|
-
/** Generate Solana Pay URL for QR codes */
|
|
76
|
-
getSolanaPayUrl: (options = {}) => {
|
|
77
|
-
return buildSolanaPayUrl({
|
|
78
|
-
recipient: recipientWallet,
|
|
79
|
-
amount: naturalAmount,
|
|
80
|
-
splToken: mintAddress,
|
|
81
|
-
label: options.label,
|
|
82
|
-
reference: options.reference,
|
|
83
|
-
message: memo
|
|
84
|
-
});
|
|
85
|
-
},
|
|
86
|
-
/** Get the token mint address (undefined for native SOL) */
|
|
87
|
-
getMintAddress: () => mintAddress,
|
|
88
|
-
/** Check if this is a native SOL payment */
|
|
89
|
-
isNativePayment: () => asset === "native",
|
|
90
|
-
/** Get network information */
|
|
91
|
-
getNetworkInfo: () => ({
|
|
92
|
-
network,
|
|
93
|
-
isMainnet: network === "mainnet-beta",
|
|
94
|
-
explorerUrl: network === "mainnet-beta" ? "https://explorer.solana.com" : "https://explorer.solana.com?cluster=devnet"
|
|
95
|
-
}),
|
|
96
|
-
/** Build explorer URL for a transaction */
|
|
97
|
-
getExplorerUrl: (signature) => {
|
|
98
|
-
const baseUrl = "https://explorer.solana.com/tx";
|
|
99
|
-
const cluster = network === "mainnet-beta" ? "" : "?cluster=devnet";
|
|
100
|
-
return `${baseUrl}/${signature}${cluster}`;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
function createPaymentReference() {
|
|
105
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
106
|
-
return crypto.randomUUID();
|
|
107
|
-
}
|
|
108
|
-
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
109
|
-
}
|
|
110
|
-
function createX402AuthorizationHeader(signature, paymentRequiredHeader) {
|
|
111
|
-
const cleanHeader = paymentRequiredHeader.replace(/^[Xx]402\s+/, "");
|
|
112
|
-
const required = http.decodePaymentRequiredHeader(cleanHeader);
|
|
113
|
-
const accepts = Array.isArray(required.accepts) ? required.accepts[0] : required.accepts;
|
|
114
|
-
const payload = {
|
|
115
|
-
accepted: accepts,
|
|
116
|
-
client: {
|
|
117
|
-
scheme: accepts.scheme,
|
|
118
|
-
// TypeScript knows this exists on PaymentRequirements
|
|
119
|
-
network: accepts.network
|
|
120
|
-
},
|
|
121
|
-
payment: {
|
|
122
|
-
signature
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
const token = http.encodePaymentSignatureHeader(payload);
|
|
126
|
-
return `x402 ${token}`;
|
|
127
|
-
}
|
|
128
|
-
async function sendSolanaPayment({
|
|
129
|
-
connection,
|
|
130
|
-
wallet,
|
|
131
|
-
recipientAddress,
|
|
132
|
-
amount,
|
|
133
|
-
// memo, // TODO: Add memo support to transaction
|
|
134
|
-
commitment = "confirmed"
|
|
135
|
-
}) {
|
|
136
|
-
if (!wallet.publicKey) {
|
|
137
|
-
throw new Error("Wallet not connected");
|
|
138
|
-
}
|
|
139
|
-
if (amount <= 0n) {
|
|
140
|
-
throw new Error("Amount must be greater than 0");
|
|
141
|
-
}
|
|
142
|
-
try {
|
|
143
|
-
const recipientPubkey = new web3_js.PublicKey(recipientAddress);
|
|
144
|
-
const transaction = new web3_js.Transaction().add(
|
|
145
|
-
web3_js.SystemProgram.transfer({
|
|
146
|
-
fromPubkey: wallet.publicKey,
|
|
147
|
-
toPubkey: recipientPubkey,
|
|
148
|
-
lamports: amount
|
|
149
|
-
})
|
|
150
|
-
);
|
|
151
|
-
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(commitment);
|
|
152
|
-
transaction.recentBlockhash = blockhash;
|
|
153
|
-
transaction.feePayer = wallet.publicKey;
|
|
154
|
-
const signature = await wallet.sendTransaction(transaction, connection);
|
|
155
|
-
await connection.confirmTransaction({
|
|
156
|
-
signature,
|
|
157
|
-
blockhash,
|
|
158
|
-
lastValidBlockHeight
|
|
159
|
-
}, commitment);
|
|
160
|
-
return {
|
|
161
|
-
signature,
|
|
162
|
-
amountSol: Number(amount) / web3_js.LAMPORTS_PER_SOL
|
|
163
|
-
};
|
|
164
|
-
} catch (error) {
|
|
165
|
-
console.error("Payment failed:", error);
|
|
166
|
-
throw new Error(error.message || "Payment failed");
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function usePaywallResource({
|
|
170
|
-
url,
|
|
171
|
-
connection,
|
|
172
|
-
wallet
|
|
173
|
-
}) {
|
|
174
|
-
const [data, setData] = react.useState(null);
|
|
175
|
-
const [isLocked, setIsLocked] = react.useState(true);
|
|
176
|
-
const [isLoading, setIsLoading] = react.useState(true);
|
|
177
|
-
const [error, setError] = react.useState(null);
|
|
178
|
-
const [paymentHeader, setPaymentHeader] = react.useState(null);
|
|
179
|
-
const [price, setPrice] = react.useState();
|
|
180
|
-
const [recipient, setRecipient] = react.useState();
|
|
181
|
-
const fetchData = react.useCallback(async (authHeader) => {
|
|
182
|
-
setIsLoading(true);
|
|
183
|
-
setError(null);
|
|
184
|
-
try {
|
|
185
|
-
const headers = {
|
|
186
|
-
"Content-Type": "application/json"
|
|
187
|
-
};
|
|
188
|
-
if (authHeader) {
|
|
189
|
-
headers["Authorization"] = authHeader;
|
|
190
|
-
}
|
|
191
|
-
const res = await fetch(url, {
|
|
192
|
-
headers,
|
|
193
|
-
credentials: "include"
|
|
194
|
-
// Ensure cookies are sent/received
|
|
195
|
-
});
|
|
196
|
-
if (res.status === 402) {
|
|
197
|
-
const wwwAuth = res.headers.get("WWW-Authenticate") || res.headers.get("Payment-Required");
|
|
198
|
-
const errorReason = res.headers.get("X-Payment-Error");
|
|
199
|
-
console.log("[usePaywallResource] 402 Response. Header:", wwwAuth, "Error:", errorReason);
|
|
200
|
-
if (authHeader && errorReason) {
|
|
201
|
-
console.error("[usePaywallResource] Verification Failed:", errorReason);
|
|
202
|
-
setError(`Verification Failed: ${errorReason}`);
|
|
203
|
-
}
|
|
204
|
-
if (wwwAuth) {
|
|
205
|
-
setPaymentHeader(wwwAuth);
|
|
206
|
-
try {
|
|
207
|
-
const { decodePaymentRequiredHeader: decodePaymentRequiredHeader2 } = await import('@x402/core/http');
|
|
208
|
-
const cleanHeader = wwwAuth.replace(/^[Xx]402\s+/, "");
|
|
209
|
-
const decoded = decodePaymentRequiredHeader2(cleanHeader);
|
|
210
|
-
console.log("[usePaywallResource] Decoded header:", decoded);
|
|
211
|
-
const accepts = Array.isArray(decoded.accepts) ? decoded.accepts[0] : decoded.accepts;
|
|
212
|
-
console.log("[usePaywallResource] Accepts:", accepts);
|
|
213
|
-
if (accepts) {
|
|
214
|
-
const amountStr = accepts.amount || accepts.price || accepts.maxAmountRequired || "0";
|
|
215
|
-
setPrice(BigInt(amountStr));
|
|
216
|
-
setRecipient(accepts.payTo);
|
|
217
|
-
console.log("[usePaywallResource] Set price:", amountStr, "recipient:", accepts.payTo);
|
|
218
|
-
} else {
|
|
219
|
-
console.warn("[usePaywallResource] No accepts found in header");
|
|
220
|
-
}
|
|
221
|
-
} catch (e) {
|
|
222
|
-
console.warn("[usePaywallResource] Failed to parse x402 header:", e);
|
|
223
|
-
}
|
|
224
|
-
} else {
|
|
225
|
-
console.warn("[usePaywallResource] 402 response missing WWW-Authenticate header");
|
|
226
|
-
}
|
|
227
|
-
setIsLocked(true);
|
|
228
|
-
setData(null);
|
|
229
|
-
} else if (res.ok) {
|
|
230
|
-
const json = await res.json();
|
|
231
|
-
setData(json);
|
|
232
|
-
setIsLocked(false);
|
|
233
|
-
} else {
|
|
234
|
-
throw new Error(`Request failed with status ${res.status}`);
|
|
235
|
-
}
|
|
236
|
-
} catch (err) {
|
|
237
|
-
setError(err.message || "Failed to fetch resource");
|
|
238
|
-
} finally {
|
|
239
|
-
setIsLoading(false);
|
|
240
|
-
}
|
|
241
|
-
}, [url]);
|
|
242
|
-
react.useEffect(() => {
|
|
243
|
-
fetchData();
|
|
244
|
-
}, [fetchData]);
|
|
245
|
-
const unlock = react.useCallback(async () => {
|
|
246
|
-
if (!paymentHeader || !price || !recipient) {
|
|
247
|
-
setError("Missing payment requirements");
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
setIsLoading(true);
|
|
251
|
-
try {
|
|
252
|
-
const { signature } = await sendSolanaPayment({
|
|
253
|
-
connection,
|
|
254
|
-
wallet,
|
|
255
|
-
recipientAddress: recipient,
|
|
256
|
-
amount: price
|
|
257
|
-
});
|
|
258
|
-
const authHeader = createX402AuthorizationHeader(signature, paymentHeader);
|
|
259
|
-
await fetchData(authHeader);
|
|
260
|
-
} catch (err) {
|
|
261
|
-
console.error("Unlock failed", err);
|
|
262
|
-
setError(err.message || "Payment/Unlock failed");
|
|
263
|
-
setIsLoading(false);
|
|
264
|
-
}
|
|
265
|
-
}, [connection, wallet, paymentHeader, price, recipient, fetchData]);
|
|
266
|
-
return {
|
|
267
|
-
data,
|
|
268
|
-
isLocked,
|
|
269
|
-
isLoading,
|
|
270
|
-
price,
|
|
271
|
-
recipient,
|
|
272
|
-
error,
|
|
273
|
-
unlock
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// src/pricing/utils.ts
|
|
278
|
-
function lamportsToSol(lamports) {
|
|
279
|
-
return Number(lamports) / 1e9;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// src/pricing/index.ts
|
|
283
|
-
var priceCache = null;
|
|
284
|
-
var currentConfig = {};
|
|
285
|
-
function configurePricing(newConfig) {
|
|
286
|
-
currentConfig = { ...currentConfig, ...newConfig };
|
|
287
|
-
}
|
|
288
|
-
var PROVIDERS = [
|
|
289
|
-
{
|
|
290
|
-
name: "coincap",
|
|
291
|
-
url: "https://api.coincap.io/v2/assets/solana",
|
|
292
|
-
parse: (data) => parseFloat(data.data?.priceUsd || "0")
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
name: "binance",
|
|
296
|
-
url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
|
|
297
|
-
parse: (data) => parseFloat(data.price || "0")
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
name: "coingecko",
|
|
301
|
-
url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
|
|
302
|
-
parse: (data) => data.solana?.usd || 0
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
name: "kraken",
|
|
306
|
-
url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
|
|
307
|
-
parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
|
|
308
|
-
}
|
|
309
|
-
];
|
|
310
|
-
async function fetchFromProvider(provider, timeout) {
|
|
311
|
-
const controller = new AbortController();
|
|
312
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
313
|
-
try {
|
|
314
|
-
const response = await fetch(provider.url, {
|
|
315
|
-
headers: { "Accept": "application/json" },
|
|
316
|
-
signal: controller.signal
|
|
317
|
-
});
|
|
318
|
-
if (!response.ok) {
|
|
319
|
-
throw new Error(`HTTP ${response.status}`);
|
|
320
|
-
}
|
|
321
|
-
const data = await response.json();
|
|
322
|
-
const price = provider.parse(data);
|
|
323
|
-
if (!price || price <= 0) {
|
|
324
|
-
throw new Error("Invalid price");
|
|
325
|
-
}
|
|
326
|
-
return { price, source: provider.name };
|
|
327
|
-
} finally {
|
|
328
|
-
clearTimeout(timeoutId);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
async function fetchPriceParallel(timeout) {
|
|
332
|
-
const promises = PROVIDERS.map(
|
|
333
|
-
(provider) => fetchFromProvider(provider, timeout).catch(() => null)
|
|
334
|
-
);
|
|
335
|
-
const results = await Promise.all(promises);
|
|
336
|
-
const validResult = results.find((r) => r !== null);
|
|
337
|
-
if (validResult) {
|
|
338
|
-
return validResult;
|
|
339
|
-
}
|
|
340
|
-
throw new Error("All providers failed");
|
|
341
|
-
}
|
|
342
|
-
async function fetchPriceSequential(timeout) {
|
|
343
|
-
for (const provider of PROVIDERS) {
|
|
344
|
-
try {
|
|
345
|
-
return await fetchFromProvider(provider, timeout);
|
|
346
|
-
} catch {
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
throw new Error("All providers failed");
|
|
351
|
-
}
|
|
352
|
-
async function getSolPrice() {
|
|
353
|
-
const cacheTTL = currentConfig.cacheTTL ?? 6e4;
|
|
354
|
-
const timeout = currentConfig.timeout ?? 3e3;
|
|
355
|
-
const useParallel = currentConfig.parallelFetch ?? true;
|
|
356
|
-
const now = Date.now();
|
|
357
|
-
if (priceCache && now - priceCache.timestamp < cacheTTL) {
|
|
358
|
-
return priceCache.data;
|
|
359
|
-
}
|
|
360
|
-
if (currentConfig.customProvider) {
|
|
361
|
-
try {
|
|
362
|
-
const price = await currentConfig.customProvider();
|
|
363
|
-
if (price > 0) {
|
|
364
|
-
const data = {
|
|
365
|
-
solPrice: price,
|
|
366
|
-
fetchedAt: /* @__PURE__ */ new Date(),
|
|
367
|
-
source: "custom"
|
|
368
|
-
};
|
|
369
|
-
priceCache = { data, timestamp: now };
|
|
370
|
-
return data;
|
|
371
|
-
}
|
|
372
|
-
} catch {
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
try {
|
|
376
|
-
const result = useParallel ? await fetchPriceParallel(timeout) : await fetchPriceSequential(timeout);
|
|
377
|
-
const data = {
|
|
378
|
-
solPrice: result.price,
|
|
379
|
-
fetchedAt: /* @__PURE__ */ new Date(),
|
|
380
|
-
source: result.source
|
|
381
|
-
};
|
|
382
|
-
priceCache = { data, timestamp: now };
|
|
383
|
-
return data;
|
|
384
|
-
} catch {
|
|
385
|
-
if (priceCache) {
|
|
386
|
-
return {
|
|
387
|
-
...priceCache.data,
|
|
388
|
-
source: `${priceCache.data.source} (stale)`
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
throw new Error(
|
|
392
|
-
"Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
async function lamportsToUsd(lamports) {
|
|
397
|
-
const { solPrice } = await getSolPrice();
|
|
398
|
-
const sol = Number(lamports) / 1e9;
|
|
399
|
-
return sol * solPrice;
|
|
400
|
-
}
|
|
401
|
-
async function usdToLamports(usd) {
|
|
402
|
-
const { solPrice } = await getSolPrice();
|
|
403
|
-
const sol = usd / solPrice;
|
|
404
|
-
return BigInt(Math.floor(sol * 1e9));
|
|
405
|
-
}
|
|
406
|
-
async function formatPriceDisplay(lamports) {
|
|
407
|
-
const { solPrice } = await getSolPrice();
|
|
408
|
-
const sol = Number(lamports) / 1e9;
|
|
409
|
-
const usd = sol * solPrice;
|
|
410
|
-
return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
|
|
411
|
-
}
|
|
412
|
-
function formatPriceSync(lamports, solPrice) {
|
|
413
|
-
const sol = Number(lamports) / 1e9;
|
|
414
|
-
const usd = sol * solPrice;
|
|
415
|
-
return {
|
|
416
|
-
sol,
|
|
417
|
-
usd,
|
|
418
|
-
formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
function clearPriceCache() {
|
|
422
|
-
priceCache = null;
|
|
423
|
-
}
|
|
424
|
-
function getProviders() {
|
|
425
|
-
return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// src/client/hooks.ts
|
|
429
|
-
function usePricing(refreshIntervalMs = 6e4) {
|
|
430
|
-
const [priceData, setPriceData] = react.useState(null);
|
|
431
|
-
const [isLoading, setIsLoading] = react.useState(true);
|
|
432
|
-
const [error, setError] = react.useState(null);
|
|
433
|
-
const intervalRef = react.useRef(null);
|
|
434
|
-
const fetchPrice = react.useCallback(async () => {
|
|
435
|
-
try {
|
|
436
|
-
setIsLoading(true);
|
|
437
|
-
setError(null);
|
|
438
|
-
const data = await getSolPrice();
|
|
439
|
-
setPriceData(data);
|
|
440
|
-
} catch (err) {
|
|
441
|
-
setError(err instanceof Error ? err.message : "Failed to fetch price");
|
|
442
|
-
} finally {
|
|
443
|
-
setIsLoading(false);
|
|
444
|
-
}
|
|
445
|
-
}, []);
|
|
446
|
-
react.useEffect(() => {
|
|
447
|
-
fetchPrice();
|
|
448
|
-
if (refreshIntervalMs > 0) {
|
|
449
|
-
intervalRef.current = setInterval(fetchPrice, refreshIntervalMs);
|
|
450
|
-
}
|
|
451
|
-
return () => {
|
|
452
|
-
if (intervalRef.current) {
|
|
453
|
-
clearInterval(intervalRef.current);
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
}, [fetchPrice, refreshIntervalMs]);
|
|
457
|
-
return {
|
|
458
|
-
solPrice: priceData?.solPrice ?? null,
|
|
459
|
-
source: priceData?.source ?? null,
|
|
460
|
-
fetchedAt: priceData?.fetchedAt ?? null,
|
|
461
|
-
isLoading,
|
|
462
|
-
error,
|
|
463
|
-
refresh: fetchPrice
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
function useLamportsToUsd(lamports) {
|
|
467
|
-
const { solPrice, isLoading } = usePricing();
|
|
468
|
-
if (isLoading || !solPrice || lamports === null) {
|
|
469
|
-
return { usd: null, formatted: null, isLoading };
|
|
470
|
-
}
|
|
471
|
-
const lamportsBigInt = typeof lamports === "number" ? BigInt(lamports) : lamports;
|
|
472
|
-
const sol = Number(lamportsBigInt) / 1e9;
|
|
473
|
-
const usd = sol * solPrice;
|
|
474
|
-
return {
|
|
475
|
-
usd,
|
|
476
|
-
formatted: `$${usd.toFixed(2)}`,
|
|
477
|
-
isLoading: false
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
function useMicropay() {
|
|
481
|
-
const [status, setStatus] = react.useState("idle");
|
|
482
|
-
const [error, setError] = react.useState(null);
|
|
483
|
-
const [signature, setSignature] = react.useState(null);
|
|
484
|
-
const reset = react.useCallback(() => {
|
|
485
|
-
setStatus("idle");
|
|
486
|
-
setError(null);
|
|
487
|
-
setSignature(null);
|
|
488
|
-
}, []);
|
|
489
|
-
const pay = react.useCallback(async (_options) => {
|
|
490
|
-
setStatus("pending");
|
|
491
|
-
setError(null);
|
|
492
|
-
try {
|
|
493
|
-
throw new Error(
|
|
494
|
-
"useMicropay requires implementation of onSign/onSend callbacks. See documentation for wallet adapter integration."
|
|
495
|
-
);
|
|
496
|
-
} catch (err) {
|
|
497
|
-
const errorMessage = err instanceof Error ? err.message : "Payment failed";
|
|
498
|
-
setError(errorMessage);
|
|
499
|
-
setStatus("error");
|
|
500
|
-
return { success: false, error: errorMessage };
|
|
501
|
-
}
|
|
502
|
-
}, []);
|
|
503
|
-
return {
|
|
504
|
-
status,
|
|
505
|
-
error,
|
|
506
|
-
signature,
|
|
507
|
-
pay,
|
|
508
|
-
reset
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
function useFormatPrice(lamports) {
|
|
512
|
-
const { solPrice, isLoading } = usePricing();
|
|
513
|
-
if (isLoading || !solPrice || lamports === null) {
|
|
514
|
-
return {
|
|
515
|
-
sol: null,
|
|
516
|
-
usd: null,
|
|
517
|
-
formatted: "Loading...",
|
|
518
|
-
isLoading
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
const lamportsBigInt = typeof lamports === "number" ? BigInt(lamports) : lamports;
|
|
522
|
-
const sol = Number(lamportsBigInt) / 1e9;
|
|
523
|
-
const usd = sol * solPrice;
|
|
524
|
-
return {
|
|
525
|
-
sol,
|
|
526
|
-
usd,
|
|
527
|
-
formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`,
|
|
528
|
-
isLoading: false
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
16
|
var LocalSvmFacilitator = class {
|
|
532
17
|
scheme = "exact";
|
|
533
18
|
caipFamily = "solana:*";
|
|
@@ -1080,15 +565,162 @@ async function getRemainingCredits(token, secret) {
|
|
|
1080
565
|
};
|
|
1081
566
|
}
|
|
1082
567
|
|
|
568
|
+
// src/pricing/utils.ts
|
|
569
|
+
function lamportsToSol(lamports) {
|
|
570
|
+
return Number(lamports) / 1e9;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/pricing/index.ts
|
|
574
|
+
var priceCache = null;
|
|
575
|
+
var currentConfig = {};
|
|
576
|
+
function configurePricing(newConfig) {
|
|
577
|
+
currentConfig = { ...currentConfig, ...newConfig };
|
|
578
|
+
}
|
|
579
|
+
var PROVIDERS = [
|
|
580
|
+
{
|
|
581
|
+
name: "coincap",
|
|
582
|
+
url: "https://api.coincap.io/v2/assets/solana",
|
|
583
|
+
parse: (data) => parseFloat(data.data?.priceUsd || "0")
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: "binance",
|
|
587
|
+
url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
|
|
588
|
+
parse: (data) => parseFloat(data.price || "0")
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: "coingecko",
|
|
592
|
+
url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
|
|
593
|
+
parse: (data) => data.solana?.usd || 0
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
name: "kraken",
|
|
597
|
+
url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
|
|
598
|
+
parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
|
|
599
|
+
}
|
|
600
|
+
];
|
|
601
|
+
async function fetchFromProvider(provider, timeout) {
|
|
602
|
+
const controller = new AbortController();
|
|
603
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
604
|
+
try {
|
|
605
|
+
const response = await fetch(provider.url, {
|
|
606
|
+
headers: { "Accept": "application/json" },
|
|
607
|
+
signal: controller.signal
|
|
608
|
+
});
|
|
609
|
+
if (!response.ok) {
|
|
610
|
+
throw new Error(`HTTP ${response.status}`);
|
|
611
|
+
}
|
|
612
|
+
const data = await response.json();
|
|
613
|
+
const price = provider.parse(data);
|
|
614
|
+
if (!price || price <= 0) {
|
|
615
|
+
throw new Error("Invalid price");
|
|
616
|
+
}
|
|
617
|
+
return { price, source: provider.name };
|
|
618
|
+
} finally {
|
|
619
|
+
clearTimeout(timeoutId);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function fetchPriceParallel(timeout) {
|
|
623
|
+
const promises = PROVIDERS.map(
|
|
624
|
+
(provider) => fetchFromProvider(provider, timeout).catch(() => null)
|
|
625
|
+
);
|
|
626
|
+
const results = await Promise.all(promises);
|
|
627
|
+
const validResult = results.find((r) => r !== null);
|
|
628
|
+
if (validResult) {
|
|
629
|
+
return validResult;
|
|
630
|
+
}
|
|
631
|
+
throw new Error("All providers failed");
|
|
632
|
+
}
|
|
633
|
+
async function fetchPriceSequential(timeout) {
|
|
634
|
+
for (const provider of PROVIDERS) {
|
|
635
|
+
try {
|
|
636
|
+
return await fetchFromProvider(provider, timeout);
|
|
637
|
+
} catch {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
throw new Error("All providers failed");
|
|
642
|
+
}
|
|
643
|
+
async function getSolPrice() {
|
|
644
|
+
const cacheTTL = currentConfig.cacheTTL ?? 6e4;
|
|
645
|
+
const timeout = currentConfig.timeout ?? 3e3;
|
|
646
|
+
const useParallel = currentConfig.parallelFetch ?? true;
|
|
647
|
+
const now = Date.now();
|
|
648
|
+
if (priceCache && now - priceCache.timestamp < cacheTTL) {
|
|
649
|
+
return priceCache.data;
|
|
650
|
+
}
|
|
651
|
+
if (currentConfig.customProvider) {
|
|
652
|
+
try {
|
|
653
|
+
const price = await currentConfig.customProvider();
|
|
654
|
+
if (price > 0) {
|
|
655
|
+
const data = {
|
|
656
|
+
solPrice: price,
|
|
657
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
658
|
+
source: "custom"
|
|
659
|
+
};
|
|
660
|
+
priceCache = { data, timestamp: now };
|
|
661
|
+
return data;
|
|
662
|
+
}
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
const result = useParallel ? await fetchPriceParallel(timeout) : await fetchPriceSequential(timeout);
|
|
668
|
+
const data = {
|
|
669
|
+
solPrice: result.price,
|
|
670
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
671
|
+
source: result.source
|
|
672
|
+
};
|
|
673
|
+
priceCache = { data, timestamp: now };
|
|
674
|
+
return data;
|
|
675
|
+
} catch {
|
|
676
|
+
if (priceCache) {
|
|
677
|
+
return {
|
|
678
|
+
...priceCache.data,
|
|
679
|
+
source: `${priceCache.data.source} (stale)`
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
throw new Error(
|
|
683
|
+
"Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function lamportsToUsd(lamports) {
|
|
688
|
+
const { solPrice } = await getSolPrice();
|
|
689
|
+
const sol = Number(lamports) / 1e9;
|
|
690
|
+
return sol * solPrice;
|
|
691
|
+
}
|
|
692
|
+
async function usdToLamports(usd) {
|
|
693
|
+
const { solPrice } = await getSolPrice();
|
|
694
|
+
const sol = usd / solPrice;
|
|
695
|
+
return BigInt(Math.floor(sol * 1e9));
|
|
696
|
+
}
|
|
697
|
+
async function formatPriceDisplay(lamports) {
|
|
698
|
+
const { solPrice } = await getSolPrice();
|
|
699
|
+
const sol = Number(lamports) / 1e9;
|
|
700
|
+
const usd = sol * solPrice;
|
|
701
|
+
return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
|
|
702
|
+
}
|
|
703
|
+
function formatPriceSync(lamports, solPrice) {
|
|
704
|
+
const sol = Number(lamports) / 1e9;
|
|
705
|
+
const usd = sol * solPrice;
|
|
706
|
+
return {
|
|
707
|
+
sol,
|
|
708
|
+
usd,
|
|
709
|
+
formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function clearPriceCache() {
|
|
713
|
+
priceCache = null;
|
|
714
|
+
}
|
|
715
|
+
function getProviders() {
|
|
716
|
+
return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
|
|
717
|
+
}
|
|
718
|
+
|
|
1083
719
|
exports.LocalSvmFacilitator = LocalSvmFacilitator;
|
|
1084
720
|
exports.addCredits = addCredits;
|
|
1085
|
-
exports.buildSolanaPayUrl = buildSolanaPayUrl;
|
|
1086
721
|
exports.clearPriceCache = clearPriceCache;
|
|
1087
722
|
exports.configurePricing = configurePricing;
|
|
1088
723
|
exports.createCreditSession = createCreditSession;
|
|
1089
|
-
exports.createPaymentFlow = createPaymentFlow;
|
|
1090
|
-
exports.createPaymentReference = createPaymentReference;
|
|
1091
|
-
exports.createX402AuthorizationHeader = createX402AuthorizationHeader;
|
|
1092
724
|
exports.executeAgentPayment = executeAgentPayment;
|
|
1093
725
|
exports.formatPriceDisplay = formatPriceDisplay;
|
|
1094
726
|
exports.formatPriceSync = formatPriceSync;
|
|
@@ -1101,14 +733,8 @@ exports.hasAgentSufficientBalance = hasAgentSufficientBalance;
|
|
|
1101
733
|
exports.keypairFromBase58 = keypairFromBase58;
|
|
1102
734
|
exports.lamportsToSol = lamportsToSol;
|
|
1103
735
|
exports.lamportsToUsd = lamportsToUsd;
|
|
1104
|
-
exports.sendSolanaPayment = sendSolanaPayment;
|
|
1105
736
|
exports.usdToLamports = usdToLamports;
|
|
1106
737
|
exports.useCredit = useCredit;
|
|
1107
|
-
exports.useFormatPrice = useFormatPrice;
|
|
1108
|
-
exports.useLamportsToUsd = useLamportsToUsd;
|
|
1109
|
-
exports.useMicropay = useMicropay;
|
|
1110
|
-
exports.usePaywallResource = usePaywallResource;
|
|
1111
|
-
exports.usePricing = usePricing;
|
|
1112
738
|
exports.validateCreditSession = validateCreditSession;
|
|
1113
739
|
Object.keys(core).forEach(function (k) {
|
|
1114
740
|
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
package/dist/index.d.cts
CHANGED
|
@@ -3,7 +3,6 @@ import { PaymentPayload, PaymentRequirements, VerifyResponse, SettleResponse, Su
|
|
|
3
3
|
export * from '@x402/core/types';
|
|
4
4
|
export * from '@x402/core/client';
|
|
5
5
|
export * from '@x402/svm';
|
|
6
|
-
export { PaymentFlowConfig, PaymentResult, PaymentStatus, PaywallConfig, PaywallState, SendPaymentParams, SolanaPayUrlParams, WalletAdapterInterface, buildSolanaPayUrl, createPaymentFlow, createPaymentReference, createX402AuthorizationHeader, sendSolanaPayment, useFormatPrice, useLamportsToUsd, useMicropay, usePaywallResource, usePricing } from './client/index.cjs';
|
|
7
6
|
export { AgentPaymentResult, CreditSessionClaims, CreditSessionConfig, CreditSessionData, CreditValidation, ExecuteAgentPaymentParams, UseCreditResult, addCredits, createCreditSession, executeAgentPayment, generateAgentKeypair, getAgentBalance, getRemainingCredits, hasAgentSufficientBalance, keypairFromBase58, useCredit, validateCreditSession } from './agent/index.cjs';
|
|
8
7
|
export { CustomPriceProvider, PriceConfig, PriceData, clearPriceCache, configurePricing, formatPriceDisplay, formatPriceSync, getProviders, getSolPrice, lamportsToSol, lamportsToUsd, usdToLamports } from './pricing/index.cjs';
|
|
9
8
|
import '@solana/web3.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { PaymentPayload, PaymentRequirements, VerifyResponse, SettleResponse, Su
|
|
|
3
3
|
export * from '@x402/core/types';
|
|
4
4
|
export * from '@x402/core/client';
|
|
5
5
|
export * from '@x402/svm';
|
|
6
|
-
export { PaymentFlowConfig, PaymentResult, PaymentStatus, PaywallConfig, PaywallState, SendPaymentParams, SolanaPayUrlParams, WalletAdapterInterface, buildSolanaPayUrl, createPaymentFlow, createPaymentReference, createX402AuthorizationHeader, sendSolanaPayment, useFormatPrice, useLamportsToUsd, useMicropay, usePaywallResource, usePricing } from './client/index.js';
|
|
7
6
|
export { AgentPaymentResult, CreditSessionClaims, CreditSessionConfig, CreditSessionData, CreditValidation, ExecuteAgentPaymentParams, UseCreditResult, addCredits, createCreditSession, executeAgentPayment, generateAgentKeypair, getAgentBalance, getRemainingCredits, hasAgentSufficientBalance, keypairFromBase58, useCredit, validateCreditSession } from './agent/index.js';
|
|
8
7
|
export { CustomPriceProvider, PriceConfig, PriceData, clearPriceCache, configurePricing, formatPriceDisplay, formatPriceSync, getProviders, getSolPrice, lamportsToSol, lamportsToUsd, usdToLamports } from './pricing/index.js';
|
|
9
8
|
import '@solana/web3.js';
|
package/dist/index.js
CHANGED
|
@@ -3,526 +3,11 @@ import { VerifyError, SettleError } from '@x402/core/types';
|
|
|
3
3
|
export * from '@x402/core/types';
|
|
4
4
|
export * from '@x402/core/client';
|
|
5
5
|
export * from '@x402/svm';
|
|
6
|
-
import {
|
|
7
|
-
import { PublicKey, Transaction, SystemProgram, LAMPORTS_PER_SOL, Connection, Keypair, TransactionMessage, VersionedTransaction, ComputeBudgetProgram } from '@solana/web3.js';
|
|
8
|
-
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
6
|
+
import { Connection, PublicKey, SystemProgram, LAMPORTS_PER_SOL, Keypair, TransactionMessage, VersionedTransaction, ComputeBudgetProgram } from '@solana/web3.js';
|
|
9
7
|
import bs58 from 'bs58';
|
|
10
8
|
import { SignJWT, jwtVerify } from 'jose';
|
|
11
9
|
|
|
12
10
|
// src/index.ts
|
|
13
|
-
|
|
14
|
-
// src/client/types.ts
|
|
15
|
-
var TOKEN_MINTS = {
|
|
16
|
-
/** USDC on mainnet */
|
|
17
|
-
USDC_MAINNET: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
18
|
-
/** USDC on devnet */
|
|
19
|
-
USDC_DEVNET: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
20
|
-
/** USDT on mainnet */
|
|
21
|
-
USDT_MAINNET: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// src/client/payment.ts
|
|
25
|
-
function buildSolanaPayUrl(params) {
|
|
26
|
-
const { recipient, amount, splToken, reference, label, message } = params;
|
|
27
|
-
const url = new URL(`solana:${recipient}`);
|
|
28
|
-
if (amount !== void 0) {
|
|
29
|
-
url.searchParams.set("amount", amount.toString());
|
|
30
|
-
}
|
|
31
|
-
if (splToken) {
|
|
32
|
-
url.searchParams.set("spl-token", splToken);
|
|
33
|
-
}
|
|
34
|
-
if (reference) {
|
|
35
|
-
url.searchParams.set("reference", reference);
|
|
36
|
-
}
|
|
37
|
-
if (label) {
|
|
38
|
-
url.searchParams.set("label", label);
|
|
39
|
-
}
|
|
40
|
-
if (message) {
|
|
41
|
-
url.searchParams.set("message", message);
|
|
42
|
-
}
|
|
43
|
-
return url.toString();
|
|
44
|
-
}
|
|
45
|
-
function createPaymentFlow(config) {
|
|
46
|
-
const { network, recipientWallet, amount, asset = "native", memo } = config;
|
|
47
|
-
let decimals = 9;
|
|
48
|
-
let mintAddress;
|
|
49
|
-
if (asset === "usdc") {
|
|
50
|
-
decimals = 6;
|
|
51
|
-
mintAddress = network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
52
|
-
} else if (asset === "usdt") {
|
|
53
|
-
decimals = 6;
|
|
54
|
-
mintAddress = TOKEN_MINTS.USDT_MAINNET;
|
|
55
|
-
} else if (typeof asset === "object" && "mint" in asset) {
|
|
56
|
-
decimals = asset.decimals ?? 6;
|
|
57
|
-
mintAddress = asset.mint;
|
|
58
|
-
}
|
|
59
|
-
const naturalAmount = Number(amount) / Math.pow(10, decimals);
|
|
60
|
-
return {
|
|
61
|
-
/** Get the payment configuration */
|
|
62
|
-
getConfig: () => ({ ...config }),
|
|
63
|
-
/** Get amount in natural display units (e.g., 0.01 SOL) */
|
|
64
|
-
getDisplayAmount: () => naturalAmount,
|
|
65
|
-
/** Get amount formatted with symbol */
|
|
66
|
-
getFormattedAmount: () => {
|
|
67
|
-
const symbol = asset === "native" ? "SOL" : asset === "usdc" ? "USDC" : asset === "usdt" ? "USDT" : "tokens";
|
|
68
|
-
return `${naturalAmount.toFixed(decimals > 6 ? 4 : 2)} ${symbol}`;
|
|
69
|
-
},
|
|
70
|
-
/** Generate Solana Pay URL for QR codes */
|
|
71
|
-
getSolanaPayUrl: (options = {}) => {
|
|
72
|
-
return buildSolanaPayUrl({
|
|
73
|
-
recipient: recipientWallet,
|
|
74
|
-
amount: naturalAmount,
|
|
75
|
-
splToken: mintAddress,
|
|
76
|
-
label: options.label,
|
|
77
|
-
reference: options.reference,
|
|
78
|
-
message: memo
|
|
79
|
-
});
|
|
80
|
-
},
|
|
81
|
-
/** Get the token mint address (undefined for native SOL) */
|
|
82
|
-
getMintAddress: () => mintAddress,
|
|
83
|
-
/** Check if this is a native SOL payment */
|
|
84
|
-
isNativePayment: () => asset === "native",
|
|
85
|
-
/** Get network information */
|
|
86
|
-
getNetworkInfo: () => ({
|
|
87
|
-
network,
|
|
88
|
-
isMainnet: network === "mainnet-beta",
|
|
89
|
-
explorerUrl: network === "mainnet-beta" ? "https://explorer.solana.com" : "https://explorer.solana.com?cluster=devnet"
|
|
90
|
-
}),
|
|
91
|
-
/** Build explorer URL for a transaction */
|
|
92
|
-
getExplorerUrl: (signature) => {
|
|
93
|
-
const baseUrl = "https://explorer.solana.com/tx";
|
|
94
|
-
const cluster = network === "mainnet-beta" ? "" : "?cluster=devnet";
|
|
95
|
-
return `${baseUrl}/${signature}${cluster}`;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function createPaymentReference() {
|
|
100
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
101
|
-
return crypto.randomUUID();
|
|
102
|
-
}
|
|
103
|
-
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
104
|
-
}
|
|
105
|
-
function createX402AuthorizationHeader(signature, paymentRequiredHeader) {
|
|
106
|
-
const cleanHeader = paymentRequiredHeader.replace(/^[Xx]402\s+/, "");
|
|
107
|
-
const required = decodePaymentRequiredHeader(cleanHeader);
|
|
108
|
-
const accepts = Array.isArray(required.accepts) ? required.accepts[0] : required.accepts;
|
|
109
|
-
const payload = {
|
|
110
|
-
accepted: accepts,
|
|
111
|
-
client: {
|
|
112
|
-
scheme: accepts.scheme,
|
|
113
|
-
// TypeScript knows this exists on PaymentRequirements
|
|
114
|
-
network: accepts.network
|
|
115
|
-
},
|
|
116
|
-
payment: {
|
|
117
|
-
signature
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
const token = encodePaymentSignatureHeader(payload);
|
|
121
|
-
return `x402 ${token}`;
|
|
122
|
-
}
|
|
123
|
-
async function sendSolanaPayment({
|
|
124
|
-
connection,
|
|
125
|
-
wallet,
|
|
126
|
-
recipientAddress,
|
|
127
|
-
amount,
|
|
128
|
-
// memo, // TODO: Add memo support to transaction
|
|
129
|
-
commitment = "confirmed"
|
|
130
|
-
}) {
|
|
131
|
-
if (!wallet.publicKey) {
|
|
132
|
-
throw new Error("Wallet not connected");
|
|
133
|
-
}
|
|
134
|
-
if (amount <= 0n) {
|
|
135
|
-
throw new Error("Amount must be greater than 0");
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const recipientPubkey = new PublicKey(recipientAddress);
|
|
139
|
-
const transaction = new Transaction().add(
|
|
140
|
-
SystemProgram.transfer({
|
|
141
|
-
fromPubkey: wallet.publicKey,
|
|
142
|
-
toPubkey: recipientPubkey,
|
|
143
|
-
lamports: amount
|
|
144
|
-
})
|
|
145
|
-
);
|
|
146
|
-
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(commitment);
|
|
147
|
-
transaction.recentBlockhash = blockhash;
|
|
148
|
-
transaction.feePayer = wallet.publicKey;
|
|
149
|
-
const signature = await wallet.sendTransaction(transaction, connection);
|
|
150
|
-
await connection.confirmTransaction({
|
|
151
|
-
signature,
|
|
152
|
-
blockhash,
|
|
153
|
-
lastValidBlockHeight
|
|
154
|
-
}, commitment);
|
|
155
|
-
return {
|
|
156
|
-
signature,
|
|
157
|
-
amountSol: Number(amount) / LAMPORTS_PER_SOL
|
|
158
|
-
};
|
|
159
|
-
} catch (error) {
|
|
160
|
-
console.error("Payment failed:", error);
|
|
161
|
-
throw new Error(error.message || "Payment failed");
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
function usePaywallResource({
|
|
165
|
-
url,
|
|
166
|
-
connection,
|
|
167
|
-
wallet
|
|
168
|
-
}) {
|
|
169
|
-
const [data, setData] = useState(null);
|
|
170
|
-
const [isLocked, setIsLocked] = useState(true);
|
|
171
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
172
|
-
const [error, setError] = useState(null);
|
|
173
|
-
const [paymentHeader, setPaymentHeader] = useState(null);
|
|
174
|
-
const [price, setPrice] = useState();
|
|
175
|
-
const [recipient, setRecipient] = useState();
|
|
176
|
-
const fetchData = useCallback(async (authHeader) => {
|
|
177
|
-
setIsLoading(true);
|
|
178
|
-
setError(null);
|
|
179
|
-
try {
|
|
180
|
-
const headers = {
|
|
181
|
-
"Content-Type": "application/json"
|
|
182
|
-
};
|
|
183
|
-
if (authHeader) {
|
|
184
|
-
headers["Authorization"] = authHeader;
|
|
185
|
-
}
|
|
186
|
-
const res = await fetch(url, {
|
|
187
|
-
headers,
|
|
188
|
-
credentials: "include"
|
|
189
|
-
// Ensure cookies are sent/received
|
|
190
|
-
});
|
|
191
|
-
if (res.status === 402) {
|
|
192
|
-
const wwwAuth = res.headers.get("WWW-Authenticate") || res.headers.get("Payment-Required");
|
|
193
|
-
const errorReason = res.headers.get("X-Payment-Error");
|
|
194
|
-
console.log("[usePaywallResource] 402 Response. Header:", wwwAuth, "Error:", errorReason);
|
|
195
|
-
if (authHeader && errorReason) {
|
|
196
|
-
console.error("[usePaywallResource] Verification Failed:", errorReason);
|
|
197
|
-
setError(`Verification Failed: ${errorReason}`);
|
|
198
|
-
}
|
|
199
|
-
if (wwwAuth) {
|
|
200
|
-
setPaymentHeader(wwwAuth);
|
|
201
|
-
try {
|
|
202
|
-
const { decodePaymentRequiredHeader: decodePaymentRequiredHeader2 } = await import('@x402/core/http');
|
|
203
|
-
const cleanHeader = wwwAuth.replace(/^[Xx]402\s+/, "");
|
|
204
|
-
const decoded = decodePaymentRequiredHeader2(cleanHeader);
|
|
205
|
-
console.log("[usePaywallResource] Decoded header:", decoded);
|
|
206
|
-
const accepts = Array.isArray(decoded.accepts) ? decoded.accepts[0] : decoded.accepts;
|
|
207
|
-
console.log("[usePaywallResource] Accepts:", accepts);
|
|
208
|
-
if (accepts) {
|
|
209
|
-
const amountStr = accepts.amount || accepts.price || accepts.maxAmountRequired || "0";
|
|
210
|
-
setPrice(BigInt(amountStr));
|
|
211
|
-
setRecipient(accepts.payTo);
|
|
212
|
-
console.log("[usePaywallResource] Set price:", amountStr, "recipient:", accepts.payTo);
|
|
213
|
-
} else {
|
|
214
|
-
console.warn("[usePaywallResource] No accepts found in header");
|
|
215
|
-
}
|
|
216
|
-
} catch (e) {
|
|
217
|
-
console.warn("[usePaywallResource] Failed to parse x402 header:", e);
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
console.warn("[usePaywallResource] 402 response missing WWW-Authenticate header");
|
|
221
|
-
}
|
|
222
|
-
setIsLocked(true);
|
|
223
|
-
setData(null);
|
|
224
|
-
} else if (res.ok) {
|
|
225
|
-
const json = await res.json();
|
|
226
|
-
setData(json);
|
|
227
|
-
setIsLocked(false);
|
|
228
|
-
} else {
|
|
229
|
-
throw new Error(`Request failed with status ${res.status}`);
|
|
230
|
-
}
|
|
231
|
-
} catch (err) {
|
|
232
|
-
setError(err.message || "Failed to fetch resource");
|
|
233
|
-
} finally {
|
|
234
|
-
setIsLoading(false);
|
|
235
|
-
}
|
|
236
|
-
}, [url]);
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
fetchData();
|
|
239
|
-
}, [fetchData]);
|
|
240
|
-
const unlock = useCallback(async () => {
|
|
241
|
-
if (!paymentHeader || !price || !recipient) {
|
|
242
|
-
setError("Missing payment requirements");
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
setIsLoading(true);
|
|
246
|
-
try {
|
|
247
|
-
const { signature } = await sendSolanaPayment({
|
|
248
|
-
connection,
|
|
249
|
-
wallet,
|
|
250
|
-
recipientAddress: recipient,
|
|
251
|
-
amount: price
|
|
252
|
-
});
|
|
253
|
-
const authHeader = createX402AuthorizationHeader(signature, paymentHeader);
|
|
254
|
-
await fetchData(authHeader);
|
|
255
|
-
} catch (err) {
|
|
256
|
-
console.error("Unlock failed", err);
|
|
257
|
-
setError(err.message || "Payment/Unlock failed");
|
|
258
|
-
setIsLoading(false);
|
|
259
|
-
}
|
|
260
|
-
}, [connection, wallet, paymentHeader, price, recipient, fetchData]);
|
|
261
|
-
return {
|
|
262
|
-
data,
|
|
263
|
-
isLocked,
|
|
264
|
-
isLoading,
|
|
265
|
-
price,
|
|
266
|
-
recipient,
|
|
267
|
-
error,
|
|
268
|
-
unlock
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// src/pricing/utils.ts
|
|
273
|
-
function lamportsToSol(lamports) {
|
|
274
|
-
return Number(lamports) / 1e9;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// src/pricing/index.ts
|
|
278
|
-
var priceCache = null;
|
|
279
|
-
var currentConfig = {};
|
|
280
|
-
function configurePricing(newConfig) {
|
|
281
|
-
currentConfig = { ...currentConfig, ...newConfig };
|
|
282
|
-
}
|
|
283
|
-
var PROVIDERS = [
|
|
284
|
-
{
|
|
285
|
-
name: "coincap",
|
|
286
|
-
url: "https://api.coincap.io/v2/assets/solana",
|
|
287
|
-
parse: (data) => parseFloat(data.data?.priceUsd || "0")
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
name: "binance",
|
|
291
|
-
url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
|
|
292
|
-
parse: (data) => parseFloat(data.price || "0")
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
name: "coingecko",
|
|
296
|
-
url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
|
|
297
|
-
parse: (data) => data.solana?.usd || 0
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
name: "kraken",
|
|
301
|
-
url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
|
|
302
|
-
parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
|
|
303
|
-
}
|
|
304
|
-
];
|
|
305
|
-
async function fetchFromProvider(provider, timeout) {
|
|
306
|
-
const controller = new AbortController();
|
|
307
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
308
|
-
try {
|
|
309
|
-
const response = await fetch(provider.url, {
|
|
310
|
-
headers: { "Accept": "application/json" },
|
|
311
|
-
signal: controller.signal
|
|
312
|
-
});
|
|
313
|
-
if (!response.ok) {
|
|
314
|
-
throw new Error(`HTTP ${response.status}`);
|
|
315
|
-
}
|
|
316
|
-
const data = await response.json();
|
|
317
|
-
const price = provider.parse(data);
|
|
318
|
-
if (!price || price <= 0) {
|
|
319
|
-
throw new Error("Invalid price");
|
|
320
|
-
}
|
|
321
|
-
return { price, source: provider.name };
|
|
322
|
-
} finally {
|
|
323
|
-
clearTimeout(timeoutId);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
async function fetchPriceParallel(timeout) {
|
|
327
|
-
const promises = PROVIDERS.map(
|
|
328
|
-
(provider) => fetchFromProvider(provider, timeout).catch(() => null)
|
|
329
|
-
);
|
|
330
|
-
const results = await Promise.all(promises);
|
|
331
|
-
const validResult = results.find((r) => r !== null);
|
|
332
|
-
if (validResult) {
|
|
333
|
-
return validResult;
|
|
334
|
-
}
|
|
335
|
-
throw new Error("All providers failed");
|
|
336
|
-
}
|
|
337
|
-
async function fetchPriceSequential(timeout) {
|
|
338
|
-
for (const provider of PROVIDERS) {
|
|
339
|
-
try {
|
|
340
|
-
return await fetchFromProvider(provider, timeout);
|
|
341
|
-
} catch {
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
throw new Error("All providers failed");
|
|
346
|
-
}
|
|
347
|
-
async function getSolPrice() {
|
|
348
|
-
const cacheTTL = currentConfig.cacheTTL ?? 6e4;
|
|
349
|
-
const timeout = currentConfig.timeout ?? 3e3;
|
|
350
|
-
const useParallel = currentConfig.parallelFetch ?? true;
|
|
351
|
-
const now = Date.now();
|
|
352
|
-
if (priceCache && now - priceCache.timestamp < cacheTTL) {
|
|
353
|
-
return priceCache.data;
|
|
354
|
-
}
|
|
355
|
-
if (currentConfig.customProvider) {
|
|
356
|
-
try {
|
|
357
|
-
const price = await currentConfig.customProvider();
|
|
358
|
-
if (price > 0) {
|
|
359
|
-
const data = {
|
|
360
|
-
solPrice: price,
|
|
361
|
-
fetchedAt: /* @__PURE__ */ new Date(),
|
|
362
|
-
source: "custom"
|
|
363
|
-
};
|
|
364
|
-
priceCache = { data, timestamp: now };
|
|
365
|
-
return data;
|
|
366
|
-
}
|
|
367
|
-
} catch {
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
try {
|
|
371
|
-
const result = useParallel ? await fetchPriceParallel(timeout) : await fetchPriceSequential(timeout);
|
|
372
|
-
const data = {
|
|
373
|
-
solPrice: result.price,
|
|
374
|
-
fetchedAt: /* @__PURE__ */ new Date(),
|
|
375
|
-
source: result.source
|
|
376
|
-
};
|
|
377
|
-
priceCache = { data, timestamp: now };
|
|
378
|
-
return data;
|
|
379
|
-
} catch {
|
|
380
|
-
if (priceCache) {
|
|
381
|
-
return {
|
|
382
|
-
...priceCache.data,
|
|
383
|
-
source: `${priceCache.data.source} (stale)`
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
throw new Error(
|
|
387
|
-
"Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
async function lamportsToUsd(lamports) {
|
|
392
|
-
const { solPrice } = await getSolPrice();
|
|
393
|
-
const sol = Number(lamports) / 1e9;
|
|
394
|
-
return sol * solPrice;
|
|
395
|
-
}
|
|
396
|
-
async function usdToLamports(usd) {
|
|
397
|
-
const { solPrice } = await getSolPrice();
|
|
398
|
-
const sol = usd / solPrice;
|
|
399
|
-
return BigInt(Math.floor(sol * 1e9));
|
|
400
|
-
}
|
|
401
|
-
async function formatPriceDisplay(lamports) {
|
|
402
|
-
const { solPrice } = await getSolPrice();
|
|
403
|
-
const sol = Number(lamports) / 1e9;
|
|
404
|
-
const usd = sol * solPrice;
|
|
405
|
-
return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
|
|
406
|
-
}
|
|
407
|
-
function formatPriceSync(lamports, solPrice) {
|
|
408
|
-
const sol = Number(lamports) / 1e9;
|
|
409
|
-
const usd = sol * solPrice;
|
|
410
|
-
return {
|
|
411
|
-
sol,
|
|
412
|
-
usd,
|
|
413
|
-
formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
function clearPriceCache() {
|
|
417
|
-
priceCache = null;
|
|
418
|
-
}
|
|
419
|
-
function getProviders() {
|
|
420
|
-
return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// src/client/hooks.ts
|
|
424
|
-
function usePricing(refreshIntervalMs = 6e4) {
|
|
425
|
-
const [priceData, setPriceData] = useState(null);
|
|
426
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
427
|
-
const [error, setError] = useState(null);
|
|
428
|
-
const intervalRef = useRef(null);
|
|
429
|
-
const fetchPrice = useCallback(async () => {
|
|
430
|
-
try {
|
|
431
|
-
setIsLoading(true);
|
|
432
|
-
setError(null);
|
|
433
|
-
const data = await getSolPrice();
|
|
434
|
-
setPriceData(data);
|
|
435
|
-
} catch (err) {
|
|
436
|
-
setError(err instanceof Error ? err.message : "Failed to fetch price");
|
|
437
|
-
} finally {
|
|
438
|
-
setIsLoading(false);
|
|
439
|
-
}
|
|
440
|
-
}, []);
|
|
441
|
-
useEffect(() => {
|
|
442
|
-
fetchPrice();
|
|
443
|
-
if (refreshIntervalMs > 0) {
|
|
444
|
-
intervalRef.current = setInterval(fetchPrice, refreshIntervalMs);
|
|
445
|
-
}
|
|
446
|
-
return () => {
|
|
447
|
-
if (intervalRef.current) {
|
|
448
|
-
clearInterval(intervalRef.current);
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
}, [fetchPrice, refreshIntervalMs]);
|
|
452
|
-
return {
|
|
453
|
-
solPrice: priceData?.solPrice ?? null,
|
|
454
|
-
source: priceData?.source ?? null,
|
|
455
|
-
fetchedAt: priceData?.fetchedAt ?? null,
|
|
456
|
-
isLoading,
|
|
457
|
-
error,
|
|
458
|
-
refresh: fetchPrice
|
|
459
|
-
};
|
|
460
|
-
}
|
|
461
|
-
function useLamportsToUsd(lamports) {
|
|
462
|
-
const { solPrice, isLoading } = usePricing();
|
|
463
|
-
if (isLoading || !solPrice || lamports === null) {
|
|
464
|
-
return { usd: null, formatted: null, isLoading };
|
|
465
|
-
}
|
|
466
|
-
const lamportsBigInt = typeof lamports === "number" ? BigInt(lamports) : lamports;
|
|
467
|
-
const sol = Number(lamportsBigInt) / 1e9;
|
|
468
|
-
const usd = sol * solPrice;
|
|
469
|
-
return {
|
|
470
|
-
usd,
|
|
471
|
-
formatted: `$${usd.toFixed(2)}`,
|
|
472
|
-
isLoading: false
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
function useMicropay() {
|
|
476
|
-
const [status, setStatus] = useState("idle");
|
|
477
|
-
const [error, setError] = useState(null);
|
|
478
|
-
const [signature, setSignature] = useState(null);
|
|
479
|
-
const reset = useCallback(() => {
|
|
480
|
-
setStatus("idle");
|
|
481
|
-
setError(null);
|
|
482
|
-
setSignature(null);
|
|
483
|
-
}, []);
|
|
484
|
-
const pay = useCallback(async (_options) => {
|
|
485
|
-
setStatus("pending");
|
|
486
|
-
setError(null);
|
|
487
|
-
try {
|
|
488
|
-
throw new Error(
|
|
489
|
-
"useMicropay requires implementation of onSign/onSend callbacks. See documentation for wallet adapter integration."
|
|
490
|
-
);
|
|
491
|
-
} catch (err) {
|
|
492
|
-
const errorMessage = err instanceof Error ? err.message : "Payment failed";
|
|
493
|
-
setError(errorMessage);
|
|
494
|
-
setStatus("error");
|
|
495
|
-
return { success: false, error: errorMessage };
|
|
496
|
-
}
|
|
497
|
-
}, []);
|
|
498
|
-
return {
|
|
499
|
-
status,
|
|
500
|
-
error,
|
|
501
|
-
signature,
|
|
502
|
-
pay,
|
|
503
|
-
reset
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
function useFormatPrice(lamports) {
|
|
507
|
-
const { solPrice, isLoading } = usePricing();
|
|
508
|
-
if (isLoading || !solPrice || lamports === null) {
|
|
509
|
-
return {
|
|
510
|
-
sol: null,
|
|
511
|
-
usd: null,
|
|
512
|
-
formatted: "Loading...",
|
|
513
|
-
isLoading
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
const lamportsBigInt = typeof lamports === "number" ? BigInt(lamports) : lamports;
|
|
517
|
-
const sol = Number(lamportsBigInt) / 1e9;
|
|
518
|
-
const usd = sol * solPrice;
|
|
519
|
-
return {
|
|
520
|
-
sol,
|
|
521
|
-
usd,
|
|
522
|
-
formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`,
|
|
523
|
-
isLoading: false
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
11
|
var LocalSvmFacilitator = class {
|
|
527
12
|
scheme = "exact";
|
|
528
13
|
caipFamily = "solana:*";
|
|
@@ -1075,4 +560,155 @@ async function getRemainingCredits(token, secret) {
|
|
|
1075
560
|
};
|
|
1076
561
|
}
|
|
1077
562
|
|
|
1078
|
-
|
|
563
|
+
// src/pricing/utils.ts
|
|
564
|
+
function lamportsToSol(lamports) {
|
|
565
|
+
return Number(lamports) / 1e9;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/pricing/index.ts
|
|
569
|
+
var priceCache = null;
|
|
570
|
+
var currentConfig = {};
|
|
571
|
+
function configurePricing(newConfig) {
|
|
572
|
+
currentConfig = { ...currentConfig, ...newConfig };
|
|
573
|
+
}
|
|
574
|
+
var PROVIDERS = [
|
|
575
|
+
{
|
|
576
|
+
name: "coincap",
|
|
577
|
+
url: "https://api.coincap.io/v2/assets/solana",
|
|
578
|
+
parse: (data) => parseFloat(data.data?.priceUsd || "0")
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "binance",
|
|
582
|
+
url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
|
|
583
|
+
parse: (data) => parseFloat(data.price || "0")
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: "coingecko",
|
|
587
|
+
url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
|
|
588
|
+
parse: (data) => data.solana?.usd || 0
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: "kraken",
|
|
592
|
+
url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
|
|
593
|
+
parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
|
|
594
|
+
}
|
|
595
|
+
];
|
|
596
|
+
async function fetchFromProvider(provider, timeout) {
|
|
597
|
+
const controller = new AbortController();
|
|
598
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
599
|
+
try {
|
|
600
|
+
const response = await fetch(provider.url, {
|
|
601
|
+
headers: { "Accept": "application/json" },
|
|
602
|
+
signal: controller.signal
|
|
603
|
+
});
|
|
604
|
+
if (!response.ok) {
|
|
605
|
+
throw new Error(`HTTP ${response.status}`);
|
|
606
|
+
}
|
|
607
|
+
const data = await response.json();
|
|
608
|
+
const price = provider.parse(data);
|
|
609
|
+
if (!price || price <= 0) {
|
|
610
|
+
throw new Error("Invalid price");
|
|
611
|
+
}
|
|
612
|
+
return { price, source: provider.name };
|
|
613
|
+
} finally {
|
|
614
|
+
clearTimeout(timeoutId);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async function fetchPriceParallel(timeout) {
|
|
618
|
+
const promises = PROVIDERS.map(
|
|
619
|
+
(provider) => fetchFromProvider(provider, timeout).catch(() => null)
|
|
620
|
+
);
|
|
621
|
+
const results = await Promise.all(promises);
|
|
622
|
+
const validResult = results.find((r) => r !== null);
|
|
623
|
+
if (validResult) {
|
|
624
|
+
return validResult;
|
|
625
|
+
}
|
|
626
|
+
throw new Error("All providers failed");
|
|
627
|
+
}
|
|
628
|
+
async function fetchPriceSequential(timeout) {
|
|
629
|
+
for (const provider of PROVIDERS) {
|
|
630
|
+
try {
|
|
631
|
+
return await fetchFromProvider(provider, timeout);
|
|
632
|
+
} catch {
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
throw new Error("All providers failed");
|
|
637
|
+
}
|
|
638
|
+
async function getSolPrice() {
|
|
639
|
+
const cacheTTL = currentConfig.cacheTTL ?? 6e4;
|
|
640
|
+
const timeout = currentConfig.timeout ?? 3e3;
|
|
641
|
+
const useParallel = currentConfig.parallelFetch ?? true;
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
if (priceCache && now - priceCache.timestamp < cacheTTL) {
|
|
644
|
+
return priceCache.data;
|
|
645
|
+
}
|
|
646
|
+
if (currentConfig.customProvider) {
|
|
647
|
+
try {
|
|
648
|
+
const price = await currentConfig.customProvider();
|
|
649
|
+
if (price > 0) {
|
|
650
|
+
const data = {
|
|
651
|
+
solPrice: price,
|
|
652
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
653
|
+
source: "custom"
|
|
654
|
+
};
|
|
655
|
+
priceCache = { data, timestamp: now };
|
|
656
|
+
return data;
|
|
657
|
+
}
|
|
658
|
+
} catch {
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
const result = useParallel ? await fetchPriceParallel(timeout) : await fetchPriceSequential(timeout);
|
|
663
|
+
const data = {
|
|
664
|
+
solPrice: result.price,
|
|
665
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
666
|
+
source: result.source
|
|
667
|
+
};
|
|
668
|
+
priceCache = { data, timestamp: now };
|
|
669
|
+
return data;
|
|
670
|
+
} catch {
|
|
671
|
+
if (priceCache) {
|
|
672
|
+
return {
|
|
673
|
+
...priceCache.data,
|
|
674
|
+
source: `${priceCache.data.source} (stale)`
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
throw new Error(
|
|
678
|
+
"Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function lamportsToUsd(lamports) {
|
|
683
|
+
const { solPrice } = await getSolPrice();
|
|
684
|
+
const sol = Number(lamports) / 1e9;
|
|
685
|
+
return sol * solPrice;
|
|
686
|
+
}
|
|
687
|
+
async function usdToLamports(usd) {
|
|
688
|
+
const { solPrice } = await getSolPrice();
|
|
689
|
+
const sol = usd / solPrice;
|
|
690
|
+
return BigInt(Math.floor(sol * 1e9));
|
|
691
|
+
}
|
|
692
|
+
async function formatPriceDisplay(lamports) {
|
|
693
|
+
const { solPrice } = await getSolPrice();
|
|
694
|
+
const sol = Number(lamports) / 1e9;
|
|
695
|
+
const usd = sol * solPrice;
|
|
696
|
+
return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
|
|
697
|
+
}
|
|
698
|
+
function formatPriceSync(lamports, solPrice) {
|
|
699
|
+
const sol = Number(lamports) / 1e9;
|
|
700
|
+
const usd = sol * solPrice;
|
|
701
|
+
return {
|
|
702
|
+
sol,
|
|
703
|
+
usd,
|
|
704
|
+
formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function clearPriceCache() {
|
|
708
|
+
priceCache = null;
|
|
709
|
+
}
|
|
710
|
+
function getProviders() {
|
|
711
|
+
return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export { LocalSvmFacilitator, addCredits, clearPriceCache, configurePricing, createCreditSession, executeAgentPayment, formatPriceDisplay, formatPriceSync, generateAgentKeypair, getAgentBalance, getProviders, getRemainingCredits, getSolPrice, hasAgentSufficientBalance, keypairFromBase58, lamportsToSol, lamportsToUsd, usdToLamports, useCredit, validateCreditSession };
|
package/package.json
CHANGED