@alleyboss/micropay-solana-x402-paywall 2.3.1 → 3.0.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 +64 -142
- package/dist/agent/index.cjs +1 -2
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +11 -2
- package/dist/agent/index.d.ts +11 -2
- package/dist/agent/index.js +1 -2
- package/dist/agent/index.js.map +1 -1
- package/dist/client/index.cjs +1 -1
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +10 -1
- package/dist/client/index.d.ts +10 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/express/index.cjs +79 -0
- package/dist/express/index.cjs.map +1 -0
- package/dist/express/index.d.cts +40 -0
- package/dist/express/index.d.ts +40 -0
- package/dist/express/index.js +76 -0
- package/dist/express/index.js.map +1 -0
- package/dist/index.cjs +257 -1357
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -12
- package/dist/index.d.ts +6 -12
- package/dist/index.js +239 -1319
- package/dist/index.js.map +1 -1
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +1 -1
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js.map +1 -1
- package/dist/{session-D2IoWAWV.d.cts → types-BWYQMw03.d.cts} +1 -16
- package/dist/{session-D2IoWAWV.d.ts → types-BWYQMw03.d.ts} +1 -16
- package/package.json +29 -69
- package/dist/client-D-dteoJw.d.cts +0 -63
- package/dist/client-DfCIRrNG.d.ts +0 -63
- package/dist/memory-Daxkczti.d.cts +0 -29
- package/dist/memory-Daxkczti.d.ts +0 -29
- package/dist/middleware/index.cjs +0 -273
- package/dist/middleware/index.cjs.map +0 -1
- package/dist/middleware/index.d.cts +0 -91
- package/dist/middleware/index.d.ts +0 -91
- package/dist/middleware/index.js +0 -267
- package/dist/middleware/index.js.map +0 -1
- package/dist/nextjs-BDyOqGAq.d.cts +0 -81
- package/dist/nextjs-CbX8_9yK.d.ts +0 -81
- package/dist/payment-BGp7eMQl.d.cts +0 -103
- package/dist/payment-BGp7eMQl.d.ts +0 -103
- package/dist/priority-fees-C-OH4Trr.d.cts +0 -50
- package/dist/priority-fees-C-OH4Trr.d.ts +0 -50
- package/dist/solana/index.cjs +0 -589
- package/dist/solana/index.cjs.map +0 -1
- package/dist/solana/index.d.cts +0 -195
- package/dist/solana/index.d.ts +0 -195
- package/dist/solana/index.js +0 -567
- package/dist/solana/index.js.map +0 -1
- package/dist/store/index.cjs +0 -99
- package/dist/store/index.cjs.map +0 -1
- package/dist/store/index.d.cts +0 -38
- package/dist/store/index.d.ts +0 -38
- package/dist/store/index.js +0 -96
- package/dist/store/index.js.map +0 -1
- package/dist/utils/index.cjs +0 -68
- package/dist/utils/index.cjs.map +0 -1
- package/dist/utils/index.d.cts +0 -30
- package/dist/utils/index.d.ts +0 -30
- package/dist/utils/index.js +0 -65
- package/dist/utils/index.js.map +0 -1
- package/dist/x402/index.cjs +0 -387
- package/dist/x402/index.cjs.map +0 -1
- package/dist/x402/index.d.cts +0 -96
- package/dist/x402/index.d.ts +0 -96
- package/dist/x402/index.js +0 -375
- package/dist/x402/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1151 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
export * from '@x402/core';
|
|
2
|
+
export * from '@x402/core/types';
|
|
3
|
+
export * from '@x402/core/client';
|
|
4
|
+
export * from '@x402/svm';
|
|
5
|
+
import { PublicKey, SystemProgram, LAMPORTS_PER_SOL, Keypair, TransactionMessage, VersionedTransaction, ComputeBudgetProgram } from '@solana/web3.js';
|
|
2
6
|
import { SignJWT, jwtVerify } from 'jose';
|
|
3
7
|
import { v4 } from 'uuid';
|
|
4
8
|
|
|
5
|
-
// src/
|
|
6
|
-
var TOKEN_MINTS = {
|
|
7
|
-
/** USDC on mainnet */
|
|
8
|
-
USDC_MAINNET: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
9
|
-
/** USDC on devnet */
|
|
10
|
-
USDC_DEVNET: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
11
|
-
/** USDT on mainnet */
|
|
12
|
-
USDT_MAINNET: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
13
|
-
};
|
|
14
|
-
var cachedConnection = null;
|
|
15
|
-
var cachedNetwork = null;
|
|
16
|
-
var cachedFallbacks = [];
|
|
17
|
-
var cachedFallbackEnabled = false;
|
|
18
|
-
function buildRpcUrl(config2) {
|
|
19
|
-
const { network, rpcUrl, tatumApiKey } = config2;
|
|
20
|
-
if (rpcUrl) {
|
|
21
|
-
if (rpcUrl.includes("tatum.io") && tatumApiKey && !rpcUrl.includes(tatumApiKey)) {
|
|
22
|
-
return rpcUrl.endsWith("/") ? `${rpcUrl}${tatumApiKey}` : `${rpcUrl}/${tatumApiKey}`;
|
|
23
|
-
}
|
|
24
|
-
return rpcUrl;
|
|
25
|
-
}
|
|
26
|
-
if (tatumApiKey) {
|
|
27
|
-
const baseUrl = network === "mainnet-beta" ? "https://solana-mainnet.gateway.tatum.io" : "https://solana-devnet.gateway.tatum.io";
|
|
28
|
-
return `${baseUrl}/${tatumApiKey}`;
|
|
29
|
-
}
|
|
30
|
-
return clusterApiUrl(network);
|
|
31
|
-
}
|
|
32
|
-
function createConnection(rpcUrl) {
|
|
33
|
-
return new Connection(rpcUrl, {
|
|
34
|
-
commitment: "confirmed",
|
|
35
|
-
confirmTransactionInitialTimeout: 6e4
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
function getConnection(config2) {
|
|
39
|
-
const { network } = config2;
|
|
40
|
-
if (cachedConnection && cachedNetwork === network) {
|
|
41
|
-
return cachedConnection;
|
|
42
|
-
}
|
|
43
|
-
const rpcUrl = buildRpcUrl(config2);
|
|
44
|
-
cachedConnection = createConnection(rpcUrl);
|
|
45
|
-
cachedNetwork = network;
|
|
46
|
-
cachedFallbackEnabled = config2.enableFallback ?? false;
|
|
47
|
-
cachedFallbacks = [];
|
|
48
|
-
if (cachedFallbackEnabled && config2.fallbackRpcUrls?.length) {
|
|
49
|
-
cachedFallbacks = config2.fallbackRpcUrls.map(createConnection);
|
|
50
|
-
}
|
|
51
|
-
return cachedConnection;
|
|
52
|
-
}
|
|
53
|
-
function getConnectionWithFallback(config2) {
|
|
54
|
-
const connection = getConnection(config2);
|
|
55
|
-
return {
|
|
56
|
-
connection,
|
|
57
|
-
fallbacks: cachedFallbacks,
|
|
58
|
-
fallbackEnabled: cachedFallbackEnabled
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
async function withFallback(config2, operation) {
|
|
62
|
-
const { connection, fallbacks, fallbackEnabled } = getConnectionWithFallback(config2);
|
|
63
|
-
try {
|
|
64
|
-
return await operation(connection);
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if (!fallbackEnabled || fallbacks.length === 0) {
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
if (!isRetryableError(error)) {
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
for (let i = 0; i < fallbacks.length; i++) {
|
|
73
|
-
try {
|
|
74
|
-
return await operation(fallbacks[i]);
|
|
75
|
-
} catch (fallbackError) {
|
|
76
|
-
if (i === fallbacks.length - 1) {
|
|
77
|
-
throw fallbackError;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function isRetryableError(error) {
|
|
85
|
-
if (error instanceof Error) {
|
|
86
|
-
const message = error.message.toLowerCase();
|
|
87
|
-
return message.includes("429") || message.includes("503") || message.includes("502") || message.includes("timeout") || message.includes("econnrefused") || message.includes("enotfound") || message.includes("rate limit");
|
|
88
|
-
}
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
function resetConnection() {
|
|
92
|
-
cachedConnection = null;
|
|
93
|
-
cachedNetwork = null;
|
|
94
|
-
}
|
|
95
|
-
function isMainnet(network) {
|
|
96
|
-
return network === "mainnet-beta";
|
|
97
|
-
}
|
|
98
|
-
function toX402Network(network) {
|
|
99
|
-
return network === "mainnet-beta" ? "solana-mainnet" : "solana-devnet";
|
|
100
|
-
}
|
|
101
|
-
var SIGNATURE_REGEX = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
102
|
-
var WALLET_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
103
|
-
function isValidSignature(signature) {
|
|
104
|
-
if (!signature || typeof signature !== "string") return false;
|
|
105
|
-
return SIGNATURE_REGEX.test(signature);
|
|
106
|
-
}
|
|
107
|
-
function isValidWalletAddress(address) {
|
|
108
|
-
if (!address || typeof address !== "string") return false;
|
|
109
|
-
return WALLET_REGEX.test(address);
|
|
110
|
-
}
|
|
111
|
-
function parseSOLTransfer(transaction, expectedRecipient) {
|
|
112
|
-
const instructions = transaction.transaction.message.instructions;
|
|
113
|
-
for (const ix of instructions) {
|
|
114
|
-
if ("parsed" in ix && ix.program === "system") {
|
|
115
|
-
const parsed = ix.parsed;
|
|
116
|
-
if (parsed.type === "transfer" && parsed.info.destination === expectedRecipient) {
|
|
117
|
-
return {
|
|
118
|
-
from: parsed.info.source,
|
|
119
|
-
to: parsed.info.destination,
|
|
120
|
-
amount: BigInt(parsed.info.lamports)
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (transaction.meta?.innerInstructions) {
|
|
126
|
-
for (const inner of transaction.meta.innerInstructions) {
|
|
127
|
-
for (const ix of inner.instructions) {
|
|
128
|
-
if ("parsed" in ix && ix.program === "system") {
|
|
129
|
-
const parsed = ix.parsed;
|
|
130
|
-
if (parsed.type === "transfer" && parsed.info.destination === expectedRecipient) {
|
|
131
|
-
return {
|
|
132
|
-
from: parsed.info.source,
|
|
133
|
-
to: parsed.info.destination,
|
|
134
|
-
amount: BigInt(parsed.info.lamports)
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
async function verifyPayment(params) {
|
|
144
|
-
const {
|
|
145
|
-
signature,
|
|
146
|
-
expectedRecipient,
|
|
147
|
-
expectedAmount,
|
|
148
|
-
maxAgeSeconds = 300,
|
|
149
|
-
clientConfig,
|
|
150
|
-
signatureStore
|
|
151
|
-
} = params;
|
|
152
|
-
if (!isValidSignature(signature)) {
|
|
153
|
-
return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
|
|
154
|
-
}
|
|
155
|
-
if (!isValidWalletAddress(expectedRecipient)) {
|
|
156
|
-
return { valid: false, confirmed: false, signature, error: "Invalid recipient address" };
|
|
157
|
-
}
|
|
158
|
-
if (expectedAmount <= 0n) {
|
|
159
|
-
return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
|
|
160
|
-
}
|
|
161
|
-
if (signatureStore) {
|
|
162
|
-
const isUsed = await signatureStore.hasBeenUsed(signature);
|
|
163
|
-
if (isUsed) {
|
|
164
|
-
return { valid: false, confirmed: true, signature, error: "Signature already used" };
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
|
|
168
|
-
const connection = getConnection(clientConfig);
|
|
169
|
-
try {
|
|
170
|
-
const transaction = await connection.getParsedTransaction(signature, {
|
|
171
|
-
commitment: "confirmed",
|
|
172
|
-
maxSupportedTransactionVersion: 0
|
|
173
|
-
});
|
|
174
|
-
if (!transaction) {
|
|
175
|
-
return { valid: false, confirmed: false, signature, error: "Transaction not found" };
|
|
176
|
-
}
|
|
177
|
-
if (transaction.meta?.err) {
|
|
178
|
-
return {
|
|
179
|
-
valid: false,
|
|
180
|
-
confirmed: true,
|
|
181
|
-
signature,
|
|
182
|
-
error: "Transaction failed on-chain"
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
if (transaction.blockTime) {
|
|
186
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
187
|
-
if (now - transaction.blockTime > effectiveMaxAge) {
|
|
188
|
-
return { valid: false, confirmed: true, signature, error: "Transaction too old" };
|
|
189
|
-
}
|
|
190
|
-
if (transaction.blockTime > now + 60) {
|
|
191
|
-
return { valid: false, confirmed: true, signature, error: "Invalid transaction time" };
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const transferDetails = parseSOLTransfer(transaction, expectedRecipient);
|
|
195
|
-
if (!transferDetails) {
|
|
196
|
-
return {
|
|
197
|
-
valid: false,
|
|
198
|
-
confirmed: true,
|
|
199
|
-
signature,
|
|
200
|
-
error: "No valid SOL transfer to recipient found"
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
if (transferDetails.amount < expectedAmount) {
|
|
204
|
-
return {
|
|
205
|
-
valid: false,
|
|
206
|
-
confirmed: true,
|
|
207
|
-
signature,
|
|
208
|
-
from: transferDetails.from,
|
|
209
|
-
to: transferDetails.to,
|
|
210
|
-
amount: transferDetails.amount,
|
|
211
|
-
error: "Insufficient payment amount"
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
return {
|
|
215
|
-
valid: true,
|
|
216
|
-
confirmed: true,
|
|
217
|
-
signature,
|
|
218
|
-
from: transferDetails.from,
|
|
219
|
-
to: transferDetails.to,
|
|
220
|
-
amount: transferDetails.amount,
|
|
221
|
-
blockTime: transaction.blockTime ?? void 0,
|
|
222
|
-
slot: transaction.slot
|
|
223
|
-
};
|
|
224
|
-
} catch (error) {
|
|
225
|
-
return {
|
|
226
|
-
valid: false,
|
|
227
|
-
confirmed: false,
|
|
228
|
-
signature,
|
|
229
|
-
error: "Verification failed"
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
async function waitForConfirmation(signature, clientConfig) {
|
|
234
|
-
if (!isValidSignature(signature)) {
|
|
235
|
-
return { confirmed: false, error: "Invalid signature format" };
|
|
236
|
-
}
|
|
237
|
-
const connection = getConnection(clientConfig);
|
|
238
|
-
try {
|
|
239
|
-
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
240
|
-
if (confirmation.value.err) {
|
|
241
|
-
return { confirmed: false, error: "Transaction failed" };
|
|
242
|
-
}
|
|
243
|
-
return { confirmed: true, slot: confirmation.context?.slot };
|
|
244
|
-
} catch {
|
|
245
|
-
return { confirmed: false, error: "Confirmation timeout" };
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
async function getWalletTransactions(walletAddress, clientConfig, limit = 20) {
|
|
249
|
-
if (!isValidWalletAddress(walletAddress)) {
|
|
250
|
-
return [];
|
|
251
|
-
}
|
|
252
|
-
const safeLimit = Math.min(Math.max(limit, 1), 100);
|
|
253
|
-
const connection = getConnection(clientConfig);
|
|
254
|
-
try {
|
|
255
|
-
const pubkey = new PublicKey(walletAddress);
|
|
256
|
-
const signatures = await connection.getSignaturesForAddress(pubkey, { limit: safeLimit });
|
|
257
|
-
return signatures.map((sig) => ({
|
|
258
|
-
signature: sig.signature,
|
|
259
|
-
blockTime: sig.blockTime ?? void 0,
|
|
260
|
-
slot: sig.slot
|
|
261
|
-
}));
|
|
262
|
-
} catch {
|
|
263
|
-
return [];
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
function lamportsToSol(lamports) {
|
|
267
|
-
return Number(lamports) / LAMPORTS_PER_SOL;
|
|
268
|
-
}
|
|
269
|
-
function solToLamports(sol) {
|
|
270
|
-
if (!Number.isFinite(sol) || sol < 0) {
|
|
271
|
-
throw new Error("Invalid SOL amount");
|
|
272
|
-
}
|
|
273
|
-
return BigInt(Math.floor(sol * LAMPORTS_PER_SOL));
|
|
274
|
-
}
|
|
275
|
-
var SIGNATURE_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
276
|
-
var WALLET_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
277
|
-
function resolveMintAddress(asset, network) {
|
|
278
|
-
if (asset === "native") return null;
|
|
279
|
-
if (asset === "usdc") {
|
|
280
|
-
return network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
281
|
-
}
|
|
282
|
-
if (asset === "usdt") {
|
|
283
|
-
return TOKEN_MINTS.USDT_MAINNET;
|
|
284
|
-
}
|
|
285
|
-
if (typeof asset === "object" && "mint" in asset) {
|
|
286
|
-
return asset.mint;
|
|
287
|
-
}
|
|
288
|
-
return null;
|
|
289
|
-
}
|
|
290
|
-
function getTokenDecimals(asset) {
|
|
291
|
-
if (asset === "native") return 9;
|
|
292
|
-
if (asset === "usdc" || asset === "usdt") return 6;
|
|
293
|
-
if (typeof asset === "object" && "decimals" in asset) {
|
|
294
|
-
return asset.decimals ?? 6;
|
|
295
|
-
}
|
|
296
|
-
return 6;
|
|
297
|
-
}
|
|
298
|
-
function parseSPLTransfer(transaction, expectedRecipient, expectedMint) {
|
|
299
|
-
const instructions = transaction.transaction.message.instructions;
|
|
300
|
-
for (const ix of instructions) {
|
|
301
|
-
if ("parsed" in ix && (ix.program === "spl-token" || ix.program === "spl-token-2022")) {
|
|
302
|
-
const parsed = ix.parsed;
|
|
303
|
-
if (parsed.type === "transfer" || parsed.type === "transferChecked") {
|
|
304
|
-
const amount = parsed.info.amount || parsed.info.tokenAmount?.amount;
|
|
305
|
-
if (amount && parsed.info.destination) {
|
|
306
|
-
return {
|
|
307
|
-
from: parsed.info.authority || parsed.info.source || "",
|
|
308
|
-
to: parsed.info.destination,
|
|
309
|
-
amount: BigInt(amount),
|
|
310
|
-
mint: parsed.info.mint || expectedMint
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (transaction.meta?.innerInstructions) {
|
|
317
|
-
for (const inner of transaction.meta.innerInstructions) {
|
|
318
|
-
for (const ix of inner.instructions) {
|
|
319
|
-
if ("parsed" in ix && (ix.program === "spl-token" || ix.program === "spl-token-2022")) {
|
|
320
|
-
const parsed = ix.parsed;
|
|
321
|
-
if (parsed.type === "transfer" || parsed.type === "transferChecked") {
|
|
322
|
-
const amount = parsed.info.amount || parsed.info.tokenAmount?.amount;
|
|
323
|
-
if (amount) {
|
|
324
|
-
return {
|
|
325
|
-
from: parsed.info.authority || parsed.info.source || "",
|
|
326
|
-
to: parsed.info.destination || "",
|
|
327
|
-
amount: BigInt(amount),
|
|
328
|
-
mint: parsed.info.mint || expectedMint
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
if (transaction.meta?.postTokenBalances && transaction.meta?.preTokenBalances) {
|
|
337
|
-
const preBalances = transaction.meta.preTokenBalances;
|
|
338
|
-
const postBalances = transaction.meta.postTokenBalances;
|
|
339
|
-
for (const post of postBalances) {
|
|
340
|
-
if (post.mint === expectedMint && post.owner === expectedRecipient) {
|
|
341
|
-
const pre = preBalances.find(
|
|
342
|
-
(p) => p.accountIndex === post.accountIndex
|
|
343
|
-
);
|
|
344
|
-
const preAmount = BigInt(pre?.uiTokenAmount?.amount || "0");
|
|
345
|
-
const postAmount = BigInt(post.uiTokenAmount?.amount || "0");
|
|
346
|
-
const transferred = postAmount - preAmount;
|
|
347
|
-
if (transferred > 0n) {
|
|
348
|
-
return {
|
|
349
|
-
from: "",
|
|
350
|
-
// Can't determine from balance changes
|
|
351
|
-
to: expectedRecipient,
|
|
352
|
-
amount: transferred,
|
|
353
|
-
mint: expectedMint
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
async function verifySPLPayment(params) {
|
|
362
|
-
const {
|
|
363
|
-
signature,
|
|
364
|
-
expectedRecipient,
|
|
365
|
-
expectedAmount,
|
|
366
|
-
asset,
|
|
367
|
-
clientConfig,
|
|
368
|
-
maxAgeSeconds = 300,
|
|
369
|
-
signatureStore
|
|
370
|
-
} = params;
|
|
371
|
-
if (signatureStore) {
|
|
372
|
-
const isUsed = await signatureStore.hasBeenUsed(signature);
|
|
373
|
-
if (isUsed) {
|
|
374
|
-
return { valid: false, confirmed: true, signature, error: "Signature already used" };
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
if (!SIGNATURE_REGEX2.test(signature)) {
|
|
378
|
-
return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
|
|
379
|
-
}
|
|
380
|
-
if (!WALLET_REGEX2.test(expectedRecipient)) {
|
|
381
|
-
return { valid: false, confirmed: false, signature, error: "Invalid recipient address" };
|
|
382
|
-
}
|
|
383
|
-
const mintAddress = resolveMintAddress(asset, clientConfig.network);
|
|
384
|
-
if (!mintAddress) {
|
|
385
|
-
return { valid: false, confirmed: false, signature, error: "Invalid asset configuration" };
|
|
386
|
-
}
|
|
387
|
-
if (expectedAmount <= 0n) {
|
|
388
|
-
return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
|
|
389
|
-
}
|
|
390
|
-
const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
|
|
391
|
-
const connection = getConnection(clientConfig);
|
|
392
|
-
try {
|
|
393
|
-
const transaction = await connection.getParsedTransaction(signature, {
|
|
394
|
-
commitment: "confirmed",
|
|
395
|
-
maxSupportedTransactionVersion: 0
|
|
396
|
-
});
|
|
397
|
-
if (!transaction) {
|
|
398
|
-
return { valid: false, confirmed: false, signature, error: "Transaction not found" };
|
|
399
|
-
}
|
|
400
|
-
if (transaction.meta?.err) {
|
|
401
|
-
return { valid: false, confirmed: true, signature, error: "Transaction failed on-chain" };
|
|
402
|
-
}
|
|
403
|
-
if (transaction.blockTime) {
|
|
404
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
405
|
-
if (now - transaction.blockTime > effectiveMaxAge) {
|
|
406
|
-
return { valid: false, confirmed: true, signature, error: "Transaction too old" };
|
|
407
|
-
}
|
|
408
|
-
if (transaction.blockTime > now + 60) {
|
|
409
|
-
return { valid: false, confirmed: true, signature, error: "Invalid transaction time" };
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
const transfer = parseSPLTransfer(transaction, expectedRecipient, mintAddress);
|
|
413
|
-
if (!transfer) {
|
|
414
|
-
return {
|
|
415
|
-
valid: false,
|
|
416
|
-
confirmed: true,
|
|
417
|
-
signature,
|
|
418
|
-
error: "No valid token transfer to recipient found"
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
if (transfer.to) {
|
|
422
|
-
try {
|
|
423
|
-
const destinationInfo = await connection.getParsedAccountInfo(new PublicKey(transfer.to));
|
|
424
|
-
const owner = destinationInfo.value?.data?.parsed?.info?.owner;
|
|
425
|
-
if (owner && owner !== expectedRecipient) {
|
|
426
|
-
return {
|
|
427
|
-
valid: false,
|
|
428
|
-
confirmed: true,
|
|
429
|
-
signature,
|
|
430
|
-
error: "Recipient mismatch: Token account not owned by merchant"
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
} catch (e) {
|
|
434
|
-
return {
|
|
435
|
-
valid: false,
|
|
436
|
-
confirmed: true,
|
|
437
|
-
signature,
|
|
438
|
-
error: "Could not verify token account owner"
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
if (transfer.mint !== mintAddress) {
|
|
443
|
-
return {
|
|
444
|
-
valid: false,
|
|
445
|
-
confirmed: true,
|
|
446
|
-
signature,
|
|
447
|
-
error: "Token mint mismatch"
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
if (transfer.amount < expectedAmount) {
|
|
451
|
-
return {
|
|
452
|
-
valid: false,
|
|
453
|
-
confirmed: true,
|
|
454
|
-
signature,
|
|
455
|
-
from: transfer.from,
|
|
456
|
-
to: transfer.to,
|
|
457
|
-
mint: transfer.mint,
|
|
458
|
-
amount: transfer.amount,
|
|
459
|
-
error: "Insufficient payment amount"
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
return {
|
|
463
|
-
valid: true,
|
|
464
|
-
confirmed: true,
|
|
465
|
-
signature,
|
|
466
|
-
from: transfer.from,
|
|
467
|
-
to: transfer.to,
|
|
468
|
-
mint: transfer.mint,
|
|
469
|
-
amount: transfer.amount,
|
|
470
|
-
blockTime: transaction.blockTime ?? void 0,
|
|
471
|
-
slot: transaction.slot
|
|
472
|
-
};
|
|
473
|
-
} catch {
|
|
474
|
-
return { valid: false, confirmed: false, signature, error: "Verification failed" };
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
function isNativeAsset(asset) {
|
|
478
|
-
return asset === "native";
|
|
479
|
-
}
|
|
480
|
-
var DEFAULT_COMPUTE_UNITS = 2e5;
|
|
481
|
-
var DEFAULT_MICRO_LAMPORTS = 1e3;
|
|
482
|
-
function createPriorityFeeInstructions(config2 = {}) {
|
|
483
|
-
const { enabled = false, microLamports, computeUnits } = config2;
|
|
484
|
-
if (!enabled) {
|
|
485
|
-
return [];
|
|
486
|
-
}
|
|
487
|
-
const instructions = [];
|
|
488
|
-
const units = computeUnits ?? DEFAULT_COMPUTE_UNITS;
|
|
489
|
-
instructions.push(ComputeBudgetProgram.setComputeUnitLimit({ units }));
|
|
490
|
-
const price = microLamports ?? DEFAULT_MICRO_LAMPORTS;
|
|
491
|
-
instructions.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: price }));
|
|
492
|
-
return instructions;
|
|
493
|
-
}
|
|
494
|
-
async function estimatePriorityFee(connection, accounts = []) {
|
|
495
|
-
try {
|
|
496
|
-
const fees = await connection.getRecentPrioritizationFees({
|
|
497
|
-
lockedWritableAccounts: accounts
|
|
498
|
-
});
|
|
499
|
-
if (fees.length === 0) {
|
|
500
|
-
return DEFAULT_MICRO_LAMPORTS;
|
|
501
|
-
}
|
|
502
|
-
const sortedFees = fees.map((f) => f.prioritizationFee).filter((f) => f > 0).sort((a, b) => a - b);
|
|
503
|
-
if (sortedFees.length === 0) {
|
|
504
|
-
return DEFAULT_MICRO_LAMPORTS;
|
|
505
|
-
}
|
|
506
|
-
const medianIndex = Math.floor(sortedFees.length / 2);
|
|
507
|
-
return sortedFees[medianIndex];
|
|
508
|
-
} catch {
|
|
509
|
-
return DEFAULT_MICRO_LAMPORTS;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
function calculatePriorityFeeCost(microLamportsPerCU, computeUnits) {
|
|
513
|
-
return Math.ceil(microLamportsPerCU * computeUnits / 1e6);
|
|
514
|
-
}
|
|
515
|
-
async function buildVersionedTransaction(config2) {
|
|
516
|
-
const {
|
|
517
|
-
connection,
|
|
518
|
-
payer,
|
|
519
|
-
instructions,
|
|
520
|
-
lookupTables = [],
|
|
521
|
-
priorityFee,
|
|
522
|
-
recentBlockhash
|
|
523
|
-
} = config2;
|
|
524
|
-
const priorityIxs = createPriorityFeeInstructions(priorityFee);
|
|
525
|
-
const allInstructions = [...priorityIxs, ...instructions];
|
|
526
|
-
let blockhash;
|
|
527
|
-
let lastValidBlockHeight;
|
|
528
|
-
if (recentBlockhash) {
|
|
529
|
-
blockhash = recentBlockhash;
|
|
530
|
-
const slot = await connection.getSlot();
|
|
531
|
-
lastValidBlockHeight = slot + 150;
|
|
532
|
-
} else {
|
|
533
|
-
const latestBlockhash = await connection.getLatestBlockhash("confirmed");
|
|
534
|
-
blockhash = latestBlockhash.blockhash;
|
|
535
|
-
lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
|
|
536
|
-
}
|
|
537
|
-
const message = new TransactionMessage({
|
|
538
|
-
payerKey: payer,
|
|
539
|
-
recentBlockhash: blockhash,
|
|
540
|
-
instructions: allInstructions
|
|
541
|
-
}).compileToV0Message(lookupTables);
|
|
542
|
-
const transaction = new VersionedTransaction(message);
|
|
543
|
-
return {
|
|
544
|
-
transaction,
|
|
545
|
-
blockhash,
|
|
546
|
-
lastValidBlockHeight
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
async function fetchLookupTables(connection, addresses) {
|
|
550
|
-
const tables = [];
|
|
551
|
-
for (const address of addresses) {
|
|
552
|
-
const result = await connection.getAddressLookupTable(address);
|
|
553
|
-
if (result.value) {
|
|
554
|
-
tables.push(result.value);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return tables;
|
|
558
|
-
}
|
|
559
|
-
function isVersionedTransaction(tx) {
|
|
560
|
-
return tx instanceof VersionedTransaction;
|
|
561
|
-
}
|
|
562
|
-
var MAX_ARTICLES_PER_SESSION = 100;
|
|
563
|
-
var MIN_SECRET_LENGTH = 32;
|
|
564
|
-
function getSecretKey(secret) {
|
|
565
|
-
if (!secret || typeof secret !== "string") {
|
|
566
|
-
throw new Error("Session secret is required");
|
|
567
|
-
}
|
|
568
|
-
if (secret.length < MIN_SECRET_LENGTH) {
|
|
569
|
-
throw new Error(`Session secret must be at least ${MIN_SECRET_LENGTH} characters`);
|
|
570
|
-
}
|
|
571
|
-
return new TextEncoder().encode(secret);
|
|
572
|
-
}
|
|
573
|
-
function validateWalletAddress(address) {
|
|
574
|
-
if (!address || typeof address !== "string") return false;
|
|
575
|
-
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
576
|
-
return base58Regex.test(address);
|
|
577
|
-
}
|
|
578
|
-
function validateArticleId(articleId) {
|
|
579
|
-
if (!articleId || typeof articleId !== "string") return false;
|
|
580
|
-
if (articleId.length > 128) return false;
|
|
581
|
-
const safeIdRegex = /^[a-zA-Z0-9_-]+$/;
|
|
582
|
-
return safeIdRegex.test(articleId);
|
|
583
|
-
}
|
|
584
|
-
async function createSession(walletAddress, articleId, config2, siteWide = false) {
|
|
585
|
-
if (!validateWalletAddress(walletAddress)) {
|
|
586
|
-
throw new Error("Invalid wallet address format");
|
|
587
|
-
}
|
|
588
|
-
if (!validateArticleId(articleId)) {
|
|
589
|
-
throw new Error("Invalid article ID format");
|
|
590
|
-
}
|
|
591
|
-
if (!config2.durationHours || config2.durationHours <= 0 || config2.durationHours > 720) {
|
|
592
|
-
throw new Error("Session duration must be between 1 and 720 hours");
|
|
593
|
-
}
|
|
594
|
-
const sessionId = v4();
|
|
595
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
596
|
-
const expiresAt = now + config2.durationHours * 3600;
|
|
597
|
-
const session = {
|
|
598
|
-
id: sessionId,
|
|
599
|
-
walletAddress,
|
|
600
|
-
unlockedArticles: [articleId],
|
|
601
|
-
siteWideUnlock: Boolean(siteWide),
|
|
602
|
-
createdAt: now,
|
|
603
|
-
expiresAt
|
|
604
|
-
};
|
|
605
|
-
const payload = {
|
|
606
|
-
sub: walletAddress,
|
|
607
|
-
sid: sessionId,
|
|
608
|
-
articles: session.unlockedArticles,
|
|
609
|
-
siteWide: session.siteWideUnlock,
|
|
610
|
-
iat: now,
|
|
611
|
-
exp: expiresAt
|
|
612
|
-
};
|
|
613
|
-
const token = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config2.durationHours}h`).sign(getSecretKey(config2.secret));
|
|
614
|
-
return { token, session };
|
|
615
|
-
}
|
|
616
|
-
async function validateSession(token, secret) {
|
|
617
|
-
if (!token || typeof token !== "string") {
|
|
618
|
-
return { valid: false, reason: "Invalid token format" };
|
|
619
|
-
}
|
|
620
|
-
try {
|
|
621
|
-
const { payload } = await jwtVerify(token, getSecretKey(secret));
|
|
622
|
-
const sessionPayload = payload;
|
|
623
|
-
if (!sessionPayload.sub || !sessionPayload.sid || !sessionPayload.exp) {
|
|
624
|
-
return { valid: false, reason: "Malformed session payload" };
|
|
625
|
-
}
|
|
626
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
627
|
-
if (sessionPayload.exp < now) {
|
|
628
|
-
return { valid: false, reason: "Session expired" };
|
|
629
|
-
}
|
|
630
|
-
if (!validateWalletAddress(sessionPayload.sub)) {
|
|
631
|
-
return { valid: false, reason: "Invalid session data" };
|
|
632
|
-
}
|
|
633
|
-
const session = {
|
|
634
|
-
id: sessionPayload.sid,
|
|
635
|
-
walletAddress: sessionPayload.sub,
|
|
636
|
-
unlockedArticles: Array.isArray(sessionPayload.articles) ? sessionPayload.articles : [],
|
|
637
|
-
siteWideUnlock: Boolean(sessionPayload.siteWide),
|
|
638
|
-
createdAt: sessionPayload.iat ?? 0,
|
|
639
|
-
expiresAt: sessionPayload.exp
|
|
640
|
-
};
|
|
641
|
-
return { valid: true, session };
|
|
642
|
-
} catch (error) {
|
|
643
|
-
return { valid: false, reason: "Invalid session" };
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
async function addArticleToSession(token, articleId, secret) {
|
|
647
|
-
if (!validateArticleId(articleId)) {
|
|
648
|
-
return null;
|
|
649
|
-
}
|
|
650
|
-
const validation = await validateSession(token, secret);
|
|
651
|
-
if (!validation.valid || !validation.session) {
|
|
652
|
-
return null;
|
|
653
|
-
}
|
|
654
|
-
const session = validation.session;
|
|
655
|
-
if (session.unlockedArticles.includes(articleId)) {
|
|
656
|
-
return { token, session };
|
|
657
|
-
}
|
|
658
|
-
if (session.unlockedArticles.length >= MAX_ARTICLES_PER_SESSION) {
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
const updatedArticles = [...session.unlockedArticles, articleId];
|
|
662
|
-
const payload = {
|
|
663
|
-
sub: session.walletAddress,
|
|
664
|
-
sid: session.id,
|
|
665
|
-
articles: updatedArticles,
|
|
666
|
-
siteWide: session.siteWideUnlock,
|
|
667
|
-
iat: session.createdAt,
|
|
668
|
-
exp: session.expiresAt
|
|
669
|
-
};
|
|
670
|
-
const newToken = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(getSecretKey(secret));
|
|
671
|
-
return {
|
|
672
|
-
token: newToken,
|
|
673
|
-
session: { ...session, unlockedArticles: updatedArticles }
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
async function isArticleUnlocked(token, articleId, secret) {
|
|
677
|
-
if (!validateArticleId(articleId)) {
|
|
678
|
-
return false;
|
|
679
|
-
}
|
|
680
|
-
const validation = await validateSession(token, secret);
|
|
681
|
-
if (!validation.valid || !validation.session) {
|
|
682
|
-
return false;
|
|
683
|
-
}
|
|
684
|
-
if (validation.session.siteWideUnlock) {
|
|
685
|
-
return true;
|
|
686
|
-
}
|
|
687
|
-
return validation.session.unlockedArticles.includes(articleId);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// src/x402/config.ts
|
|
691
|
-
var WALLET_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
692
|
-
function sanitizeDisplayString(str, maxLength = 200) {
|
|
693
|
-
if (!str || typeof str !== "string") return "";
|
|
694
|
-
return str.slice(0, maxLength).replace(/[<>"'&]/g, "");
|
|
695
|
-
}
|
|
696
|
-
function isValidUrl(url) {
|
|
697
|
-
try {
|
|
698
|
-
const parsed = new URL(url);
|
|
699
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
700
|
-
} catch {
|
|
701
|
-
return false;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
function buildPaymentRequirement(params) {
|
|
705
|
-
if (!WALLET_REGEX3.test(params.creatorWallet)) {
|
|
706
|
-
throw new Error("Invalid creator wallet address");
|
|
707
|
-
}
|
|
708
|
-
if (params.priceInLamports <= 0n) {
|
|
709
|
-
throw new Error("Price must be positive");
|
|
710
|
-
}
|
|
711
|
-
if (!isValidUrl(params.resourceUrl)) {
|
|
712
|
-
throw new Error("Invalid resource URL");
|
|
713
|
-
}
|
|
714
|
-
if (params.network !== "devnet" && params.network !== "mainnet-beta") {
|
|
715
|
-
throw new Error("Invalid network");
|
|
716
|
-
}
|
|
717
|
-
const timeout = params.maxTimeoutSeconds ?? 300;
|
|
718
|
-
if (timeout < 60 || timeout > 3600) {
|
|
719
|
-
throw new Error("Timeout must be between 60 and 3600 seconds");
|
|
720
|
-
}
|
|
721
|
-
const x402Network = toX402Network(params.network);
|
|
722
|
-
const safeTitle = sanitizeDisplayString(params.articleTitle, 200);
|
|
723
|
-
const safeArticleId = sanitizeDisplayString(params.articleId, 128);
|
|
724
|
-
return {
|
|
725
|
-
scheme: "exact",
|
|
726
|
-
network: x402Network,
|
|
727
|
-
maxAmountRequired: params.priceInLamports.toString(),
|
|
728
|
-
resource: params.resourceUrl,
|
|
729
|
-
description: `Unlock: ${safeTitle}`,
|
|
730
|
-
mimeType: "text/html",
|
|
731
|
-
payTo: params.creatorWallet,
|
|
732
|
-
maxTimeoutSeconds: timeout,
|
|
733
|
-
asset: "native",
|
|
734
|
-
extra: {
|
|
735
|
-
name: safeTitle,
|
|
736
|
-
articleId: safeArticleId
|
|
737
|
-
}
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
function encodePaymentRequired(requirement) {
|
|
741
|
-
return Buffer.from(JSON.stringify(requirement)).toString("base64");
|
|
742
|
-
}
|
|
743
|
-
function decodePaymentRequired(encoded) {
|
|
744
|
-
if (!encoded || typeof encoded !== "string") {
|
|
745
|
-
throw new Error("Invalid encoded requirement");
|
|
746
|
-
}
|
|
747
|
-
if (encoded.length > 1e4) {
|
|
748
|
-
throw new Error("Encoded requirement too large");
|
|
749
|
-
}
|
|
750
|
-
try {
|
|
751
|
-
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
|
|
752
|
-
return JSON.parse(decoded);
|
|
753
|
-
} catch {
|
|
754
|
-
throw new Error("Failed to decode payment requirement");
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
var X402_HEADERS = {
|
|
758
|
-
PAYMENT_REQUIRED: "X-Payment-Required",
|
|
759
|
-
PAYMENT: "X-Payment",
|
|
760
|
-
PAYMENT_RESPONSE: "X-Payment-Response"
|
|
761
|
-
};
|
|
762
|
-
function create402ResponseBody(requirement) {
|
|
763
|
-
const assetStr = typeof requirement.asset === "string" ? requirement.asset : requirement.asset.mint;
|
|
764
|
-
return {
|
|
765
|
-
error: "Payment Required",
|
|
766
|
-
message: requirement.description,
|
|
767
|
-
price: {
|
|
768
|
-
amount: requirement.maxAmountRequired,
|
|
769
|
-
asset: assetStr,
|
|
770
|
-
network: requirement.network
|
|
771
|
-
}
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
function create402Headers(requirement) {
|
|
775
|
-
const encoded = encodePaymentRequired(requirement);
|
|
776
|
-
return {
|
|
777
|
-
"Content-Type": "application/json",
|
|
778
|
-
[X402_HEADERS.PAYMENT_REQUIRED]: encoded,
|
|
779
|
-
"Access-Control-Expose-Headers": X402_HEADERS.PAYMENT_REQUIRED
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// src/x402/verification.ts
|
|
784
|
-
var SIGNATURE_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
785
|
-
async function verifyX402Payment(payload, requirement, clientConfig) {
|
|
786
|
-
if (!payload || typeof payload !== "object") {
|
|
787
|
-
return { valid: false, invalidReason: "Invalid payload" };
|
|
788
|
-
}
|
|
789
|
-
const signature = payload.payload?.signature;
|
|
790
|
-
if (!signature || typeof signature !== "string") {
|
|
791
|
-
return { valid: false, invalidReason: "Missing transaction signature" };
|
|
792
|
-
}
|
|
793
|
-
if (!SIGNATURE_REGEX3.test(signature)) {
|
|
794
|
-
return { valid: false, invalidReason: "Invalid signature format" };
|
|
795
|
-
}
|
|
796
|
-
if (payload.x402Version !== 1) {
|
|
797
|
-
return { valid: false, invalidReason: "Unsupported x402 version" };
|
|
798
|
-
}
|
|
799
|
-
if (payload.scheme !== "exact") {
|
|
800
|
-
return { valid: false, invalidReason: "Unsupported payment scheme" };
|
|
801
|
-
}
|
|
802
|
-
if (payload.network !== requirement.network) {
|
|
803
|
-
return {
|
|
804
|
-
valid: false,
|
|
805
|
-
invalidReason: `Network mismatch: expected ${requirement.network}`
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
const walletRegex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
809
|
-
if (!walletRegex.test(requirement.payTo)) {
|
|
810
|
-
return { valid: false, invalidReason: "Invalid recipient configuration" };
|
|
811
|
-
}
|
|
812
|
-
let expectedAmount;
|
|
813
|
-
try {
|
|
814
|
-
expectedAmount = BigInt(requirement.maxAmountRequired);
|
|
815
|
-
if (expectedAmount <= 0n) {
|
|
816
|
-
return { valid: false, invalidReason: "Invalid payment amount" };
|
|
817
|
-
}
|
|
818
|
-
} catch {
|
|
819
|
-
return { valid: false, invalidReason: "Invalid payment amount format" };
|
|
820
|
-
}
|
|
821
|
-
const verification = await verifyPayment({
|
|
822
|
-
signature,
|
|
823
|
-
expectedRecipient: requirement.payTo,
|
|
824
|
-
expectedAmount,
|
|
825
|
-
maxAgeSeconds: requirement.maxTimeoutSeconds,
|
|
826
|
-
clientConfig
|
|
827
|
-
});
|
|
828
|
-
if (!verification.valid) {
|
|
829
|
-
return {
|
|
830
|
-
valid: false,
|
|
831
|
-
invalidReason: verification.error || "Transaction verification failed"
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
return {
|
|
835
|
-
valid: true,
|
|
836
|
-
settled: verification.confirmed,
|
|
837
|
-
from: verification.from,
|
|
838
|
-
transaction: {
|
|
839
|
-
signature: verification.signature,
|
|
840
|
-
blockTime: verification.blockTime,
|
|
841
|
-
slot: verification.slot
|
|
842
|
-
}
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
function parsePaymentHeader(header) {
|
|
846
|
-
if (!header || typeof header !== "string") {
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
849
|
-
if (header.length > 1e4) {
|
|
850
|
-
return null;
|
|
851
|
-
}
|
|
852
|
-
try {
|
|
853
|
-
const decoded = Buffer.from(header, "base64").toString("utf-8");
|
|
854
|
-
const parsed = JSON.parse(decoded);
|
|
855
|
-
if (!parsed || typeof parsed !== "object") {
|
|
856
|
-
return null;
|
|
857
|
-
}
|
|
858
|
-
return parsed;
|
|
859
|
-
} catch {
|
|
860
|
-
return null;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
function encodePaymentRequirement(requirement) {
|
|
864
|
-
return Buffer.from(JSON.stringify(requirement)).toString("base64");
|
|
865
|
-
}
|
|
866
|
-
function encodePaymentResponse(response) {
|
|
867
|
-
return Buffer.from(JSON.stringify(response)).toString("base64");
|
|
868
|
-
}
|
|
869
|
-
function create402Response(requirement, body) {
|
|
870
|
-
const headers = new Headers({
|
|
871
|
-
"Content-Type": "application/json",
|
|
872
|
-
"X-Payment-Required": encodePaymentRequirement(requirement)
|
|
873
|
-
});
|
|
874
|
-
const responseBody = body || {
|
|
875
|
-
error: "Payment Required",
|
|
876
|
-
message: "This resource requires payment to access",
|
|
877
|
-
x402Version: 1,
|
|
878
|
-
accepts: [requirement]
|
|
879
|
-
};
|
|
880
|
-
return new Response(JSON.stringify(responseBody), {
|
|
881
|
-
status: 402,
|
|
882
|
-
headers
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// src/store/memory.ts
|
|
887
|
-
function createMemoryStore(options = {}) {
|
|
888
|
-
const { cleanupInterval = 6e4 } = options;
|
|
889
|
-
const store = /* @__PURE__ */ new Map();
|
|
890
|
-
const cleanupTimer = setInterval(() => {
|
|
891
|
-
const now = Date.now();
|
|
892
|
-
for (const [key, record] of store.entries()) {
|
|
893
|
-
if (record.expiresAt < now) {
|
|
894
|
-
store.delete(key);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}, cleanupInterval);
|
|
898
|
-
return {
|
|
899
|
-
async hasBeenUsed(signature) {
|
|
900
|
-
const record = store.get(signature);
|
|
901
|
-
if (!record) return false;
|
|
902
|
-
if (record.expiresAt < Date.now()) {
|
|
903
|
-
store.delete(signature);
|
|
904
|
-
return false;
|
|
905
|
-
}
|
|
906
|
-
return true;
|
|
907
|
-
},
|
|
908
|
-
async markAsUsed(signature, resourceId, expiresAt) {
|
|
909
|
-
store.set(signature, {
|
|
910
|
-
resourceId,
|
|
911
|
-
usedAt: Date.now(),
|
|
912
|
-
expiresAt: expiresAt.getTime()
|
|
913
|
-
});
|
|
914
|
-
},
|
|
915
|
-
async getUsage(signature) {
|
|
916
|
-
const record = store.get(signature);
|
|
917
|
-
if (!record) return null;
|
|
918
|
-
if (record.expiresAt < Date.now()) {
|
|
919
|
-
store.delete(signature);
|
|
920
|
-
return null;
|
|
921
|
-
}
|
|
922
|
-
return {
|
|
923
|
-
signature,
|
|
924
|
-
resourceId: record.resourceId,
|
|
925
|
-
usedAt: new Date(record.usedAt),
|
|
926
|
-
expiresAt: new Date(record.expiresAt),
|
|
927
|
-
walletAddress: record.walletAddress
|
|
928
|
-
};
|
|
929
|
-
},
|
|
930
|
-
/** Stop cleanup timer (for graceful shutdown) */
|
|
931
|
-
close() {
|
|
932
|
-
clearInterval(cleanupTimer);
|
|
933
|
-
store.clear();
|
|
934
|
-
}
|
|
935
|
-
};
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// src/store/redis.ts
|
|
939
|
-
function createRedisStore(options) {
|
|
940
|
-
const { client, keyPrefix = "micropay:sig:" } = options;
|
|
941
|
-
const buildKey = (signature) => `${keyPrefix}${signature}`;
|
|
942
|
-
return {
|
|
943
|
-
async hasBeenUsed(signature) {
|
|
944
|
-
const exists = await client.exists(buildKey(signature));
|
|
945
|
-
return exists > 0;
|
|
946
|
-
},
|
|
947
|
-
async markAsUsed(signature, resourceId, expiresAt) {
|
|
948
|
-
const key = buildKey(signature);
|
|
949
|
-
const ttl = Math.max(1, Math.floor((expiresAt.getTime() - Date.now()) / 1e3));
|
|
950
|
-
const record = {
|
|
951
|
-
signature,
|
|
952
|
-
resourceId,
|
|
953
|
-
usedAt: /* @__PURE__ */ new Date(),
|
|
954
|
-
expiresAt
|
|
955
|
-
};
|
|
956
|
-
if (client.setex) {
|
|
957
|
-
await client.setex(key, ttl, JSON.stringify(record));
|
|
958
|
-
} else {
|
|
959
|
-
await client.set(key, JSON.stringify(record), { EX: ttl });
|
|
960
|
-
}
|
|
961
|
-
},
|
|
962
|
-
async getUsage(signature) {
|
|
963
|
-
const data = await client.get(buildKey(signature));
|
|
964
|
-
if (!data) return null;
|
|
965
|
-
try {
|
|
966
|
-
const record = JSON.parse(data);
|
|
967
|
-
return {
|
|
968
|
-
...record,
|
|
969
|
-
usedAt: new Date(record.usedAt),
|
|
970
|
-
expiresAt: new Date(record.expiresAt)
|
|
971
|
-
};
|
|
972
|
-
} catch {
|
|
973
|
-
return null;
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
// src/middleware/nextjs.ts
|
|
980
|
-
function matchesProtectedPath(path, patterns) {
|
|
981
|
-
for (const pattern of patterns) {
|
|
982
|
-
const regexPattern = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/{{DOUBLE_STAR}}/g, ".*");
|
|
983
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
984
|
-
if (regex.test(path)) {
|
|
985
|
-
return true;
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
return false;
|
|
989
|
-
}
|
|
990
|
-
async function checkPaywallAccess(path, sessionToken, config2) {
|
|
991
|
-
if (!matchesProtectedPath(path, config2.protectedPaths)) {
|
|
992
|
-
return { allowed: true };
|
|
993
|
-
}
|
|
994
|
-
if (!sessionToken) {
|
|
995
|
-
return {
|
|
996
|
-
allowed: false,
|
|
997
|
-
reason: "No session token",
|
|
998
|
-
requiresPayment: true
|
|
999
|
-
};
|
|
1000
|
-
}
|
|
1001
|
-
const validation = await validateSession(sessionToken, config2.sessionSecret);
|
|
1002
|
-
if (!validation.valid || !validation.session) {
|
|
1003
|
-
return {
|
|
1004
|
-
allowed: false,
|
|
1005
|
-
reason: validation.reason || "Invalid session",
|
|
1006
|
-
requiresPayment: true
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
return {
|
|
1010
|
-
allowed: true,
|
|
1011
|
-
session: validation.session
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
function createPaywallMiddleware(config2) {
|
|
1015
|
-
const { cookieName = "x402_session" } = config2;
|
|
1016
|
-
return async function middleware(request) {
|
|
1017
|
-
const url = new URL(request.url);
|
|
1018
|
-
const path = url.pathname;
|
|
1019
|
-
const cookieHeader = request.headers.get("cookie") || "";
|
|
1020
|
-
const cookies = Object.fromEntries(
|
|
1021
|
-
cookieHeader.split(";").map((c) => {
|
|
1022
|
-
const [key, ...vals] = c.trim().split("=");
|
|
1023
|
-
return [key, vals.join("=")];
|
|
1024
|
-
})
|
|
1025
|
-
);
|
|
1026
|
-
const sessionToken = cookies[cookieName];
|
|
1027
|
-
const result = await checkPaywallAccess(path, sessionToken, config2);
|
|
1028
|
-
if (!result.allowed && result.requiresPayment) {
|
|
1029
|
-
const headers = {
|
|
1030
|
-
"Content-Type": "application/json"
|
|
1031
|
-
};
|
|
1032
|
-
if (config2.paymentRequirement) {
|
|
1033
|
-
const requirement = typeof config2.paymentRequirement === "function" ? config2.paymentRequirement(path) : config2.paymentRequirement;
|
|
1034
|
-
headers["X-Payment-Required"] = encodePaymentRequirement(requirement);
|
|
1035
|
-
}
|
|
1036
|
-
const body = config2.custom402Response ? config2.custom402Response(path) : {
|
|
1037
|
-
error: "Payment Required",
|
|
1038
|
-
message: "This resource requires payment to access",
|
|
1039
|
-
x402Version: 1,
|
|
1040
|
-
path
|
|
1041
|
-
};
|
|
1042
|
-
return new Response(JSON.stringify(body), {
|
|
1043
|
-
status: 402,
|
|
1044
|
-
headers
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
return null;
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
function withPaywall(handler, options) {
|
|
1051
|
-
const { sessionSecret, cookieName = "x402_session", articleId } = options;
|
|
1052
|
-
return async function protectedHandler(request) {
|
|
1053
|
-
const cookieHeader = request.headers.get("cookie") || "";
|
|
1054
|
-
const cookies = Object.fromEntries(
|
|
1055
|
-
cookieHeader.split(";").map((c) => {
|
|
1056
|
-
const [key, ...vals] = c.trim().split("=");
|
|
1057
|
-
return [key, vals.join("=")];
|
|
1058
|
-
})
|
|
1059
|
-
);
|
|
1060
|
-
const sessionToken = cookies[cookieName];
|
|
1061
|
-
if (!sessionToken) {
|
|
1062
|
-
return new Response(
|
|
1063
|
-
JSON.stringify({ error: "Payment Required", message: "No session token" }),
|
|
1064
|
-
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
|
-
const validation = await validateSession(sessionToken, sessionSecret);
|
|
1068
|
-
if (!validation.valid || !validation.session) {
|
|
1069
|
-
return new Response(
|
|
1070
|
-
JSON.stringify({ error: "Payment Required", message: validation.reason }),
|
|
1071
|
-
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
|
-
if (articleId) {
|
|
1075
|
-
const { session } = validation;
|
|
1076
|
-
const hasAccess = session.siteWideUnlock || session.unlockedArticles.includes(articleId);
|
|
1077
|
-
if (!hasAccess) {
|
|
1078
|
-
return new Response(
|
|
1079
|
-
JSON.stringify({ error: "Payment Required", message: "Article not unlocked" }),
|
|
1080
|
-
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
1081
|
-
);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
return handler(request, validation.session);
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
9
|
+
// src/index.ts
|
|
1087
10
|
|
|
1088
|
-
// src/
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const jitterAmount = delay * 0.25;
|
|
1098
|
-
delay += Math.random() * jitterAmount * 2 - jitterAmount;
|
|
1099
|
-
}
|
|
1100
|
-
return Math.floor(delay);
|
|
1101
|
-
}
|
|
1102
|
-
async function withRetry(fn, options = {}) {
|
|
1103
|
-
const {
|
|
1104
|
-
maxAttempts = 3,
|
|
1105
|
-
baseDelay = 500,
|
|
1106
|
-
maxDelay = 1e4,
|
|
1107
|
-
jitter = true,
|
|
1108
|
-
retryOn = () => true
|
|
1109
|
-
} = options;
|
|
1110
|
-
let lastError;
|
|
1111
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1112
|
-
try {
|
|
1113
|
-
return await fn();
|
|
1114
|
-
} catch (error) {
|
|
1115
|
-
lastError = error;
|
|
1116
|
-
if (!retryOn(error)) {
|
|
1117
|
-
throw error;
|
|
1118
|
-
}
|
|
1119
|
-
if (attempt < maxAttempts - 1) {
|
|
1120
|
-
const delay = calculateDelay(attempt, {
|
|
1121
|
-
baseDelay,
|
|
1122
|
-
maxDelay,
|
|
1123
|
-
jitter
|
|
1124
|
-
});
|
|
1125
|
-
await sleep(delay);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
throw lastError;
|
|
1130
|
-
}
|
|
1131
|
-
function isRetryableRPCError(error) {
|
|
1132
|
-
if (error instanceof Error) {
|
|
1133
|
-
const message = error.message.toLowerCase();
|
|
1134
|
-
if (message.includes("429") || message.includes("rate limit")) {
|
|
1135
|
-
return true;
|
|
1136
|
-
}
|
|
1137
|
-
if (message.includes("timeout") || message.includes("econnreset")) {
|
|
1138
|
-
return true;
|
|
1139
|
-
}
|
|
1140
|
-
if (message.includes("503") || message.includes("502") || message.includes("500")) {
|
|
1141
|
-
return true;
|
|
1142
|
-
}
|
|
1143
|
-
if (message.includes("blockhash not found") || message.includes("slot skipped")) {
|
|
1144
|
-
return true;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
return false;
|
|
1148
|
-
}
|
|
11
|
+
// src/client/types.ts
|
|
12
|
+
var TOKEN_MINTS = {
|
|
13
|
+
/** USDC on mainnet */
|
|
14
|
+
USDC_MAINNET: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
15
|
+
/** USDC on devnet */
|
|
16
|
+
USDC_DEVNET: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
17
|
+
/** USDT on mainnet */
|
|
18
|
+
USDT_MAINNET: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
19
|
+
};
|
|
1149
20
|
|
|
1150
21
|
// src/client/payment.ts
|
|
1151
22
|
function buildSolanaPayUrl(params) {
|
|
@@ -1192,176 +63,95 @@ function createPaymentFlow(config2) {
|
|
|
1192
63
|
getFormattedAmount: () => {
|
|
1193
64
|
const symbol = asset === "native" ? "SOL" : asset === "usdc" ? "USDC" : asset === "usdt" ? "USDT" : "tokens";
|
|
1194
65
|
return `${naturalAmount.toFixed(decimals > 6 ? 4 : 2)} ${symbol}`;
|
|
1195
|
-
},
|
|
1196
|
-
/** Generate Solana Pay URL for QR codes */
|
|
1197
|
-
getSolanaPayUrl: (options = {}) => {
|
|
1198
|
-
return buildSolanaPayUrl({
|
|
1199
|
-
recipient: recipientWallet,
|
|
1200
|
-
amount: naturalAmount,
|
|
1201
|
-
splToken: mintAddress,
|
|
1202
|
-
label: options.label,
|
|
1203
|
-
reference: options.reference,
|
|
1204
|
-
message: memo
|
|
1205
|
-
});
|
|
1206
|
-
},
|
|
1207
|
-
/** Get the token mint address (undefined for native SOL) */
|
|
1208
|
-
getMintAddress: () => mintAddress,
|
|
1209
|
-
/** Check if this is a native SOL payment */
|
|
1210
|
-
isNativePayment: () => asset === "native",
|
|
1211
|
-
/** Get network information */
|
|
1212
|
-
getNetworkInfo: () => ({
|
|
1213
|
-
network,
|
|
1214
|
-
isMainnet: network === "mainnet-beta",
|
|
1215
|
-
explorerUrl: network === "mainnet-beta" ? "https://explorer.solana.com" : "https://explorer.solana.com?cluster=devnet"
|
|
1216
|
-
}),
|
|
1217
|
-
/** Build explorer URL for a transaction */
|
|
1218
|
-
getExplorerUrl: (signature) => {
|
|
1219
|
-
const baseUrl = "https://explorer.solana.com/tx";
|
|
1220
|
-
const cluster = network === "mainnet-beta" ? "" : "?cluster=devnet";
|
|
1221
|
-
return `${baseUrl}/${signature}${cluster}`;
|
|
1222
|
-
}
|
|
1223
|
-
};
|
|
1224
|
-
}
|
|
1225
|
-
function createPaymentReference() {
|
|
1226
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1227
|
-
return crypto.randomUUID();
|
|
1228
|
-
}
|
|
1229
|
-
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// src/pricing/index.ts
|
|
1233
|
-
var cachedPrice = null;
|
|
1234
|
-
var config = {};
|
|
1235
|
-
var lastProviderIndex = -1;
|
|
1236
|
-
function configurePricing(newConfig) {
|
|
1237
|
-
config = { ...config, ...newConfig };
|
|
1238
|
-
cachedPrice = null;
|
|
1239
|
-
}
|
|
1240
|
-
var PROVIDERS = [
|
|
1241
|
-
{
|
|
1242
|
-
name: "coincap",
|
|
1243
|
-
url: "https://api.coincap.io/v2/assets/solana",
|
|
1244
|
-
parse: (data) => parseFloat(data.data?.priceUsd || "0")
|
|
1245
|
-
},
|
|
1246
|
-
{
|
|
1247
|
-
name: "binance",
|
|
1248
|
-
url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
|
|
1249
|
-
parse: (data) => parseFloat(data.price || "0")
|
|
1250
|
-
},
|
|
1251
|
-
{
|
|
1252
|
-
name: "coingecko",
|
|
1253
|
-
url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
|
|
1254
|
-
parse: (data) => data.solana?.usd || 0
|
|
1255
|
-
},
|
|
1256
|
-
{
|
|
1257
|
-
name: "kraken",
|
|
1258
|
-
url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
|
|
1259
|
-
parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
|
|
1260
|
-
}
|
|
1261
|
-
];
|
|
1262
|
-
async function fetchFromProvider(provider, timeout) {
|
|
1263
|
-
const controller = new AbortController();
|
|
1264
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1265
|
-
try {
|
|
1266
|
-
const response = await fetch(provider.url, {
|
|
1267
|
-
headers: { "Accept": "application/json" },
|
|
1268
|
-
signal: controller.signal
|
|
1269
|
-
});
|
|
1270
|
-
if (!response.ok) {
|
|
1271
|
-
throw new Error(`HTTP ${response.status}`);
|
|
1272
|
-
}
|
|
1273
|
-
const data = await response.json();
|
|
1274
|
-
const price = provider.parse(data);
|
|
1275
|
-
if (!price || price <= 0) {
|
|
1276
|
-
throw new Error("Invalid price");
|
|
1277
|
-
}
|
|
1278
|
-
return price;
|
|
1279
|
-
} finally {
|
|
1280
|
-
clearTimeout(timeoutId);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
async function getSolPrice() {
|
|
1284
|
-
const cacheTTL = config.cacheTTL ?? 6e4;
|
|
1285
|
-
const timeout = config.timeout ?? 5e3;
|
|
1286
|
-
if (cachedPrice && Date.now() - cachedPrice.fetchedAt.getTime() < cacheTTL) {
|
|
1287
|
-
return cachedPrice;
|
|
1288
|
-
}
|
|
1289
|
-
if (config.customProvider) {
|
|
1290
|
-
try {
|
|
1291
|
-
const price = await config.customProvider();
|
|
1292
|
-
if (price > 0) {
|
|
1293
|
-
cachedPrice = {
|
|
1294
|
-
solPrice: price,
|
|
1295
|
-
fetchedAt: /* @__PURE__ */ new Date(),
|
|
1296
|
-
source: "custom"
|
|
1297
|
-
};
|
|
1298
|
-
return cachedPrice;
|
|
1299
|
-
}
|
|
1300
|
-
} catch {
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
for (let i = 0; i < PROVIDERS.length; i++) {
|
|
1304
|
-
const idx = (lastProviderIndex + 1 + i) % PROVIDERS.length;
|
|
1305
|
-
const provider = PROVIDERS[idx];
|
|
1306
|
-
try {
|
|
1307
|
-
const price = await fetchFromProvider(provider, timeout);
|
|
1308
|
-
lastProviderIndex = idx;
|
|
1309
|
-
cachedPrice = {
|
|
1310
|
-
solPrice: price,
|
|
1311
|
-
fetchedAt: /* @__PURE__ */ new Date(),
|
|
1312
|
-
source: provider.name
|
|
1313
|
-
};
|
|
1314
|
-
return cachedPrice;
|
|
1315
|
-
} catch {
|
|
1316
|
-
continue;
|
|
66
|
+
},
|
|
67
|
+
/** Generate Solana Pay URL for QR codes */
|
|
68
|
+
getSolanaPayUrl: (options = {}) => {
|
|
69
|
+
return buildSolanaPayUrl({
|
|
70
|
+
recipient: recipientWallet,
|
|
71
|
+
amount: naturalAmount,
|
|
72
|
+
splToken: mintAddress,
|
|
73
|
+
label: options.label,
|
|
74
|
+
reference: options.reference,
|
|
75
|
+
message: memo
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
/** Get the token mint address (undefined for native SOL) */
|
|
79
|
+
getMintAddress: () => mintAddress,
|
|
80
|
+
/** Check if this is a native SOL payment */
|
|
81
|
+
isNativePayment: () => asset === "native",
|
|
82
|
+
/** Get network information */
|
|
83
|
+
getNetworkInfo: () => ({
|
|
84
|
+
network,
|
|
85
|
+
isMainnet: network === "mainnet-beta",
|
|
86
|
+
explorerUrl: network === "mainnet-beta" ? "https://explorer.solana.com" : "https://explorer.solana.com?cluster=devnet"
|
|
87
|
+
}),
|
|
88
|
+
/** Build explorer URL for a transaction */
|
|
89
|
+
getExplorerUrl: (signature) => {
|
|
90
|
+
const baseUrl = "https://explorer.solana.com/tx";
|
|
91
|
+
const cluster = network === "mainnet-beta" ? "" : "?cluster=devnet";
|
|
92
|
+
return `${baseUrl}/${signature}${cluster}`;
|
|
1317
93
|
}
|
|
1318
|
-
}
|
|
1319
|
-
if (cachedPrice) {
|
|
1320
|
-
return {
|
|
1321
|
-
...cachedPrice,
|
|
1322
|
-
source: `${cachedPrice.source} (stale)`
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
throw new Error(
|
|
1326
|
-
"Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
|
|
1327
|
-
);
|
|
1328
|
-
}
|
|
1329
|
-
async function lamportsToUsd(lamports) {
|
|
1330
|
-
const { solPrice } = await getSolPrice();
|
|
1331
|
-
const sol = Number(lamports) / 1e9;
|
|
1332
|
-
return sol * solPrice;
|
|
94
|
+
};
|
|
1333
95
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
96
|
+
function createPaymentReference() {
|
|
97
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
98
|
+
return crypto.randomUUID();
|
|
99
|
+
}
|
|
100
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1338
101
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const
|
|
1343
|
-
|
|
102
|
+
var DEFAULT_COMPUTE_UNITS = 2e5;
|
|
103
|
+
var DEFAULT_MICRO_LAMPORTS = 1e3;
|
|
104
|
+
function createPriorityFeeInstructions(config2 = {}) {
|
|
105
|
+
const { enabled = false, microLamports, computeUnits } = config2;
|
|
106
|
+
if (!enabled) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const instructions = [];
|
|
110
|
+
const units = computeUnits ?? DEFAULT_COMPUTE_UNITS;
|
|
111
|
+
instructions.push(ComputeBudgetProgram.setComputeUnitLimit({ units }));
|
|
112
|
+
const price = microLamports ?? DEFAULT_MICRO_LAMPORTS;
|
|
113
|
+
instructions.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: price }));
|
|
114
|
+
return instructions;
|
|
1344
115
|
}
|
|
1345
|
-
function
|
|
1346
|
-
const
|
|
1347
|
-
|
|
116
|
+
async function buildVersionedTransaction(config2) {
|
|
117
|
+
const {
|
|
118
|
+
connection,
|
|
119
|
+
payer,
|
|
120
|
+
instructions,
|
|
121
|
+
priorityFee,
|
|
122
|
+
recentBlockhash
|
|
123
|
+
} = config2;
|
|
124
|
+
const priorityIxs = createPriorityFeeInstructions(priorityFee);
|
|
125
|
+
const allInstructions = [...priorityIxs, ...instructions];
|
|
126
|
+
let blockhash;
|
|
127
|
+
let lastValidBlockHeight;
|
|
128
|
+
if (recentBlockhash) {
|
|
129
|
+
blockhash = recentBlockhash;
|
|
130
|
+
const slot = await connection.getSlot();
|
|
131
|
+
lastValidBlockHeight = slot + 150;
|
|
132
|
+
} else {
|
|
133
|
+
const latestBlockhash = await connection.getLatestBlockhash("confirmed");
|
|
134
|
+
blockhash = latestBlockhash.blockhash;
|
|
135
|
+
lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
|
|
136
|
+
}
|
|
137
|
+
const message = new TransactionMessage({
|
|
138
|
+
payerKey: payer,
|
|
139
|
+
recentBlockhash: blockhash,
|
|
140
|
+
instructions: allInstructions
|
|
141
|
+
}).compileToV0Message([]);
|
|
142
|
+
const transaction = new VersionedTransaction(message);
|
|
1348
143
|
return {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
144
|
+
transaction,
|
|
145
|
+
blockhash,
|
|
146
|
+
lastValidBlockHeight
|
|
1352
147
|
};
|
|
1353
148
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
function getProviders() {
|
|
1359
|
-
return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
|
|
1360
|
-
}
|
|
1361
|
-
var WALLET_REGEX4 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
1362
|
-
function isValidWalletAddress2(address) {
|
|
149
|
+
|
|
150
|
+
// src/agent/agentPayment.ts
|
|
151
|
+
var WALLET_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
152
|
+
function isValidWalletAddress(address) {
|
|
1363
153
|
if (!address || typeof address !== "string") return false;
|
|
1364
|
-
return
|
|
154
|
+
return WALLET_REGEX.test(address);
|
|
1365
155
|
}
|
|
1366
156
|
async function executeAgentPayment(params) {
|
|
1367
157
|
const {
|
|
@@ -1372,7 +162,7 @@ async function executeAgentPayment(params) {
|
|
|
1372
162
|
priorityFee,
|
|
1373
163
|
confirmationTimeout = 6e4
|
|
1374
164
|
} = params;
|
|
1375
|
-
if (!
|
|
165
|
+
if (!isValidWalletAddress(recipientAddress)) {
|
|
1376
166
|
return {
|
|
1377
167
|
success: false,
|
|
1378
168
|
error: "Invalid recipient address format"
|
|
@@ -1479,23 +269,23 @@ function generateAgentKeypair() {
|
|
|
1479
269
|
};
|
|
1480
270
|
}
|
|
1481
271
|
var MAX_CREDITS = 1e3;
|
|
1482
|
-
var
|
|
1483
|
-
function
|
|
272
|
+
var MIN_SECRET_LENGTH = 32;
|
|
273
|
+
function getSecretKey(secret) {
|
|
1484
274
|
if (!secret || typeof secret !== "string") {
|
|
1485
275
|
throw new Error("Session secret is required");
|
|
1486
276
|
}
|
|
1487
|
-
if (secret.length <
|
|
1488
|
-
throw new Error(`Session secret must be at least ${
|
|
277
|
+
if (secret.length < MIN_SECRET_LENGTH) {
|
|
278
|
+
throw new Error(`Session secret must be at least ${MIN_SECRET_LENGTH} characters`);
|
|
1489
279
|
}
|
|
1490
280
|
return new TextEncoder().encode(secret);
|
|
1491
281
|
}
|
|
1492
|
-
function
|
|
282
|
+
function validateWalletAddress(address) {
|
|
1493
283
|
if (!address || typeof address !== "string") return false;
|
|
1494
284
|
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
1495
285
|
return base58Regex.test(address);
|
|
1496
286
|
}
|
|
1497
287
|
async function createCreditSession(walletAddress, purchaseId, config2) {
|
|
1498
|
-
if (!
|
|
288
|
+
if (!validateWalletAddress(walletAddress)) {
|
|
1499
289
|
throw new Error("Invalid wallet address format");
|
|
1500
290
|
}
|
|
1501
291
|
if (config2.initialCredits <= 0 || config2.initialCredits > MAX_CREDITS) {
|
|
@@ -1530,7 +320,7 @@ async function createCreditSession(walletAddress, purchaseId, config2) {
|
|
|
1530
320
|
iat: now,
|
|
1531
321
|
exp: expiresAt
|
|
1532
322
|
};
|
|
1533
|
-
const token = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config2.durationHours}h`).sign(
|
|
323
|
+
const token = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config2.durationHours}h`).sign(getSecretKey(config2.secret));
|
|
1534
324
|
return { token, session };
|
|
1535
325
|
}
|
|
1536
326
|
async function validateCreditSession(token, secret) {
|
|
@@ -1538,7 +328,7 @@ async function validateCreditSession(token, secret) {
|
|
|
1538
328
|
return { valid: false, reason: "Invalid token format" };
|
|
1539
329
|
}
|
|
1540
330
|
try {
|
|
1541
|
-
const { payload } = await jwtVerify(token,
|
|
331
|
+
const { payload } = await jwtVerify(token, getSecretKey(secret));
|
|
1542
332
|
const creditPayload = payload;
|
|
1543
333
|
if (!creditPayload.sub || !creditPayload.sid || !creditPayload.exp) {
|
|
1544
334
|
return { valid: false, reason: "Malformed session payload" };
|
|
@@ -1550,7 +340,7 @@ async function validateCreditSession(token, secret) {
|
|
|
1550
340
|
if (creditPayload.bundleExpiry && creditPayload.bundleExpiry < now) {
|
|
1551
341
|
return { valid: false, reason: "Bundle expired" };
|
|
1552
342
|
}
|
|
1553
|
-
if (!
|
|
343
|
+
if (!validateWalletAddress(creditPayload.sub)) {
|
|
1554
344
|
return { valid: false, reason: "Invalid session data" };
|
|
1555
345
|
}
|
|
1556
346
|
const session = {
|
|
@@ -1601,7 +391,7 @@ async function useCredit(token, secret, creditsToUse = 1) {
|
|
|
1601
391
|
iat: session.createdAt,
|
|
1602
392
|
exp: session.expiresAt
|
|
1603
393
|
};
|
|
1604
|
-
const newToken = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(
|
|
394
|
+
const newToken = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(getSecretKey(secret));
|
|
1605
395
|
return {
|
|
1606
396
|
success: true,
|
|
1607
397
|
remainingCredits: newCredits,
|
|
@@ -1629,7 +419,7 @@ async function addCredits(token, secret, creditsToAdd) {
|
|
|
1629
419
|
iat: session.createdAt,
|
|
1630
420
|
exp: session.expiresAt
|
|
1631
421
|
};
|
|
1632
|
-
const newToken = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(
|
|
422
|
+
const newToken = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(getSecretKey(secret));
|
|
1633
423
|
return {
|
|
1634
424
|
success: true,
|
|
1635
425
|
newToken,
|
|
@@ -1648,6 +438,136 @@ async function getRemainingCredits(token, secret) {
|
|
|
1648
438
|
};
|
|
1649
439
|
}
|
|
1650
440
|
|
|
1651
|
-
|
|
441
|
+
// src/pricing/index.ts
|
|
442
|
+
var cachedPrice = null;
|
|
443
|
+
var config = {};
|
|
444
|
+
var lastProviderIndex = -1;
|
|
445
|
+
function configurePricing(newConfig) {
|
|
446
|
+
config = { ...config, ...newConfig };
|
|
447
|
+
cachedPrice = null;
|
|
448
|
+
}
|
|
449
|
+
var PROVIDERS = [
|
|
450
|
+
{
|
|
451
|
+
name: "coincap",
|
|
452
|
+
url: "https://api.coincap.io/v2/assets/solana",
|
|
453
|
+
parse: (data) => parseFloat(data.data?.priceUsd || "0")
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
name: "binance",
|
|
457
|
+
url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
|
|
458
|
+
parse: (data) => parseFloat(data.price || "0")
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: "coingecko",
|
|
462
|
+
url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
|
|
463
|
+
parse: (data) => data.solana?.usd || 0
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "kraken",
|
|
467
|
+
url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
|
|
468
|
+
parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
|
|
469
|
+
}
|
|
470
|
+
];
|
|
471
|
+
async function fetchFromProvider(provider, timeout) {
|
|
472
|
+
const controller = new AbortController();
|
|
473
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
474
|
+
try {
|
|
475
|
+
const response = await fetch(provider.url, {
|
|
476
|
+
headers: { "Accept": "application/json" },
|
|
477
|
+
signal: controller.signal
|
|
478
|
+
});
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
throw new Error(`HTTP ${response.status}`);
|
|
481
|
+
}
|
|
482
|
+
const data = await response.json();
|
|
483
|
+
const price = provider.parse(data);
|
|
484
|
+
if (!price || price <= 0) {
|
|
485
|
+
throw new Error("Invalid price");
|
|
486
|
+
}
|
|
487
|
+
return price;
|
|
488
|
+
} finally {
|
|
489
|
+
clearTimeout(timeoutId);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async function getSolPrice() {
|
|
493
|
+
const cacheTTL = config.cacheTTL ?? 6e4;
|
|
494
|
+
const timeout = config.timeout ?? 5e3;
|
|
495
|
+
if (cachedPrice && Date.now() - cachedPrice.fetchedAt.getTime() < cacheTTL) {
|
|
496
|
+
return cachedPrice;
|
|
497
|
+
}
|
|
498
|
+
if (config.customProvider) {
|
|
499
|
+
try {
|
|
500
|
+
const price = await config.customProvider();
|
|
501
|
+
if (price > 0) {
|
|
502
|
+
cachedPrice = {
|
|
503
|
+
solPrice: price,
|
|
504
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
505
|
+
source: "custom"
|
|
506
|
+
};
|
|
507
|
+
return cachedPrice;
|
|
508
|
+
}
|
|
509
|
+
} catch {
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
for (let i = 0; i < PROVIDERS.length; i++) {
|
|
513
|
+
const idx = (lastProviderIndex + 1 + i) % PROVIDERS.length;
|
|
514
|
+
const provider = PROVIDERS[idx];
|
|
515
|
+
try {
|
|
516
|
+
const price = await fetchFromProvider(provider, timeout);
|
|
517
|
+
lastProviderIndex = idx;
|
|
518
|
+
cachedPrice = {
|
|
519
|
+
solPrice: price,
|
|
520
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
521
|
+
source: provider.name
|
|
522
|
+
};
|
|
523
|
+
return cachedPrice;
|
|
524
|
+
} catch {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (cachedPrice) {
|
|
529
|
+
return {
|
|
530
|
+
...cachedPrice,
|
|
531
|
+
source: `${cachedPrice.source} (stale)`
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
throw new Error(
|
|
535
|
+
"Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
async function lamportsToUsd(lamports) {
|
|
539
|
+
const { solPrice } = await getSolPrice();
|
|
540
|
+
const sol = Number(lamports) / 1e9;
|
|
541
|
+
return sol * solPrice;
|
|
542
|
+
}
|
|
543
|
+
async function usdToLamports(usd) {
|
|
544
|
+
const { solPrice } = await getSolPrice();
|
|
545
|
+
const sol = usd / solPrice;
|
|
546
|
+
return BigInt(Math.floor(sol * 1e9));
|
|
547
|
+
}
|
|
548
|
+
async function formatPriceDisplay(lamports) {
|
|
549
|
+
const { solPrice } = await getSolPrice();
|
|
550
|
+
const sol = Number(lamports) / 1e9;
|
|
551
|
+
const usd = sol * solPrice;
|
|
552
|
+
return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
|
|
553
|
+
}
|
|
554
|
+
function formatPriceSync(lamports, solPrice) {
|
|
555
|
+
const sol = Number(lamports) / 1e9;
|
|
556
|
+
const usd = sol * solPrice;
|
|
557
|
+
return {
|
|
558
|
+
sol,
|
|
559
|
+
usd,
|
|
560
|
+
formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function clearPriceCache() {
|
|
564
|
+
cachedPrice = null;
|
|
565
|
+
lastProviderIndex = -1;
|
|
566
|
+
}
|
|
567
|
+
function getProviders() {
|
|
568
|
+
return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export { addCredits, buildSolanaPayUrl, clearPriceCache, configurePricing, createCreditSession, createPaymentFlow, createPaymentReference, executeAgentPayment, formatPriceDisplay, formatPriceSync, generateAgentKeypair, getAgentBalance, getProviders, getRemainingCredits, getSolPrice, hasAgentSufficientBalance, keypairFromBase58, lamportsToUsd, usdToLamports, useCredit, validateCreditSession };
|
|
1652
572
|
//# sourceMappingURL=index.js.map
|
|
1653
573
|
//# sourceMappingURL=index.js.map
|