@alleyboss/micropay-solana-x402-paywall 3.3.7 → 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 +341 -504
- package/dist/index.d.cts +93 -1
- package/dist/index.d.ts +93 -1
- package/dist/index.js +343 -496
- 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,216 @@ 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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
}
|
|
16
|
+
var LocalSvmFacilitator = class {
|
|
17
|
+
scheme = "exact";
|
|
18
|
+
caipFamily = "solana:*";
|
|
19
|
+
connection;
|
|
20
|
+
constructor(rpcUrl) {
|
|
21
|
+
console.log("[LocalSvmFacilitator] Initialized with RPC:", rpcUrl);
|
|
22
|
+
this.connection = new web3_js.Connection(rpcUrl, "confirmed");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get supported payment kinds
|
|
26
|
+
* Mocking the response of the /supported endpoint
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Network Constants - CAIP-2 format for x402 v2
|
|
30
|
+
* These match the official Solana chain IDs used by x402 protocol
|
|
31
|
+
*/
|
|
32
|
+
NETWORKS = {
|
|
33
|
+
DEVNET: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
34
|
+
MAINNET: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
|
|
102
35
|
};
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
blockhash,
|
|
158
|
-
lastValidBlockHeight
|
|
159
|
-
}, commitment);
|
|
160
|
-
return {
|
|
161
|
-
signature,
|
|
162
|
-
amountSol: Number(amount) / web3_js.LAMPORTS_PER_SOL
|
|
36
|
+
/**
|
|
37
|
+
* DUMMY_FEE_PAYER Explanation (for auditors):
|
|
38
|
+
*
|
|
39
|
+
* In a HOSTED facilitator (like x402.org), the `feePayer` field in the
|
|
40
|
+
* SupportedResponse.extra specifies which address will pay transaction fees
|
|
41
|
+
* when the facilitator submits transactions on behalf of users.
|
|
42
|
+
*
|
|
43
|
+
* In LOCAL/SELF-SOVEREIGN mode (this implementation):
|
|
44
|
+
* - The USER pays their own transaction fees directly
|
|
45
|
+
* - The facilitator NEVER submits transactions - it only VERIFIES them
|
|
46
|
+
* - Therefore, no fee payer address is actually used
|
|
47
|
+
*
|
|
48
|
+
* However, the x402 protocol REQUIRES this field in the response schema.
|
|
49
|
+
* We use the System Program address (all 1s) as a placeholder because:
|
|
50
|
+
* 1. It's clearly not a real wallet (obvious placeholder)
|
|
51
|
+
* 2. It cannot receive funds or sign transactions
|
|
52
|
+
* 3. It signals to developers that fee paying is handled differently
|
|
53
|
+
*
|
|
54
|
+
* SECURITY NOTE: This is NOT a security risk because:
|
|
55
|
+
* - The fee payer is never used in verify() or settle() methods
|
|
56
|
+
* - Users sign and pay for their own transactions
|
|
57
|
+
* - The address is just a protocol-required placeholder
|
|
58
|
+
*
|
|
59
|
+
* @see https://docs.x402.org for protocol specification
|
|
60
|
+
*/
|
|
61
|
+
DUMMY_FEE_PAYER = "11111111111111111111111111111111";
|
|
62
|
+
/**
|
|
63
|
+
* Get supported payment kinds
|
|
64
|
+
* Returns x402 v2 compatible response with CAIP-2 network identifiers
|
|
65
|
+
*
|
|
66
|
+
* NOTE: The feePayer in extra.feePayer is a placeholder only.
|
|
67
|
+
* In self-sovereign mode, users pay their own transaction fees.
|
|
68
|
+
*/
|
|
69
|
+
async getSupported(_extensionKeys = []) {
|
|
70
|
+
const supported = {
|
|
71
|
+
kinds: [
|
|
72
|
+
{
|
|
73
|
+
x402Version: 2,
|
|
74
|
+
scheme: "exact",
|
|
75
|
+
network: this.NETWORKS.DEVNET,
|
|
76
|
+
extra: { feePayer: this.DUMMY_FEE_PAYER }
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
x402Version: 2,
|
|
80
|
+
scheme: "exact",
|
|
81
|
+
network: this.NETWORKS.MAINNET,
|
|
82
|
+
extra: { feePayer: this.DUMMY_FEE_PAYER }
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
extensions: [],
|
|
86
|
+
signers: {
|
|
87
|
+
// Placeholder - in self-sovereign mode, users are their own signers
|
|
88
|
+
"solana:*": [this.DUMMY_FEE_PAYER]
|
|
89
|
+
}
|
|
163
90
|
};
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
91
|
+
return supported;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get mechanism-specific extra data
|
|
95
|
+
*/
|
|
96
|
+
getExtra(_network) {
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get default signers (not used for local verification usually, but required by interface)
|
|
101
|
+
*/
|
|
102
|
+
getSigners(_network) {
|
|
103
|
+
return [];
|
|
167
104
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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);
|
|
105
|
+
/**
|
|
106
|
+
* Enable debug logging (disable in production)
|
|
107
|
+
*/
|
|
108
|
+
debug = process.env.NODE_ENV === "development";
|
|
109
|
+
/**
|
|
110
|
+
* Verify a payment on-chain
|
|
111
|
+
*/
|
|
112
|
+
async verify(payload, requirements) {
|
|
184
113
|
try {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (authHeader) {
|
|
189
|
-
headers["Authorization"] = authHeader;
|
|
114
|
+
const signature = payload.payload.signature;
|
|
115
|
+
if (!signature) {
|
|
116
|
+
return { isValid: false, invalidReason: "Missing signature in payment payload" };
|
|
190
117
|
}
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
118
|
+
const payTo = requirements.payTo;
|
|
119
|
+
const amountVal = requirements.amount || requirements.maxAmountRequired || "0";
|
|
120
|
+
const requiredAmount = BigInt(amountVal);
|
|
121
|
+
if (this.debug) {
|
|
122
|
+
console.log(`[LocalSvmFacilitator] Verifying tx: ${signature.slice(0, 8)}...`);
|
|
123
|
+
}
|
|
124
|
+
const tx = await this.fetchTransactionWithRetry(signature, 3);
|
|
125
|
+
if (!tx) {
|
|
126
|
+
return { isValid: false, invalidReason: "Transaction not found or not confirmed" };
|
|
127
|
+
}
|
|
128
|
+
const instructions = tx.transaction.message.instructions;
|
|
129
|
+
let paidAmount = 0n;
|
|
130
|
+
let payer = void 0;
|
|
131
|
+
for (const ix of instructions) {
|
|
132
|
+
if ("program" in ix && ix.program === "system") {
|
|
133
|
+
const parsed = ix.parsed;
|
|
134
|
+
if (parsed?.type === "transfer" && parsed.info?.destination === payTo) {
|
|
135
|
+
paidAmount += BigInt(parsed.info.lamports);
|
|
136
|
+
if (!payer) payer = parsed.info.source;
|
|
137
|
+
}
|
|
203
138
|
}
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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");
|
|
139
|
+
if ("program" in ix && (ix.program === "spl-token" || ix.program === "spl-token-2022")) {
|
|
140
|
+
const parsed = ix.parsed;
|
|
141
|
+
if (parsed?.type === "transferChecked" || parsed?.type === "transfer") {
|
|
142
|
+
if (this.debug) {
|
|
143
|
+
console.log(`[LocalSvmFacilitator] Found SPL transfer`);
|
|
220
144
|
}
|
|
221
|
-
} catch (e) {
|
|
222
|
-
console.warn("[usePaywallResource] Failed to parse x402 header:", e);
|
|
223
145
|
}
|
|
224
|
-
} else {
|
|
225
|
-
console.warn("[usePaywallResource] 402 response missing WWW-Authenticate header");
|
|
226
146
|
}
|
|
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
147
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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"
|
|
148
|
+
if (paidAmount >= requiredAmount) {
|
|
149
|
+
if (this.debug) {
|
|
150
|
+
console.log(`[LocalSvmFacilitator] Verification SUCCESS for tx: ${signature.slice(0, 8)}...`);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
isValid: true,
|
|
154
|
+
payer: payer || tx.transaction.message.accountKeys[0].pubkey.toBase58()
|
|
368
155
|
};
|
|
369
|
-
priceCache = { data, timestamp: now };
|
|
370
|
-
return data;
|
|
371
156
|
}
|
|
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
157
|
return {
|
|
387
|
-
|
|
388
|
-
|
|
158
|
+
isValid: false,
|
|
159
|
+
invalidReason: "Insufficient payment amount",
|
|
160
|
+
payer
|
|
389
161
|
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
164
|
+
if (this.debug) {
|
|
165
|
+
console.error("[LocalSvmFacilitator] Verify error:", errorMessage);
|
|
166
|
+
}
|
|
167
|
+
throw new types.VerifyError(500, {
|
|
168
|
+
isValid: false,
|
|
169
|
+
invalidReason: errorMessage
|
|
170
|
+
});
|
|
390
171
|
}
|
|
391
|
-
throw new Error(
|
|
392
|
-
"Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
|
|
393
|
-
);
|
|
394
172
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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);
|
|
173
|
+
/**
|
|
174
|
+
* Fetch transaction with exponential backoff retry
|
|
175
|
+
*/
|
|
176
|
+
async fetchTransactionWithRetry(signature, maxRetries = 3) {
|
|
177
|
+
let lastError;
|
|
178
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
179
|
+
try {
|
|
180
|
+
const tx = await this.connection.getParsedTransaction(signature, {
|
|
181
|
+
maxSupportedTransactionVersion: 0,
|
|
182
|
+
commitment: "confirmed"
|
|
183
|
+
});
|
|
184
|
+
if (tx) return tx;
|
|
185
|
+
if (attempt < maxRetries - 1) {
|
|
186
|
+
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
lastError = error instanceof Error ? error : new Error("RPC error");
|
|
190
|
+
if (attempt < maxRetries - 1) {
|
|
191
|
+
await this.sleep(Math.pow(2, attempt) * 1e3);
|
|
192
|
+
}
|
|
454
193
|
}
|
|
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
194
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
195
|
+
if (lastError) throw lastError;
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Sleep helper
|
|
200
|
+
*/
|
|
201
|
+
sleep(ms) {
|
|
202
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Settle a payment (not applicable for direct chain verification, usually)
|
|
206
|
+
* But we must implement it. For 'exact', settlement is just verification + finality.
|
|
207
|
+
*/
|
|
208
|
+
async settle(payload, requirements) {
|
|
209
|
+
const verifyResult = await this.verify(payload, requirements);
|
|
210
|
+
if (!verifyResult.isValid) {
|
|
211
|
+
throw new types.SettleError(400, {
|
|
212
|
+
success: false,
|
|
213
|
+
errorReason: verifyResult.invalidReason || "Verification failed",
|
|
214
|
+
transaction: payload.payload.signature,
|
|
215
|
+
network: requirements.network
|
|
216
|
+
});
|
|
217
|
+
}
|
|
514
218
|
return {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
219
|
+
success: true,
|
|
220
|
+
payer: verifyResult.payer,
|
|
221
|
+
transaction: payload.payload.signature,
|
|
222
|
+
network: requirements.network
|
|
519
223
|
};
|
|
520
224
|
}
|
|
521
|
-
|
|
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
|
-
}
|
|
225
|
+
};
|
|
531
226
|
var DEFAULT_COMPUTE_UNITS = 2e5;
|
|
532
227
|
var DEFAULT_MICRO_LAMPORTS = 1e3;
|
|
533
228
|
function createPriorityFeeInstructions(config = {}) {
|
|
@@ -870,14 +565,162 @@ async function getRemainingCredits(token, secret) {
|
|
|
870
565
|
};
|
|
871
566
|
}
|
|
872
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
|
+
|
|
719
|
+
exports.LocalSvmFacilitator = LocalSvmFacilitator;
|
|
873
720
|
exports.addCredits = addCredits;
|
|
874
|
-
exports.buildSolanaPayUrl = buildSolanaPayUrl;
|
|
875
721
|
exports.clearPriceCache = clearPriceCache;
|
|
876
722
|
exports.configurePricing = configurePricing;
|
|
877
723
|
exports.createCreditSession = createCreditSession;
|
|
878
|
-
exports.createPaymentFlow = createPaymentFlow;
|
|
879
|
-
exports.createPaymentReference = createPaymentReference;
|
|
880
|
-
exports.createX402AuthorizationHeader = createX402AuthorizationHeader;
|
|
881
724
|
exports.executeAgentPayment = executeAgentPayment;
|
|
882
725
|
exports.formatPriceDisplay = formatPriceDisplay;
|
|
883
726
|
exports.formatPriceSync = formatPriceSync;
|
|
@@ -890,14 +733,8 @@ exports.hasAgentSufficientBalance = hasAgentSufficientBalance;
|
|
|
890
733
|
exports.keypairFromBase58 = keypairFromBase58;
|
|
891
734
|
exports.lamportsToSol = lamportsToSol;
|
|
892
735
|
exports.lamportsToUsd = lamportsToUsd;
|
|
893
|
-
exports.sendSolanaPayment = sendSolanaPayment;
|
|
894
736
|
exports.usdToLamports = usdToLamports;
|
|
895
737
|
exports.useCredit = useCredit;
|
|
896
|
-
exports.useFormatPrice = useFormatPrice;
|
|
897
|
-
exports.useLamportsToUsd = useLamportsToUsd;
|
|
898
|
-
exports.useMicropay = useMicropay;
|
|
899
|
-
exports.usePaywallResource = usePaywallResource;
|
|
900
|
-
exports.usePricing = usePricing;
|
|
901
738
|
exports.validateCreditSession = validateCreditSession;
|
|
902
739
|
Object.keys(core).forEach(function (k) {
|
|
903
740
|
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|