@alleyboss/micropay-solana-x402-paywall 2.3.0 → 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 +72 -116
- package/dist/agent/index.cjs +358 -0
- package/dist/agent/index.cjs.map +1 -0
- package/dist/agent/index.d.cts +221 -0
- package/dist/agent/index.d.ts +221 -0
- package/dist/agent/index.js +347 -0
- package/dist/agent/index.js.map +1 -0
- 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 +315 -1116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -10
- package/dist/index.d.ts +6 -10
- package/dist/index.js +283 -1074
- 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 -59
- 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/solana/index.cjs +0 -589
- package/dist/solana/index.cjs.map +0 -1
- package/dist/solana/index.d.cts +0 -240
- package/dist/solana/index.d.ts +0 -240
- 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.cjs
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var core = require('@x402/core');
|
|
4
|
+
var types = require('@x402/core/types');
|
|
5
|
+
var client = require('@x402/core/client');
|
|
6
|
+
var svm = require('@x402/svm');
|
|
3
7
|
var web3_js = require('@solana/web3.js');
|
|
4
8
|
var jose = require('jose');
|
|
5
9
|
var uuid = require('uuid');
|
|
6
10
|
|
|
7
|
-
// src/
|
|
11
|
+
// src/index.ts
|
|
12
|
+
|
|
13
|
+
// src/client/types.ts
|
|
8
14
|
var TOKEN_MINTS = {
|
|
9
15
|
/** USDC on mainnet */
|
|
10
16
|
USDC_MAINNET: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
@@ -13,471 +19,87 @@ var TOKEN_MINTS = {
|
|
|
13
19
|
/** USDT on mainnet */
|
|
14
20
|
USDT_MAINNET: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
15
21
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (rpcUrl.includes("tatum.io") && tatumApiKey && !rpcUrl.includes(tatumApiKey)) {
|
|
24
|
-
return rpcUrl.endsWith("/") ? `${rpcUrl}${tatumApiKey}` : `${rpcUrl}/${tatumApiKey}`;
|
|
25
|
-
}
|
|
26
|
-
return rpcUrl;
|
|
27
|
-
}
|
|
28
|
-
if (tatumApiKey) {
|
|
29
|
-
const baseUrl = network === "mainnet-beta" ? "https://solana-mainnet.gateway.tatum.io" : "https://solana-devnet.gateway.tatum.io";
|
|
30
|
-
return `${baseUrl}/${tatumApiKey}`;
|
|
31
|
-
}
|
|
32
|
-
return web3_js.clusterApiUrl(network);
|
|
33
|
-
}
|
|
34
|
-
function createConnection(rpcUrl) {
|
|
35
|
-
return new web3_js.Connection(rpcUrl, {
|
|
36
|
-
commitment: "confirmed",
|
|
37
|
-
confirmTransactionInitialTimeout: 6e4
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
function getConnection(config2) {
|
|
41
|
-
const { network } = config2;
|
|
42
|
-
if (cachedConnection && cachedNetwork === network) {
|
|
43
|
-
return cachedConnection;
|
|
44
|
-
}
|
|
45
|
-
const rpcUrl = buildRpcUrl(config2);
|
|
46
|
-
cachedConnection = createConnection(rpcUrl);
|
|
47
|
-
cachedNetwork = network;
|
|
48
|
-
cachedFallbackEnabled = config2.enableFallback ?? false;
|
|
49
|
-
cachedFallbacks = [];
|
|
50
|
-
if (cachedFallbackEnabled && config2.fallbackRpcUrls?.length) {
|
|
51
|
-
cachedFallbacks = config2.fallbackRpcUrls.map(createConnection);
|
|
52
|
-
}
|
|
53
|
-
return cachedConnection;
|
|
54
|
-
}
|
|
55
|
-
function getConnectionWithFallback(config2) {
|
|
56
|
-
const connection = getConnection(config2);
|
|
57
|
-
return {
|
|
58
|
-
connection,
|
|
59
|
-
fallbacks: cachedFallbacks,
|
|
60
|
-
fallbackEnabled: cachedFallbackEnabled
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
async function withFallback(config2, operation) {
|
|
64
|
-
const { connection, fallbacks, fallbackEnabled } = getConnectionWithFallback(config2);
|
|
65
|
-
try {
|
|
66
|
-
return await operation(connection);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
if (!fallbackEnabled || fallbacks.length === 0) {
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
if (!isRetryableError(error)) {
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
for (let i = 0; i < fallbacks.length; i++) {
|
|
75
|
-
try {
|
|
76
|
-
return await operation(fallbacks[i]);
|
|
77
|
-
} catch (fallbackError) {
|
|
78
|
-
if (i === fallbacks.length - 1) {
|
|
79
|
-
throw fallbackError;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function isRetryableError(error) {
|
|
87
|
-
if (error instanceof Error) {
|
|
88
|
-
const message = error.message.toLowerCase();
|
|
89
|
-
return message.includes("429") || message.includes("503") || message.includes("502") || message.includes("timeout") || message.includes("econnrefused") || message.includes("enotfound") || message.includes("rate limit");
|
|
90
|
-
}
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
function resetConnection() {
|
|
94
|
-
cachedConnection = null;
|
|
95
|
-
cachedNetwork = null;
|
|
96
|
-
}
|
|
97
|
-
function isMainnet(network) {
|
|
98
|
-
return network === "mainnet-beta";
|
|
99
|
-
}
|
|
100
|
-
function toX402Network(network) {
|
|
101
|
-
return network === "mainnet-beta" ? "solana-mainnet" : "solana-devnet";
|
|
102
|
-
}
|
|
103
|
-
var SIGNATURE_REGEX = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
104
|
-
var WALLET_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
105
|
-
function isValidSignature(signature) {
|
|
106
|
-
if (!signature || typeof signature !== "string") return false;
|
|
107
|
-
return SIGNATURE_REGEX.test(signature);
|
|
108
|
-
}
|
|
109
|
-
function isValidWalletAddress(address) {
|
|
110
|
-
if (!address || typeof address !== "string") return false;
|
|
111
|
-
return WALLET_REGEX.test(address);
|
|
112
|
-
}
|
|
113
|
-
function parseSOLTransfer(transaction, expectedRecipient) {
|
|
114
|
-
const instructions = transaction.transaction.message.instructions;
|
|
115
|
-
for (const ix of instructions) {
|
|
116
|
-
if ("parsed" in ix && ix.program === "system") {
|
|
117
|
-
const parsed = ix.parsed;
|
|
118
|
-
if (parsed.type === "transfer" && parsed.info.destination === expectedRecipient) {
|
|
119
|
-
return {
|
|
120
|
-
from: parsed.info.source,
|
|
121
|
-
to: parsed.info.destination,
|
|
122
|
-
amount: BigInt(parsed.info.lamports)
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (transaction.meta?.innerInstructions) {
|
|
128
|
-
for (const inner of transaction.meta.innerInstructions) {
|
|
129
|
-
for (const ix of inner.instructions) {
|
|
130
|
-
if ("parsed" in ix && ix.program === "system") {
|
|
131
|
-
const parsed = ix.parsed;
|
|
132
|
-
if (parsed.type === "transfer" && parsed.info.destination === expectedRecipient) {
|
|
133
|
-
return {
|
|
134
|
-
from: parsed.info.source,
|
|
135
|
-
to: parsed.info.destination,
|
|
136
|
-
amount: BigInt(parsed.info.lamports)
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
async function verifyPayment(params) {
|
|
146
|
-
const {
|
|
147
|
-
signature,
|
|
148
|
-
expectedRecipient,
|
|
149
|
-
expectedAmount,
|
|
150
|
-
maxAgeSeconds = 300,
|
|
151
|
-
clientConfig,
|
|
152
|
-
signatureStore
|
|
153
|
-
} = params;
|
|
154
|
-
if (!isValidSignature(signature)) {
|
|
155
|
-
return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
|
|
156
|
-
}
|
|
157
|
-
if (!isValidWalletAddress(expectedRecipient)) {
|
|
158
|
-
return { valid: false, confirmed: false, signature, error: "Invalid recipient address" };
|
|
159
|
-
}
|
|
160
|
-
if (expectedAmount <= 0n) {
|
|
161
|
-
return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
|
|
162
|
-
}
|
|
163
|
-
if (signatureStore) {
|
|
164
|
-
const isUsed = await signatureStore.hasBeenUsed(signature);
|
|
165
|
-
if (isUsed) {
|
|
166
|
-
return { valid: false, confirmed: true, signature, error: "Signature already used" };
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
|
|
170
|
-
const connection = getConnection(clientConfig);
|
|
171
|
-
try {
|
|
172
|
-
const transaction = await connection.getParsedTransaction(signature, {
|
|
173
|
-
commitment: "confirmed",
|
|
174
|
-
maxSupportedTransactionVersion: 0
|
|
175
|
-
});
|
|
176
|
-
if (!transaction) {
|
|
177
|
-
return { valid: false, confirmed: false, signature, error: "Transaction not found" };
|
|
178
|
-
}
|
|
179
|
-
if (transaction.meta?.err) {
|
|
180
|
-
return {
|
|
181
|
-
valid: false,
|
|
182
|
-
confirmed: true,
|
|
183
|
-
signature,
|
|
184
|
-
error: "Transaction failed on-chain"
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
if (transaction.blockTime) {
|
|
188
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
189
|
-
if (now - transaction.blockTime > effectiveMaxAge) {
|
|
190
|
-
return { valid: false, confirmed: true, signature, error: "Transaction too old" };
|
|
191
|
-
}
|
|
192
|
-
if (transaction.blockTime > now + 60) {
|
|
193
|
-
return { valid: false, confirmed: true, signature, error: "Invalid transaction time" };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
const transferDetails = parseSOLTransfer(transaction, expectedRecipient);
|
|
197
|
-
if (!transferDetails) {
|
|
198
|
-
return {
|
|
199
|
-
valid: false,
|
|
200
|
-
confirmed: true,
|
|
201
|
-
signature,
|
|
202
|
-
error: "No valid SOL transfer to recipient found"
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
if (transferDetails.amount < expectedAmount) {
|
|
206
|
-
return {
|
|
207
|
-
valid: false,
|
|
208
|
-
confirmed: true,
|
|
209
|
-
signature,
|
|
210
|
-
from: transferDetails.from,
|
|
211
|
-
to: transferDetails.to,
|
|
212
|
-
amount: transferDetails.amount,
|
|
213
|
-
error: "Insufficient payment amount"
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
return {
|
|
217
|
-
valid: true,
|
|
218
|
-
confirmed: true,
|
|
219
|
-
signature,
|
|
220
|
-
from: transferDetails.from,
|
|
221
|
-
to: transferDetails.to,
|
|
222
|
-
amount: transferDetails.amount,
|
|
223
|
-
blockTime: transaction.blockTime ?? void 0,
|
|
224
|
-
slot: transaction.slot
|
|
225
|
-
};
|
|
226
|
-
} catch (error) {
|
|
227
|
-
return {
|
|
228
|
-
valid: false,
|
|
229
|
-
confirmed: false,
|
|
230
|
-
signature,
|
|
231
|
-
error: "Verification failed"
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
async function waitForConfirmation(signature, clientConfig) {
|
|
236
|
-
if (!isValidSignature(signature)) {
|
|
237
|
-
return { confirmed: false, error: "Invalid signature format" };
|
|
22
|
+
|
|
23
|
+
// src/client/payment.ts
|
|
24
|
+
function buildSolanaPayUrl(params) {
|
|
25
|
+
const { recipient, amount, splToken, reference, label, message } = params;
|
|
26
|
+
const url = new URL(`solana:${recipient}`);
|
|
27
|
+
if (amount !== void 0) {
|
|
28
|
+
url.searchParams.set("amount", amount.toString());
|
|
238
29
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
242
|
-
if (confirmation.value.err) {
|
|
243
|
-
return { confirmed: false, error: "Transaction failed" };
|
|
244
|
-
}
|
|
245
|
-
return { confirmed: true, slot: confirmation.context?.slot };
|
|
246
|
-
} catch {
|
|
247
|
-
return { confirmed: false, error: "Confirmation timeout" };
|
|
30
|
+
if (splToken) {
|
|
31
|
+
url.searchParams.set("spl-token", splToken);
|
|
248
32
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (!isValidWalletAddress(walletAddress)) {
|
|
252
|
-
return [];
|
|
33
|
+
if (reference) {
|
|
34
|
+
url.searchParams.set("reference", reference);
|
|
253
35
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
const pubkey = new web3_js.PublicKey(walletAddress);
|
|
258
|
-
const signatures = await connection.getSignaturesForAddress(pubkey, { limit: safeLimit });
|
|
259
|
-
return signatures.map((sig) => ({
|
|
260
|
-
signature: sig.signature,
|
|
261
|
-
blockTime: sig.blockTime ?? void 0,
|
|
262
|
-
slot: sig.slot
|
|
263
|
-
}));
|
|
264
|
-
} catch {
|
|
265
|
-
return [];
|
|
36
|
+
if (label) {
|
|
37
|
+
url.searchParams.set("label", label);
|
|
266
38
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return Number(lamports) / web3_js.LAMPORTS_PER_SOL;
|
|
270
|
-
}
|
|
271
|
-
function solToLamports(sol) {
|
|
272
|
-
if (!Number.isFinite(sol) || sol < 0) {
|
|
273
|
-
throw new Error("Invalid SOL amount");
|
|
39
|
+
if (message) {
|
|
40
|
+
url.searchParams.set("message", message);
|
|
274
41
|
}
|
|
275
|
-
return
|
|
42
|
+
return url.toString();
|
|
276
43
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
44
|
+
function createPaymentFlow(config2) {
|
|
45
|
+
const { network, recipientWallet, amount, asset = "native", memo } = config2;
|
|
46
|
+
let decimals = 9;
|
|
47
|
+
let mintAddress;
|
|
281
48
|
if (asset === "usdc") {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (asset === "usdt") {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (typeof asset === "object" && "mint" in asset) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
function getTokenDecimals(asset) {
|
|
293
|
-
if (asset === "native") return 9;
|
|
294
|
-
if (asset === "usdc" || asset === "usdt") return 6;
|
|
295
|
-
if (typeof asset === "object" && "decimals" in asset) {
|
|
296
|
-
return asset.decimals ?? 6;
|
|
297
|
-
}
|
|
298
|
-
return 6;
|
|
299
|
-
}
|
|
300
|
-
function parseSPLTransfer(transaction, expectedRecipient, expectedMint) {
|
|
301
|
-
const instructions = transaction.transaction.message.instructions;
|
|
302
|
-
for (const ix of instructions) {
|
|
303
|
-
if ("parsed" in ix && (ix.program === "spl-token" || ix.program === "spl-token-2022")) {
|
|
304
|
-
const parsed = ix.parsed;
|
|
305
|
-
if (parsed.type === "transfer" || parsed.type === "transferChecked") {
|
|
306
|
-
const amount = parsed.info.amount || parsed.info.tokenAmount?.amount;
|
|
307
|
-
if (amount && parsed.info.destination) {
|
|
308
|
-
return {
|
|
309
|
-
from: parsed.info.authority || parsed.info.source || "",
|
|
310
|
-
to: parsed.info.destination,
|
|
311
|
-
amount: BigInt(amount),
|
|
312
|
-
mint: parsed.info.mint || expectedMint
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
if (transaction.meta?.innerInstructions) {
|
|
319
|
-
for (const inner of transaction.meta.innerInstructions) {
|
|
320
|
-
for (const ix of inner.instructions) {
|
|
321
|
-
if ("parsed" in ix && (ix.program === "spl-token" || ix.program === "spl-token-2022")) {
|
|
322
|
-
const parsed = ix.parsed;
|
|
323
|
-
if (parsed.type === "transfer" || parsed.type === "transferChecked") {
|
|
324
|
-
const amount = parsed.info.amount || parsed.info.tokenAmount?.amount;
|
|
325
|
-
if (amount) {
|
|
326
|
-
return {
|
|
327
|
-
from: parsed.info.authority || parsed.info.source || "",
|
|
328
|
-
to: parsed.info.destination || "",
|
|
329
|
-
amount: BigInt(amount),
|
|
330
|
-
mint: parsed.info.mint || expectedMint
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
49
|
+
decimals = 6;
|
|
50
|
+
mintAddress = network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
51
|
+
} else if (asset === "usdt") {
|
|
52
|
+
decimals = 6;
|
|
53
|
+
mintAddress = TOKEN_MINTS.USDT_MAINNET;
|
|
54
|
+
} else if (typeof asset === "object" && "mint" in asset) {
|
|
55
|
+
decimals = asset.decimals ?? 6;
|
|
56
|
+
mintAddress = asset.mint;
|
|
337
57
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
58
|
+
const naturalAmount = Number(amount) / Math.pow(10, decimals);
|
|
59
|
+
return {
|
|
60
|
+
/** Get the payment configuration */
|
|
61
|
+
getConfig: () => ({ ...config2 }),
|
|
62
|
+
/** Get amount in natural display units (e.g., 0.01 SOL) */
|
|
63
|
+
getDisplayAmount: () => naturalAmount,
|
|
64
|
+
/** Get amount formatted with symbol */
|
|
65
|
+
getFormattedAmount: () => {
|
|
66
|
+
const symbol = asset === "native" ? "SOL" : asset === "usdc" ? "USDC" : asset === "usdt" ? "USDT" : "tokens";
|
|
67
|
+
return `${naturalAmount.toFixed(decimals > 6 ? 4 : 2)} ${symbol}`;
|
|
68
|
+
},
|
|
69
|
+
/** Generate Solana Pay URL for QR codes */
|
|
70
|
+
getSolanaPayUrl: (options = {}) => {
|
|
71
|
+
return buildSolanaPayUrl({
|
|
72
|
+
recipient: recipientWallet,
|
|
73
|
+
amount: naturalAmount,
|
|
74
|
+
splToken: mintAddress,
|
|
75
|
+
label: options.label,
|
|
76
|
+
reference: options.reference,
|
|
77
|
+
message: memo
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
/** Get the token mint address (undefined for native SOL) */
|
|
81
|
+
getMintAddress: () => mintAddress,
|
|
82
|
+
/** Check if this is a native SOL payment */
|
|
83
|
+
isNativePayment: () => asset === "native",
|
|
84
|
+
/** Get network information */
|
|
85
|
+
getNetworkInfo: () => ({
|
|
86
|
+
network,
|
|
87
|
+
isMainnet: network === "mainnet-beta",
|
|
88
|
+
explorerUrl: network === "mainnet-beta" ? "https://explorer.solana.com" : "https://explorer.solana.com?cluster=devnet"
|
|
89
|
+
}),
|
|
90
|
+
/** Build explorer URL for a transaction */
|
|
91
|
+
getExplorerUrl: (signature) => {
|
|
92
|
+
const baseUrl = "https://explorer.solana.com/tx";
|
|
93
|
+
const cluster = network === "mainnet-beta" ? "" : "?cluster=devnet";
|
|
94
|
+
return `${baseUrl}/${signature}${cluster}`;
|
|
359
95
|
}
|
|
360
|
-
}
|
|
361
|
-
return null;
|
|
96
|
+
};
|
|
362
97
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
expectedRecipient,
|
|
367
|
-
expectedAmount,
|
|
368
|
-
asset,
|
|
369
|
-
clientConfig,
|
|
370
|
-
maxAgeSeconds = 300,
|
|
371
|
-
signatureStore
|
|
372
|
-
} = params;
|
|
373
|
-
if (signatureStore) {
|
|
374
|
-
const isUsed = await signatureStore.hasBeenUsed(signature);
|
|
375
|
-
if (isUsed) {
|
|
376
|
-
return { valid: false, confirmed: true, signature, error: "Signature already used" };
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
if (!SIGNATURE_REGEX2.test(signature)) {
|
|
380
|
-
return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
|
|
381
|
-
}
|
|
382
|
-
if (!WALLET_REGEX2.test(expectedRecipient)) {
|
|
383
|
-
return { valid: false, confirmed: false, signature, error: "Invalid recipient address" };
|
|
384
|
-
}
|
|
385
|
-
const mintAddress = resolveMintAddress(asset, clientConfig.network);
|
|
386
|
-
if (!mintAddress) {
|
|
387
|
-
return { valid: false, confirmed: false, signature, error: "Invalid asset configuration" };
|
|
388
|
-
}
|
|
389
|
-
if (expectedAmount <= 0n) {
|
|
390
|
-
return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
|
|
391
|
-
}
|
|
392
|
-
const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
|
|
393
|
-
const connection = getConnection(clientConfig);
|
|
394
|
-
try {
|
|
395
|
-
const transaction = await connection.getParsedTransaction(signature, {
|
|
396
|
-
commitment: "confirmed",
|
|
397
|
-
maxSupportedTransactionVersion: 0
|
|
398
|
-
});
|
|
399
|
-
if (!transaction) {
|
|
400
|
-
return { valid: false, confirmed: false, signature, error: "Transaction not found" };
|
|
401
|
-
}
|
|
402
|
-
if (transaction.meta?.err) {
|
|
403
|
-
return { valid: false, confirmed: true, signature, error: "Transaction failed on-chain" };
|
|
404
|
-
}
|
|
405
|
-
if (transaction.blockTime) {
|
|
406
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
407
|
-
if (now - transaction.blockTime > effectiveMaxAge) {
|
|
408
|
-
return { valid: false, confirmed: true, signature, error: "Transaction too old" };
|
|
409
|
-
}
|
|
410
|
-
if (transaction.blockTime > now + 60) {
|
|
411
|
-
return { valid: false, confirmed: true, signature, error: "Invalid transaction time" };
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
const transfer = parseSPLTransfer(transaction, expectedRecipient, mintAddress);
|
|
415
|
-
if (!transfer) {
|
|
416
|
-
return {
|
|
417
|
-
valid: false,
|
|
418
|
-
confirmed: true,
|
|
419
|
-
signature,
|
|
420
|
-
error: "No valid token transfer to recipient found"
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
if (transfer.to) {
|
|
424
|
-
try {
|
|
425
|
-
const destinationInfo = await connection.getParsedAccountInfo(new web3_js.PublicKey(transfer.to));
|
|
426
|
-
const owner = destinationInfo.value?.data?.parsed?.info?.owner;
|
|
427
|
-
if (owner && owner !== expectedRecipient) {
|
|
428
|
-
return {
|
|
429
|
-
valid: false,
|
|
430
|
-
confirmed: true,
|
|
431
|
-
signature,
|
|
432
|
-
error: "Recipient mismatch: Token account not owned by merchant"
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
} catch (e) {
|
|
436
|
-
return {
|
|
437
|
-
valid: false,
|
|
438
|
-
confirmed: true,
|
|
439
|
-
signature,
|
|
440
|
-
error: "Could not verify token account owner"
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
if (transfer.mint !== mintAddress) {
|
|
445
|
-
return {
|
|
446
|
-
valid: false,
|
|
447
|
-
confirmed: true,
|
|
448
|
-
signature,
|
|
449
|
-
error: "Token mint mismatch"
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
if (transfer.amount < expectedAmount) {
|
|
453
|
-
return {
|
|
454
|
-
valid: false,
|
|
455
|
-
confirmed: true,
|
|
456
|
-
signature,
|
|
457
|
-
from: transfer.from,
|
|
458
|
-
to: transfer.to,
|
|
459
|
-
mint: transfer.mint,
|
|
460
|
-
amount: transfer.amount,
|
|
461
|
-
error: "Insufficient payment amount"
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
return {
|
|
465
|
-
valid: true,
|
|
466
|
-
confirmed: true,
|
|
467
|
-
signature,
|
|
468
|
-
from: transfer.from,
|
|
469
|
-
to: transfer.to,
|
|
470
|
-
mint: transfer.mint,
|
|
471
|
-
amount: transfer.amount,
|
|
472
|
-
blockTime: transaction.blockTime ?? void 0,
|
|
473
|
-
slot: transaction.slot
|
|
474
|
-
};
|
|
475
|
-
} catch {
|
|
476
|
-
return { valid: false, confirmed: false, signature, error: "Verification failed" };
|
|
98
|
+
function createPaymentReference() {
|
|
99
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
100
|
+
return crypto.randomUUID();
|
|
477
101
|
}
|
|
478
|
-
}
|
|
479
|
-
function isNativeAsset(asset) {
|
|
480
|
-
return asset === "native";
|
|
102
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
481
103
|
}
|
|
482
104
|
var DEFAULT_COMPUTE_UNITS = 2e5;
|
|
483
105
|
var DEFAULT_MICRO_LAMPORTS = 1e3;
|
|
@@ -493,33 +115,11 @@ function createPriorityFeeInstructions(config2 = {}) {
|
|
|
493
115
|
instructions.push(web3_js.ComputeBudgetProgram.setComputeUnitPrice({ microLamports: price }));
|
|
494
116
|
return instructions;
|
|
495
117
|
}
|
|
496
|
-
async function estimatePriorityFee(connection, accounts = []) {
|
|
497
|
-
try {
|
|
498
|
-
const fees = await connection.getRecentPrioritizationFees({
|
|
499
|
-
lockedWritableAccounts: accounts
|
|
500
|
-
});
|
|
501
|
-
if (fees.length === 0) {
|
|
502
|
-
return DEFAULT_MICRO_LAMPORTS;
|
|
503
|
-
}
|
|
504
|
-
const sortedFees = fees.map((f) => f.prioritizationFee).filter((f) => f > 0).sort((a, b) => a - b);
|
|
505
|
-
if (sortedFees.length === 0) {
|
|
506
|
-
return DEFAULT_MICRO_LAMPORTS;
|
|
507
|
-
}
|
|
508
|
-
const medianIndex = Math.floor(sortedFees.length / 2);
|
|
509
|
-
return sortedFees[medianIndex];
|
|
510
|
-
} catch {
|
|
511
|
-
return DEFAULT_MICRO_LAMPORTS;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
function calculatePriorityFeeCost(microLamportsPerCU, computeUnits) {
|
|
515
|
-
return Math.ceil(microLamportsPerCU * computeUnits / 1e6);
|
|
516
|
-
}
|
|
517
118
|
async function buildVersionedTransaction(config2) {
|
|
518
119
|
const {
|
|
519
120
|
connection,
|
|
520
121
|
payer,
|
|
521
122
|
instructions,
|
|
522
|
-
lookupTables = [],
|
|
523
123
|
priorityFee,
|
|
524
124
|
recentBlockhash
|
|
525
125
|
} = config2;
|
|
@@ -540,7 +140,7 @@ async function buildVersionedTransaction(config2) {
|
|
|
540
140
|
payerKey: payer,
|
|
541
141
|
recentBlockhash: blockhash,
|
|
542
142
|
instructions: allInstructions
|
|
543
|
-
}).compileToV0Message(
|
|
143
|
+
}).compileToV0Message([]);
|
|
544
144
|
const transaction = new web3_js.VersionedTransaction(message);
|
|
545
145
|
return {
|
|
546
146
|
transaction,
|
|
@@ -548,20 +148,129 @@ async function buildVersionedTransaction(config2) {
|
|
|
548
148
|
lastValidBlockHeight
|
|
549
149
|
};
|
|
550
150
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
151
|
+
|
|
152
|
+
// src/agent/agentPayment.ts
|
|
153
|
+
var WALLET_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
154
|
+
function isValidWalletAddress(address) {
|
|
155
|
+
if (!address || typeof address !== "string") return false;
|
|
156
|
+
return WALLET_REGEX.test(address);
|
|
157
|
+
}
|
|
158
|
+
async function executeAgentPayment(params) {
|
|
159
|
+
const {
|
|
160
|
+
connection,
|
|
161
|
+
agentKeypair,
|
|
162
|
+
recipientAddress,
|
|
163
|
+
amountLamports,
|
|
164
|
+
priorityFee,
|
|
165
|
+
confirmationTimeout = 6e4
|
|
166
|
+
} = params;
|
|
167
|
+
if (!isValidWalletAddress(recipientAddress)) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: "Invalid recipient address format"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (amountLamports <= 0n) {
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
error: "Amount must be greater than 0"
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const recipientPubkey = new web3_js.PublicKey(recipientAddress);
|
|
181
|
+
const transferInstruction = web3_js.SystemProgram.transfer({
|
|
182
|
+
fromPubkey: agentKeypair.publicKey,
|
|
183
|
+
toPubkey: recipientPubkey,
|
|
184
|
+
lamports: amountLamports
|
|
185
|
+
});
|
|
186
|
+
const { transaction, lastValidBlockHeight } = await buildVersionedTransaction({
|
|
187
|
+
connection,
|
|
188
|
+
payer: agentKeypair.publicKey,
|
|
189
|
+
instructions: [transferInstruction],
|
|
190
|
+
priorityFee
|
|
191
|
+
});
|
|
192
|
+
transaction.sign([agentKeypair]);
|
|
193
|
+
const signature = await connection.sendTransaction(transaction, {
|
|
194
|
+
maxRetries: 3,
|
|
195
|
+
skipPreflight: false
|
|
196
|
+
});
|
|
197
|
+
const confirmationPromise = connection.confirmTransaction(
|
|
198
|
+
{
|
|
199
|
+
signature,
|
|
200
|
+
lastValidBlockHeight,
|
|
201
|
+
blockhash: transaction.message.recentBlockhash
|
|
202
|
+
},
|
|
203
|
+
"confirmed"
|
|
204
|
+
);
|
|
205
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
206
|
+
setTimeout(() => reject(new Error("Confirmation timeout")), confirmationTimeout);
|
|
207
|
+
});
|
|
208
|
+
const confirmation = await Promise.race([confirmationPromise, timeoutPromise]);
|
|
209
|
+
if (confirmation.value.err) {
|
|
210
|
+
return {
|
|
211
|
+
success: false,
|
|
212
|
+
signature,
|
|
213
|
+
error: "Transaction failed on-chain"
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const txDetails = await connection.getTransaction(signature, {
|
|
217
|
+
commitment: "confirmed",
|
|
218
|
+
maxSupportedTransactionVersion: 0
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
success: true,
|
|
222
|
+
signature,
|
|
223
|
+
confirmedAt: txDetails?.blockTime ?? Math.floor(Date.now() / 1e3),
|
|
224
|
+
slot: txDetails?.slot ?? confirmation.context.slot,
|
|
225
|
+
amountLamports,
|
|
226
|
+
amountSol: Number(amountLamports) / web3_js.LAMPORTS_PER_SOL
|
|
227
|
+
};
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
error: errorMessage
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function getAgentBalance(connection, agentKeypair) {
|
|
237
|
+
const balance = await connection.getBalance(agentKeypair.publicKey);
|
|
238
|
+
return {
|
|
239
|
+
balance: BigInt(balance),
|
|
240
|
+
balanceSol: balance / web3_js.LAMPORTS_PER_SOL
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async function hasAgentSufficientBalance(connection, agentKeypair, requiredLamports) {
|
|
244
|
+
const { balance } = await getAgentBalance(connection, agentKeypair);
|
|
245
|
+
const totalRequired = requiredLamports + 10000n;
|
|
246
|
+
return {
|
|
247
|
+
sufficient: balance >= totalRequired,
|
|
248
|
+
balance,
|
|
249
|
+
required: totalRequired
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function keypairFromBase58(base58Secret) {
|
|
253
|
+
const bytes = Buffer.from(base58Secret, "base64");
|
|
254
|
+
if (bytes.length !== 64) {
|
|
255
|
+
const parts = base58Secret.split(",").map((n) => parseInt(n.trim(), 10));
|
|
256
|
+
if (parts.length === 64) {
|
|
257
|
+
return web3_js.Keypair.fromSecretKey(Uint8Array.from(parts));
|
|
557
258
|
}
|
|
259
|
+
throw new Error("Invalid secret key format. Expected base58 string or comma-separated bytes.");
|
|
558
260
|
}
|
|
559
|
-
return
|
|
261
|
+
return web3_js.Keypair.fromSecretKey(bytes);
|
|
560
262
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
263
|
+
function generateAgentKeypair() {
|
|
264
|
+
const keypair = web3_js.Keypair.generate();
|
|
265
|
+
const secretBytes = Array.from(keypair.secretKey);
|
|
266
|
+
return {
|
|
267
|
+
keypair,
|
|
268
|
+
secretBase58: secretBytes.join(","),
|
|
269
|
+
// Comma-separated for easy storage
|
|
270
|
+
publicKey: keypair.publicKey.toBase58()
|
|
271
|
+
};
|
|
563
272
|
}
|
|
564
|
-
var
|
|
273
|
+
var MAX_CREDITS = 1e3;
|
|
565
274
|
var MIN_SECRET_LENGTH = 32;
|
|
566
275
|
function getSecretKey(secret) {
|
|
567
276
|
if (!secret || typeof secret !== "string") {
|
|
@@ -577,659 +286,159 @@ function validateWalletAddress(address) {
|
|
|
577
286
|
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
578
287
|
return base58Regex.test(address);
|
|
579
288
|
}
|
|
580
|
-
function
|
|
581
|
-
if (!articleId || typeof articleId !== "string") return false;
|
|
582
|
-
if (articleId.length > 128) return false;
|
|
583
|
-
const safeIdRegex = /^[a-zA-Z0-9_-]+$/;
|
|
584
|
-
return safeIdRegex.test(articleId);
|
|
585
|
-
}
|
|
586
|
-
async function createSession(walletAddress, articleId, config2, siteWide = false) {
|
|
289
|
+
async function createCreditSession(walletAddress, purchaseId, config2) {
|
|
587
290
|
if (!validateWalletAddress(walletAddress)) {
|
|
588
291
|
throw new Error("Invalid wallet address format");
|
|
589
292
|
}
|
|
590
|
-
if (
|
|
591
|
-
throw new Error(
|
|
293
|
+
if (config2.initialCredits <= 0 || config2.initialCredits > MAX_CREDITS) {
|
|
294
|
+
throw new Error(`Credits must be between 1 and ${MAX_CREDITS}`);
|
|
592
295
|
}
|
|
593
|
-
if (!config2.durationHours || config2.durationHours <= 0 || config2.durationHours >
|
|
594
|
-
throw new Error("Session duration must be between 1 and
|
|
296
|
+
if (!config2.durationHours || config2.durationHours <= 0 || config2.durationHours > 8760) {
|
|
297
|
+
throw new Error("Session duration must be between 1 and 8760 hours (1 year)");
|
|
595
298
|
}
|
|
596
299
|
const sessionId = uuid.v4();
|
|
597
300
|
const now = Math.floor(Date.now() / 1e3);
|
|
598
301
|
const expiresAt = now + config2.durationHours * 3600;
|
|
302
|
+
const bundleExpiry = config2.bundleExpiryHours ? now + config2.bundleExpiryHours * 3600 : expiresAt;
|
|
599
303
|
const session = {
|
|
600
304
|
id: sessionId,
|
|
601
305
|
walletAddress,
|
|
602
|
-
unlockedArticles: [
|
|
603
|
-
siteWideUnlock:
|
|
306
|
+
unlockedArticles: [purchaseId],
|
|
307
|
+
siteWideUnlock: false,
|
|
604
308
|
createdAt: now,
|
|
605
|
-
expiresAt
|
|
309
|
+
expiresAt,
|
|
310
|
+
credits: config2.initialCredits,
|
|
311
|
+
bundleExpiry,
|
|
312
|
+
bundleType: config2.bundleType
|
|
606
313
|
};
|
|
607
314
|
const payload = {
|
|
608
315
|
sub: walletAddress,
|
|
609
316
|
sid: sessionId,
|
|
610
317
|
articles: session.unlockedArticles,
|
|
611
|
-
siteWide:
|
|
318
|
+
siteWide: false,
|
|
319
|
+
credits: config2.initialCredits,
|
|
320
|
+
bundleExpiry,
|
|
321
|
+
bundleType: config2.bundleType,
|
|
612
322
|
iat: now,
|
|
613
323
|
exp: expiresAt
|
|
614
324
|
};
|
|
615
325
|
const token = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config2.durationHours}h`).sign(getSecretKey(config2.secret));
|
|
616
326
|
return { token, session };
|
|
617
327
|
}
|
|
618
|
-
async function
|
|
328
|
+
async function validateCreditSession(token, secret) {
|
|
619
329
|
if (!token || typeof token !== "string") {
|
|
620
330
|
return { valid: false, reason: "Invalid token format" };
|
|
621
331
|
}
|
|
622
332
|
try {
|
|
623
333
|
const { payload } = await jose.jwtVerify(token, getSecretKey(secret));
|
|
624
|
-
const
|
|
625
|
-
if (!
|
|
334
|
+
const creditPayload = payload;
|
|
335
|
+
if (!creditPayload.sub || !creditPayload.sid || !creditPayload.exp) {
|
|
626
336
|
return { valid: false, reason: "Malformed session payload" };
|
|
627
337
|
}
|
|
628
338
|
const now = Math.floor(Date.now() / 1e3);
|
|
629
|
-
if (
|
|
339
|
+
if (creditPayload.exp < now) {
|
|
630
340
|
return { valid: false, reason: "Session expired" };
|
|
631
341
|
}
|
|
632
|
-
if (
|
|
342
|
+
if (creditPayload.bundleExpiry && creditPayload.bundleExpiry < now) {
|
|
343
|
+
return { valid: false, reason: "Bundle expired" };
|
|
344
|
+
}
|
|
345
|
+
if (!validateWalletAddress(creditPayload.sub)) {
|
|
633
346
|
return { valid: false, reason: "Invalid session data" };
|
|
634
347
|
}
|
|
635
348
|
const session = {
|
|
636
|
-
id:
|
|
637
|
-
walletAddress:
|
|
638
|
-
unlockedArticles: Array.isArray(
|
|
639
|
-
siteWideUnlock: Boolean(
|
|
640
|
-
createdAt:
|
|
641
|
-
expiresAt:
|
|
349
|
+
id: creditPayload.sid,
|
|
350
|
+
walletAddress: creditPayload.sub,
|
|
351
|
+
unlockedArticles: Array.isArray(creditPayload.articles) ? creditPayload.articles : [],
|
|
352
|
+
siteWideUnlock: Boolean(creditPayload.siteWide),
|
|
353
|
+
createdAt: creditPayload.iat ?? 0,
|
|
354
|
+
expiresAt: creditPayload.exp,
|
|
355
|
+
credits: creditPayload.credits ?? 0,
|
|
356
|
+
bundleExpiry: creditPayload.bundleExpiry,
|
|
357
|
+
bundleType: creditPayload.bundleType
|
|
642
358
|
};
|
|
643
359
|
return { valid: true, session };
|
|
644
|
-
} catch
|
|
360
|
+
} catch {
|
|
645
361
|
return { valid: false, reason: "Invalid session" };
|
|
646
362
|
}
|
|
647
363
|
}
|
|
648
|
-
async function
|
|
649
|
-
if (
|
|
650
|
-
return
|
|
364
|
+
async function useCredit(token, secret, creditsToUse = 1) {
|
|
365
|
+
if (creditsToUse <= 0) {
|
|
366
|
+
return { success: false, remainingCredits: 0, error: "Invalid credit amount" };
|
|
651
367
|
}
|
|
652
|
-
const validation = await
|
|
368
|
+
const validation = await validateCreditSession(token, secret);
|
|
653
369
|
if (!validation.valid || !validation.session) {
|
|
654
|
-
return
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
remainingCredits: 0,
|
|
373
|
+
error: validation.reason || "Invalid session"
|
|
374
|
+
};
|
|
655
375
|
}
|
|
656
376
|
const session = validation.session;
|
|
657
|
-
if (session.
|
|
658
|
-
return {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
377
|
+
if (session.credits < creditsToUse) {
|
|
378
|
+
return {
|
|
379
|
+
success: false,
|
|
380
|
+
remainingCredits: session.credits,
|
|
381
|
+
error: "Insufficient credits"
|
|
382
|
+
};
|
|
662
383
|
}
|
|
663
|
-
const
|
|
384
|
+
const newCredits = session.credits - creditsToUse;
|
|
664
385
|
const payload = {
|
|
665
386
|
sub: session.walletAddress,
|
|
666
387
|
sid: session.id,
|
|
667
|
-
articles:
|
|
388
|
+
articles: session.unlockedArticles,
|
|
668
389
|
siteWide: session.siteWideUnlock,
|
|
390
|
+
credits: newCredits,
|
|
391
|
+
bundleExpiry: session.bundleExpiry,
|
|
392
|
+
bundleType: session.bundleType,
|
|
669
393
|
iat: session.createdAt,
|
|
670
394
|
exp: session.expiresAt
|
|
671
395
|
};
|
|
672
396
|
const newToken = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(getSecretKey(secret));
|
|
673
397
|
return {
|
|
674
|
-
|
|
675
|
-
|
|
398
|
+
success: true,
|
|
399
|
+
remainingCredits: newCredits,
|
|
400
|
+
newToken
|
|
676
401
|
};
|
|
677
402
|
}
|
|
678
|
-
async function
|
|
679
|
-
if (
|
|
680
|
-
return false;
|
|
403
|
+
async function addCredits(token, secret, creditsToAdd) {
|
|
404
|
+
if (creditsToAdd <= 0 || creditsToAdd > MAX_CREDITS) {
|
|
405
|
+
return { success: false, error: "Invalid credit amount" };
|
|
681
406
|
}
|
|
682
|
-
const validation = await
|
|
407
|
+
const validation = await validateCreditSession(token, secret);
|
|
683
408
|
if (!validation.valid || !validation.session) {
|
|
684
|
-
return false;
|
|
685
|
-
}
|
|
686
|
-
if (validation.session.siteWideUnlock) {
|
|
687
|
-
return true;
|
|
688
|
-
}
|
|
689
|
-
return validation.session.unlockedArticles.includes(articleId);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// src/x402/config.ts
|
|
693
|
-
var WALLET_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
694
|
-
function sanitizeDisplayString(str, maxLength = 200) {
|
|
695
|
-
if (!str || typeof str !== "string") return "";
|
|
696
|
-
return str.slice(0, maxLength).replace(/[<>"'&]/g, "");
|
|
697
|
-
}
|
|
698
|
-
function isValidUrl(url) {
|
|
699
|
-
try {
|
|
700
|
-
const parsed = new URL(url);
|
|
701
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
702
|
-
} catch {
|
|
703
|
-
return false;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
function buildPaymentRequirement(params) {
|
|
707
|
-
if (!WALLET_REGEX3.test(params.creatorWallet)) {
|
|
708
|
-
throw new Error("Invalid creator wallet address");
|
|
709
|
-
}
|
|
710
|
-
if (params.priceInLamports <= 0n) {
|
|
711
|
-
throw new Error("Price must be positive");
|
|
712
|
-
}
|
|
713
|
-
if (!isValidUrl(params.resourceUrl)) {
|
|
714
|
-
throw new Error("Invalid resource URL");
|
|
715
|
-
}
|
|
716
|
-
if (params.network !== "devnet" && params.network !== "mainnet-beta") {
|
|
717
|
-
throw new Error("Invalid network");
|
|
718
|
-
}
|
|
719
|
-
const timeout = params.maxTimeoutSeconds ?? 300;
|
|
720
|
-
if (timeout < 60 || timeout > 3600) {
|
|
721
|
-
throw new Error("Timeout must be between 60 and 3600 seconds");
|
|
722
|
-
}
|
|
723
|
-
const x402Network = toX402Network(params.network);
|
|
724
|
-
const safeTitle = sanitizeDisplayString(params.articleTitle, 200);
|
|
725
|
-
const safeArticleId = sanitizeDisplayString(params.articleId, 128);
|
|
726
|
-
return {
|
|
727
|
-
scheme: "exact",
|
|
728
|
-
network: x402Network,
|
|
729
|
-
maxAmountRequired: params.priceInLamports.toString(),
|
|
730
|
-
resource: params.resourceUrl,
|
|
731
|
-
description: `Unlock: ${safeTitle}`,
|
|
732
|
-
mimeType: "text/html",
|
|
733
|
-
payTo: params.creatorWallet,
|
|
734
|
-
maxTimeoutSeconds: timeout,
|
|
735
|
-
asset: "native",
|
|
736
|
-
extra: {
|
|
737
|
-
name: safeTitle,
|
|
738
|
-
articleId: safeArticleId
|
|
739
|
-
}
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
function encodePaymentRequired(requirement) {
|
|
743
|
-
return Buffer.from(JSON.stringify(requirement)).toString("base64");
|
|
744
|
-
}
|
|
745
|
-
function decodePaymentRequired(encoded) {
|
|
746
|
-
if (!encoded || typeof encoded !== "string") {
|
|
747
|
-
throw new Error("Invalid encoded requirement");
|
|
748
|
-
}
|
|
749
|
-
if (encoded.length > 1e4) {
|
|
750
|
-
throw new Error("Encoded requirement too large");
|
|
751
|
-
}
|
|
752
|
-
try {
|
|
753
|
-
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
|
|
754
|
-
return JSON.parse(decoded);
|
|
755
|
-
} catch {
|
|
756
|
-
throw new Error("Failed to decode payment requirement");
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
var X402_HEADERS = {
|
|
760
|
-
PAYMENT_REQUIRED: "X-Payment-Required",
|
|
761
|
-
PAYMENT: "X-Payment",
|
|
762
|
-
PAYMENT_RESPONSE: "X-Payment-Response"
|
|
763
|
-
};
|
|
764
|
-
function create402ResponseBody(requirement) {
|
|
765
|
-
const assetStr = typeof requirement.asset === "string" ? requirement.asset : requirement.asset.mint;
|
|
766
|
-
return {
|
|
767
|
-
error: "Payment Required",
|
|
768
|
-
message: requirement.description,
|
|
769
|
-
price: {
|
|
770
|
-
amount: requirement.maxAmountRequired,
|
|
771
|
-
asset: assetStr,
|
|
772
|
-
network: requirement.network
|
|
773
|
-
}
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
function create402Headers(requirement) {
|
|
777
|
-
const encoded = encodePaymentRequired(requirement);
|
|
778
|
-
return {
|
|
779
|
-
"Content-Type": "application/json",
|
|
780
|
-
[X402_HEADERS.PAYMENT_REQUIRED]: encoded,
|
|
781
|
-
"Access-Control-Expose-Headers": X402_HEADERS.PAYMENT_REQUIRED
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// src/x402/verification.ts
|
|
786
|
-
var SIGNATURE_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
787
|
-
async function verifyX402Payment(payload, requirement, clientConfig) {
|
|
788
|
-
if (!payload || typeof payload !== "object") {
|
|
789
|
-
return { valid: false, invalidReason: "Invalid payload" };
|
|
790
|
-
}
|
|
791
|
-
const signature = payload.payload?.signature;
|
|
792
|
-
if (!signature || typeof signature !== "string") {
|
|
793
|
-
return { valid: false, invalidReason: "Missing transaction signature" };
|
|
794
|
-
}
|
|
795
|
-
if (!SIGNATURE_REGEX3.test(signature)) {
|
|
796
|
-
return { valid: false, invalidReason: "Invalid signature format" };
|
|
797
|
-
}
|
|
798
|
-
if (payload.x402Version !== 1) {
|
|
799
|
-
return { valid: false, invalidReason: "Unsupported x402 version" };
|
|
800
|
-
}
|
|
801
|
-
if (payload.scheme !== "exact") {
|
|
802
|
-
return { valid: false, invalidReason: "Unsupported payment scheme" };
|
|
803
|
-
}
|
|
804
|
-
if (payload.network !== requirement.network) {
|
|
805
|
-
return {
|
|
806
|
-
valid: false,
|
|
807
|
-
invalidReason: `Network mismatch: expected ${requirement.network}`
|
|
808
|
-
};
|
|
409
|
+
return { success: false, error: validation.reason || "Invalid session" };
|
|
809
410
|
}
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
const verification = await verifyPayment({
|
|
824
|
-
signature,
|
|
825
|
-
expectedRecipient: requirement.payTo,
|
|
826
|
-
expectedAmount,
|
|
827
|
-
maxAgeSeconds: requirement.maxTimeoutSeconds,
|
|
828
|
-
clientConfig
|
|
829
|
-
});
|
|
830
|
-
if (!verification.valid) {
|
|
831
|
-
return {
|
|
832
|
-
valid: false,
|
|
833
|
-
invalidReason: verification.error || "Transaction verification failed"
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
return {
|
|
837
|
-
valid: true,
|
|
838
|
-
settled: verification.confirmed,
|
|
839
|
-
from: verification.from,
|
|
840
|
-
transaction: {
|
|
841
|
-
signature: verification.signature,
|
|
842
|
-
blockTime: verification.blockTime,
|
|
843
|
-
slot: verification.slot
|
|
844
|
-
}
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
function parsePaymentHeader(header) {
|
|
848
|
-
if (!header || typeof header !== "string") {
|
|
849
|
-
return null;
|
|
850
|
-
}
|
|
851
|
-
if (header.length > 1e4) {
|
|
852
|
-
return null;
|
|
853
|
-
}
|
|
854
|
-
try {
|
|
855
|
-
const decoded = Buffer.from(header, "base64").toString("utf-8");
|
|
856
|
-
const parsed = JSON.parse(decoded);
|
|
857
|
-
if (!parsed || typeof parsed !== "object") {
|
|
858
|
-
return null;
|
|
859
|
-
}
|
|
860
|
-
return parsed;
|
|
861
|
-
} catch {
|
|
862
|
-
return null;
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
function encodePaymentRequirement(requirement) {
|
|
866
|
-
return Buffer.from(JSON.stringify(requirement)).toString("base64");
|
|
867
|
-
}
|
|
868
|
-
function encodePaymentResponse(response) {
|
|
869
|
-
return Buffer.from(JSON.stringify(response)).toString("base64");
|
|
870
|
-
}
|
|
871
|
-
function create402Response(requirement, body) {
|
|
872
|
-
const headers = new Headers({
|
|
873
|
-
"Content-Type": "application/json",
|
|
874
|
-
"X-Payment-Required": encodePaymentRequirement(requirement)
|
|
875
|
-
});
|
|
876
|
-
const responseBody = body || {
|
|
877
|
-
error: "Payment Required",
|
|
878
|
-
message: "This resource requires payment to access",
|
|
879
|
-
x402Version: 1,
|
|
880
|
-
accepts: [requirement]
|
|
881
|
-
};
|
|
882
|
-
return new Response(JSON.stringify(responseBody), {
|
|
883
|
-
status: 402,
|
|
884
|
-
headers
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// src/store/memory.ts
|
|
889
|
-
function createMemoryStore(options = {}) {
|
|
890
|
-
const { cleanupInterval = 6e4 } = options;
|
|
891
|
-
const store = /* @__PURE__ */ new Map();
|
|
892
|
-
const cleanupTimer = setInterval(() => {
|
|
893
|
-
const now = Date.now();
|
|
894
|
-
for (const [key, record] of store.entries()) {
|
|
895
|
-
if (record.expiresAt < now) {
|
|
896
|
-
store.delete(key);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
}, cleanupInterval);
|
|
900
|
-
return {
|
|
901
|
-
async hasBeenUsed(signature) {
|
|
902
|
-
const record = store.get(signature);
|
|
903
|
-
if (!record) return false;
|
|
904
|
-
if (record.expiresAt < Date.now()) {
|
|
905
|
-
store.delete(signature);
|
|
906
|
-
return false;
|
|
907
|
-
}
|
|
908
|
-
return true;
|
|
909
|
-
},
|
|
910
|
-
async markAsUsed(signature, resourceId, expiresAt) {
|
|
911
|
-
store.set(signature, {
|
|
912
|
-
resourceId,
|
|
913
|
-
usedAt: Date.now(),
|
|
914
|
-
expiresAt: expiresAt.getTime()
|
|
915
|
-
});
|
|
916
|
-
},
|
|
917
|
-
async getUsage(signature) {
|
|
918
|
-
const record = store.get(signature);
|
|
919
|
-
if (!record) return null;
|
|
920
|
-
if (record.expiresAt < Date.now()) {
|
|
921
|
-
store.delete(signature);
|
|
922
|
-
return null;
|
|
923
|
-
}
|
|
924
|
-
return {
|
|
925
|
-
signature,
|
|
926
|
-
resourceId: record.resourceId,
|
|
927
|
-
usedAt: new Date(record.usedAt),
|
|
928
|
-
expiresAt: new Date(record.expiresAt),
|
|
929
|
-
walletAddress: record.walletAddress
|
|
930
|
-
};
|
|
931
|
-
},
|
|
932
|
-
/** Stop cleanup timer (for graceful shutdown) */
|
|
933
|
-
close() {
|
|
934
|
-
clearInterval(cleanupTimer);
|
|
935
|
-
store.clear();
|
|
936
|
-
}
|
|
411
|
+
const session = validation.session;
|
|
412
|
+
const newCredits = Math.min(session.credits + creditsToAdd, MAX_CREDITS);
|
|
413
|
+
const payload = {
|
|
414
|
+
sub: session.walletAddress,
|
|
415
|
+
sid: session.id,
|
|
416
|
+
articles: session.unlockedArticles,
|
|
417
|
+
siteWide: session.siteWideUnlock,
|
|
418
|
+
credits: newCredits,
|
|
419
|
+
bundleExpiry: session.bundleExpiry,
|
|
420
|
+
bundleType: session.bundleType,
|
|
421
|
+
iat: session.createdAt,
|
|
422
|
+
exp: session.expiresAt
|
|
937
423
|
};
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// src/store/redis.ts
|
|
941
|
-
function createRedisStore(options) {
|
|
942
|
-
const { client, keyPrefix = "micropay:sig:" } = options;
|
|
943
|
-
const buildKey = (signature) => `${keyPrefix}${signature}`;
|
|
424
|
+
const newToken = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(getSecretKey(secret));
|
|
944
425
|
return {
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
},
|
|
949
|
-
async markAsUsed(signature, resourceId, expiresAt) {
|
|
950
|
-
const key = buildKey(signature);
|
|
951
|
-
const ttl = Math.max(1, Math.floor((expiresAt.getTime() - Date.now()) / 1e3));
|
|
952
|
-
const record = {
|
|
953
|
-
signature,
|
|
954
|
-
resourceId,
|
|
955
|
-
usedAt: /* @__PURE__ */ new Date(),
|
|
956
|
-
expiresAt
|
|
957
|
-
};
|
|
958
|
-
if (client.setex) {
|
|
959
|
-
await client.setex(key, ttl, JSON.stringify(record));
|
|
960
|
-
} else {
|
|
961
|
-
await client.set(key, JSON.stringify(record), { EX: ttl });
|
|
962
|
-
}
|
|
963
|
-
},
|
|
964
|
-
async getUsage(signature) {
|
|
965
|
-
const data = await client.get(buildKey(signature));
|
|
966
|
-
if (!data) return null;
|
|
967
|
-
try {
|
|
968
|
-
const record = JSON.parse(data);
|
|
969
|
-
return {
|
|
970
|
-
...record,
|
|
971
|
-
usedAt: new Date(record.usedAt),
|
|
972
|
-
expiresAt: new Date(record.expiresAt)
|
|
973
|
-
};
|
|
974
|
-
} catch {
|
|
975
|
-
return null;
|
|
976
|
-
}
|
|
977
|
-
}
|
|
426
|
+
success: true,
|
|
427
|
+
newToken,
|
|
428
|
+
totalCredits: newCredits
|
|
978
429
|
};
|
|
979
430
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
function matchesProtectedPath(path, patterns) {
|
|
983
|
-
for (const pattern of patterns) {
|
|
984
|
-
const regexPattern = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/{{DOUBLE_STAR}}/g, ".*");
|
|
985
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
986
|
-
if (regex.test(path)) {
|
|
987
|
-
return true;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
return false;
|
|
991
|
-
}
|
|
992
|
-
async function checkPaywallAccess(path, sessionToken, config2) {
|
|
993
|
-
if (!matchesProtectedPath(path, config2.protectedPaths)) {
|
|
994
|
-
return { allowed: true };
|
|
995
|
-
}
|
|
996
|
-
if (!sessionToken) {
|
|
997
|
-
return {
|
|
998
|
-
allowed: false,
|
|
999
|
-
reason: "No session token",
|
|
1000
|
-
requiresPayment: true
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
const validation = await validateSession(sessionToken, config2.sessionSecret);
|
|
431
|
+
async function getRemainingCredits(token, secret) {
|
|
432
|
+
const validation = await validateCreditSession(token, secret);
|
|
1004
433
|
if (!validation.valid || !validation.session) {
|
|
1005
|
-
return {
|
|
1006
|
-
allowed: false,
|
|
1007
|
-
reason: validation.reason || "Invalid session",
|
|
1008
|
-
requiresPayment: true
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
return {
|
|
1012
|
-
allowed: true,
|
|
1013
|
-
session: validation.session
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
function createPaywallMiddleware(config2) {
|
|
1017
|
-
const { cookieName = "x402_session" } = config2;
|
|
1018
|
-
return async function middleware(request) {
|
|
1019
|
-
const url = new URL(request.url);
|
|
1020
|
-
const path = url.pathname;
|
|
1021
|
-
const cookieHeader = request.headers.get("cookie") || "";
|
|
1022
|
-
const cookies = Object.fromEntries(
|
|
1023
|
-
cookieHeader.split(";").map((c) => {
|
|
1024
|
-
const [key, ...vals] = c.trim().split("=");
|
|
1025
|
-
return [key, vals.join("=")];
|
|
1026
|
-
})
|
|
1027
|
-
);
|
|
1028
|
-
const sessionToken = cookies[cookieName];
|
|
1029
|
-
const result = await checkPaywallAccess(path, sessionToken, config2);
|
|
1030
|
-
if (!result.allowed && result.requiresPayment) {
|
|
1031
|
-
const headers = {
|
|
1032
|
-
"Content-Type": "application/json"
|
|
1033
|
-
};
|
|
1034
|
-
if (config2.paymentRequirement) {
|
|
1035
|
-
const requirement = typeof config2.paymentRequirement === "function" ? config2.paymentRequirement(path) : config2.paymentRequirement;
|
|
1036
|
-
headers["X-Payment-Required"] = encodePaymentRequirement(requirement);
|
|
1037
|
-
}
|
|
1038
|
-
const body = config2.custom402Response ? config2.custom402Response(path) : {
|
|
1039
|
-
error: "Payment Required",
|
|
1040
|
-
message: "This resource requires payment to access",
|
|
1041
|
-
x402Version: 1,
|
|
1042
|
-
path
|
|
1043
|
-
};
|
|
1044
|
-
return new Response(JSON.stringify(body), {
|
|
1045
|
-
status: 402,
|
|
1046
|
-
headers
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
return null;
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
function withPaywall(handler, options) {
|
|
1053
|
-
const { sessionSecret, cookieName = "x402_session", articleId } = options;
|
|
1054
|
-
return async function protectedHandler(request) {
|
|
1055
|
-
const cookieHeader = request.headers.get("cookie") || "";
|
|
1056
|
-
const cookies = Object.fromEntries(
|
|
1057
|
-
cookieHeader.split(";").map((c) => {
|
|
1058
|
-
const [key, ...vals] = c.trim().split("=");
|
|
1059
|
-
return [key, vals.join("=")];
|
|
1060
|
-
})
|
|
1061
|
-
);
|
|
1062
|
-
const sessionToken = cookies[cookieName];
|
|
1063
|
-
if (!sessionToken) {
|
|
1064
|
-
return new Response(
|
|
1065
|
-
JSON.stringify({ error: "Payment Required", message: "No session token" }),
|
|
1066
|
-
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
1067
|
-
);
|
|
1068
|
-
}
|
|
1069
|
-
const validation = await validateSession(sessionToken, sessionSecret);
|
|
1070
|
-
if (!validation.valid || !validation.session) {
|
|
1071
|
-
return new Response(
|
|
1072
|
-
JSON.stringify({ error: "Payment Required", message: validation.reason }),
|
|
1073
|
-
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
1074
|
-
);
|
|
1075
|
-
}
|
|
1076
|
-
if (articleId) {
|
|
1077
|
-
const { session } = validation;
|
|
1078
|
-
const hasAccess = session.siteWideUnlock || session.unlockedArticles.includes(articleId);
|
|
1079
|
-
if (!hasAccess) {
|
|
1080
|
-
return new Response(
|
|
1081
|
-
JSON.stringify({ error: "Payment Required", message: "Article not unlocked" }),
|
|
1082
|
-
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
return handler(request, validation.session);
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// src/utils/retry.ts
|
|
1091
|
-
function sleep(ms) {
|
|
1092
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1093
|
-
}
|
|
1094
|
-
function calculateDelay(attempt, options) {
|
|
1095
|
-
const { baseDelay, maxDelay, jitter } = options;
|
|
1096
|
-
let delay = baseDelay * Math.pow(2, attempt);
|
|
1097
|
-
delay = Math.min(delay, maxDelay);
|
|
1098
|
-
if (jitter) {
|
|
1099
|
-
const jitterAmount = delay * 0.25;
|
|
1100
|
-
delay += Math.random() * jitterAmount * 2 - jitterAmount;
|
|
1101
|
-
}
|
|
1102
|
-
return Math.floor(delay);
|
|
1103
|
-
}
|
|
1104
|
-
async function withRetry(fn, options = {}) {
|
|
1105
|
-
const {
|
|
1106
|
-
maxAttempts = 3,
|
|
1107
|
-
baseDelay = 500,
|
|
1108
|
-
maxDelay = 1e4,
|
|
1109
|
-
jitter = true,
|
|
1110
|
-
retryOn = () => true
|
|
1111
|
-
} = options;
|
|
1112
|
-
let lastError;
|
|
1113
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1114
|
-
try {
|
|
1115
|
-
return await fn();
|
|
1116
|
-
} catch (error) {
|
|
1117
|
-
lastError = error;
|
|
1118
|
-
if (!retryOn(error)) {
|
|
1119
|
-
throw error;
|
|
1120
|
-
}
|
|
1121
|
-
if (attempt < maxAttempts - 1) {
|
|
1122
|
-
const delay = calculateDelay(attempt, {
|
|
1123
|
-
baseDelay,
|
|
1124
|
-
maxDelay,
|
|
1125
|
-
jitter
|
|
1126
|
-
});
|
|
1127
|
-
await sleep(delay);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
throw lastError;
|
|
1132
|
-
}
|
|
1133
|
-
function isRetryableRPCError(error) {
|
|
1134
|
-
if (error instanceof Error) {
|
|
1135
|
-
const message = error.message.toLowerCase();
|
|
1136
|
-
if (message.includes("429") || message.includes("rate limit")) {
|
|
1137
|
-
return true;
|
|
1138
|
-
}
|
|
1139
|
-
if (message.includes("timeout") || message.includes("econnreset")) {
|
|
1140
|
-
return true;
|
|
1141
|
-
}
|
|
1142
|
-
if (message.includes("503") || message.includes("502") || message.includes("500")) {
|
|
1143
|
-
return true;
|
|
1144
|
-
}
|
|
1145
|
-
if (message.includes("blockhash not found") || message.includes("slot skipped")) {
|
|
1146
|
-
return true;
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
return false;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// src/client/payment.ts
|
|
1153
|
-
function buildSolanaPayUrl(params) {
|
|
1154
|
-
const { recipient, amount, splToken, reference, label, message } = params;
|
|
1155
|
-
const url = new URL(`solana:${recipient}`);
|
|
1156
|
-
if (amount !== void 0) {
|
|
1157
|
-
url.searchParams.set("amount", amount.toString());
|
|
1158
|
-
}
|
|
1159
|
-
if (splToken) {
|
|
1160
|
-
url.searchParams.set("spl-token", splToken);
|
|
1161
|
-
}
|
|
1162
|
-
if (reference) {
|
|
1163
|
-
url.searchParams.set("reference", reference);
|
|
1164
|
-
}
|
|
1165
|
-
if (label) {
|
|
1166
|
-
url.searchParams.set("label", label);
|
|
1167
|
-
}
|
|
1168
|
-
if (message) {
|
|
1169
|
-
url.searchParams.set("message", message);
|
|
1170
|
-
}
|
|
1171
|
-
return url.toString();
|
|
1172
|
-
}
|
|
1173
|
-
function createPaymentFlow(config2) {
|
|
1174
|
-
const { network, recipientWallet, amount, asset = "native", memo } = config2;
|
|
1175
|
-
let decimals = 9;
|
|
1176
|
-
let mintAddress;
|
|
1177
|
-
if (asset === "usdc") {
|
|
1178
|
-
decimals = 6;
|
|
1179
|
-
mintAddress = network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
1180
|
-
} else if (asset === "usdt") {
|
|
1181
|
-
decimals = 6;
|
|
1182
|
-
mintAddress = TOKEN_MINTS.USDT_MAINNET;
|
|
1183
|
-
} else if (typeof asset === "object" && "mint" in asset) {
|
|
1184
|
-
decimals = asset.decimals ?? 6;
|
|
1185
|
-
mintAddress = asset.mint;
|
|
434
|
+
return { credits: 0, valid: false };
|
|
1186
435
|
}
|
|
1187
|
-
const naturalAmount = Number(amount) / Math.pow(10, decimals);
|
|
1188
436
|
return {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
getDisplayAmount: () => naturalAmount,
|
|
1193
|
-
/** Get amount formatted with symbol */
|
|
1194
|
-
getFormattedAmount: () => {
|
|
1195
|
-
const symbol = asset === "native" ? "SOL" : asset === "usdc" ? "USDC" : asset === "usdt" ? "USDT" : "tokens";
|
|
1196
|
-
return `${naturalAmount.toFixed(decimals > 6 ? 4 : 2)} ${symbol}`;
|
|
1197
|
-
},
|
|
1198
|
-
/** Generate Solana Pay URL for QR codes */
|
|
1199
|
-
getSolanaPayUrl: (options = {}) => {
|
|
1200
|
-
return buildSolanaPayUrl({
|
|
1201
|
-
recipient: recipientWallet,
|
|
1202
|
-
amount: naturalAmount,
|
|
1203
|
-
splToken: mintAddress,
|
|
1204
|
-
label: options.label,
|
|
1205
|
-
reference: options.reference,
|
|
1206
|
-
message: memo
|
|
1207
|
-
});
|
|
1208
|
-
},
|
|
1209
|
-
/** Get the token mint address (undefined for native SOL) */
|
|
1210
|
-
getMintAddress: () => mintAddress,
|
|
1211
|
-
/** Check if this is a native SOL payment */
|
|
1212
|
-
isNativePayment: () => asset === "native",
|
|
1213
|
-
/** Get network information */
|
|
1214
|
-
getNetworkInfo: () => ({
|
|
1215
|
-
network,
|
|
1216
|
-
isMainnet: network === "mainnet-beta",
|
|
1217
|
-
explorerUrl: network === "mainnet-beta" ? "https://explorer.solana.com" : "https://explorer.solana.com?cluster=devnet"
|
|
1218
|
-
}),
|
|
1219
|
-
/** Build explorer URL for a transaction */
|
|
1220
|
-
getExplorerUrl: (signature) => {
|
|
1221
|
-
const baseUrl = "https://explorer.solana.com/tx";
|
|
1222
|
-
const cluster = network === "mainnet-beta" ? "" : "?cluster=devnet";
|
|
1223
|
-
return `${baseUrl}/${signature}${cluster}`;
|
|
1224
|
-
}
|
|
437
|
+
credits: validation.session.credits,
|
|
438
|
+
valid: true,
|
|
439
|
+
bundleExpiry: validation.session.bundleExpiry
|
|
1225
440
|
};
|
|
1226
441
|
}
|
|
1227
|
-
function createPaymentReference() {
|
|
1228
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1229
|
-
return crypto.randomUUID();
|
|
1230
|
-
}
|
|
1231
|
-
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1232
|
-
}
|
|
1233
442
|
|
|
1234
443
|
// src/pricing/index.ts
|
|
1235
444
|
var cachedPrice = null;
|
|
@@ -1361,60 +570,50 @@ function getProviders() {
|
|
|
1361
570
|
return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
|
|
1362
571
|
}
|
|
1363
572
|
|
|
1364
|
-
exports.
|
|
1365
|
-
exports.X402_HEADERS = X402_HEADERS;
|
|
1366
|
-
exports.addArticleToSession = addArticleToSession;
|
|
1367
|
-
exports.buildPaymentRequirement = buildPaymentRequirement;
|
|
573
|
+
exports.addCredits = addCredits;
|
|
1368
574
|
exports.buildSolanaPayUrl = buildSolanaPayUrl;
|
|
1369
|
-
exports.buildVersionedTransaction = buildVersionedTransaction;
|
|
1370
|
-
exports.calculatePriorityFeeCost = calculatePriorityFeeCost;
|
|
1371
|
-
exports.checkPaywallAccess = checkPaywallAccess;
|
|
1372
575
|
exports.clearPriceCache = clearPriceCache;
|
|
1373
576
|
exports.configurePricing = configurePricing;
|
|
1374
|
-
exports.
|
|
1375
|
-
exports.create402Response = create402Response;
|
|
1376
|
-
exports.create402ResponseBody = create402ResponseBody;
|
|
1377
|
-
exports.createMemoryStore = createMemoryStore;
|
|
577
|
+
exports.createCreditSession = createCreditSession;
|
|
1378
578
|
exports.createPaymentFlow = createPaymentFlow;
|
|
1379
579
|
exports.createPaymentReference = createPaymentReference;
|
|
1380
|
-
exports.
|
|
1381
|
-
exports.createPriorityFeeInstructions = createPriorityFeeInstructions;
|
|
1382
|
-
exports.createRedisStore = createRedisStore;
|
|
1383
|
-
exports.createSession = createSession;
|
|
1384
|
-
exports.decodePaymentRequired = decodePaymentRequired;
|
|
1385
|
-
exports.encodePaymentRequired = encodePaymentRequired;
|
|
1386
|
-
exports.encodePaymentRequirement = encodePaymentRequirement;
|
|
1387
|
-
exports.encodePaymentResponse = encodePaymentResponse;
|
|
1388
|
-
exports.estimatePriorityFee = estimatePriorityFee;
|
|
1389
|
-
exports.fetchLookupTables = fetchLookupTables;
|
|
580
|
+
exports.executeAgentPayment = executeAgentPayment;
|
|
1390
581
|
exports.formatPriceDisplay = formatPriceDisplay;
|
|
1391
582
|
exports.formatPriceSync = formatPriceSync;
|
|
1392
|
-
exports.
|
|
1393
|
-
exports.
|
|
583
|
+
exports.generateAgentKeypair = generateAgentKeypair;
|
|
584
|
+
exports.getAgentBalance = getAgentBalance;
|
|
1394
585
|
exports.getProviders = getProviders;
|
|
586
|
+
exports.getRemainingCredits = getRemainingCredits;
|
|
1395
587
|
exports.getSolPrice = getSolPrice;
|
|
1396
|
-
exports.
|
|
1397
|
-
exports.
|
|
1398
|
-
exports.isArticleUnlocked = isArticleUnlocked;
|
|
1399
|
-
exports.isMainnet = isMainnet;
|
|
1400
|
-
exports.isNativeAsset = isNativeAsset;
|
|
1401
|
-
exports.isRetryableRPCError = isRetryableRPCError;
|
|
1402
|
-
exports.isVersionedTransaction = isVersionedTransaction;
|
|
1403
|
-
exports.lamportsToSol = lamportsToSol;
|
|
588
|
+
exports.hasAgentSufficientBalance = hasAgentSufficientBalance;
|
|
589
|
+
exports.keypairFromBase58 = keypairFromBase58;
|
|
1404
590
|
exports.lamportsToUsd = lamportsToUsd;
|
|
1405
|
-
exports.parsePaymentHeader = parsePaymentHeader;
|
|
1406
|
-
exports.resetConnection = resetConnection;
|
|
1407
|
-
exports.resolveMintAddress = resolveMintAddress;
|
|
1408
|
-
exports.solToLamports = solToLamports;
|
|
1409
|
-
exports.toX402Network = toX402Network;
|
|
1410
591
|
exports.usdToLamports = usdToLamports;
|
|
1411
|
-
exports.
|
|
1412
|
-
exports.
|
|
1413
|
-
|
|
1414
|
-
exports.
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
592
|
+
exports.useCredit = useCredit;
|
|
593
|
+
exports.validateCreditSession = validateCreditSession;
|
|
594
|
+
Object.keys(core).forEach(function (k) {
|
|
595
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
596
|
+
enumerable: true,
|
|
597
|
+
get: function () { return core[k]; }
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
Object.keys(types).forEach(function (k) {
|
|
601
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
602
|
+
enumerable: true,
|
|
603
|
+
get: function () { return types[k]; }
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
Object.keys(client).forEach(function (k) {
|
|
607
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
608
|
+
enumerable: true,
|
|
609
|
+
get: function () { return client[k]; }
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
Object.keys(svm).forEach(function (k) {
|
|
613
|
+
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
614
|
+
enumerable: true,
|
|
615
|
+
get: function () { return svm[k]; }
|
|
616
|
+
});
|
|
617
|
+
});
|
|
1419
618
|
//# sourceMappingURL=index.cjs.map
|
|
1420
619
|
//# sourceMappingURL=index.cjs.map
|