@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.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
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/
|
|
9
|
+
// src/index.ts
|
|
10
|
+
|
|
11
|
+
// src/client/types.ts
|
|
6
12
|
var TOKEN_MINTS = {
|
|
7
13
|
/** USDC on mainnet */
|
|
8
14
|
USDC_MAINNET: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
@@ -11,471 +17,87 @@ var TOKEN_MINTS = {
|
|
|
11
17
|
/** USDT on mainnet */
|
|
12
18
|
USDT_MAINNET: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
13
19
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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" };
|
|
20
|
+
|
|
21
|
+
// src/client/payment.ts
|
|
22
|
+
function buildSolanaPayUrl(params) {
|
|
23
|
+
const { recipient, amount, splToken, reference, label, message } = params;
|
|
24
|
+
const url = new URL(`solana:${recipient}`);
|
|
25
|
+
if (amount !== void 0) {
|
|
26
|
+
url.searchParams.set("amount", amount.toString());
|
|
236
27
|
}
|
|
237
|
-
|
|
238
|
-
|
|
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" };
|
|
28
|
+
if (splToken) {
|
|
29
|
+
url.searchParams.set("spl-token", splToken);
|
|
246
30
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (!isValidWalletAddress(walletAddress)) {
|
|
250
|
-
return [];
|
|
31
|
+
if (reference) {
|
|
32
|
+
url.searchParams.set("reference", reference);
|
|
251
33
|
}
|
|
252
|
-
|
|
253
|
-
|
|
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 [];
|
|
34
|
+
if (label) {
|
|
35
|
+
url.searchParams.set("label", label);
|
|
264
36
|
}
|
|
265
|
-
|
|
266
|
-
|
|
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");
|
|
37
|
+
if (message) {
|
|
38
|
+
url.searchParams.set("message", message);
|
|
272
39
|
}
|
|
273
|
-
return
|
|
40
|
+
return url.toString();
|
|
274
41
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
42
|
+
function createPaymentFlow(config2) {
|
|
43
|
+
const { network, recipientWallet, amount, asset = "native", memo } = config2;
|
|
44
|
+
let decimals = 9;
|
|
45
|
+
let mintAddress;
|
|
279
46
|
if (asset === "usdc") {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (asset === "usdt") {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (typeof asset === "object" && "mint" in asset) {
|
|
286
|
-
|
|
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
|
-
}
|
|
47
|
+
decimals = 6;
|
|
48
|
+
mintAddress = network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
49
|
+
} else if (asset === "usdt") {
|
|
50
|
+
decimals = 6;
|
|
51
|
+
mintAddress = TOKEN_MINTS.USDT_MAINNET;
|
|
52
|
+
} else if (typeof asset === "object" && "mint" in asset) {
|
|
53
|
+
decimals = asset.decimals ?? 6;
|
|
54
|
+
mintAddress = asset.mint;
|
|
335
55
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
56
|
+
const naturalAmount = Number(amount) / Math.pow(10, decimals);
|
|
57
|
+
return {
|
|
58
|
+
/** Get the payment configuration */
|
|
59
|
+
getConfig: () => ({ ...config2 }),
|
|
60
|
+
/** Get amount in natural display units (e.g., 0.01 SOL) */
|
|
61
|
+
getDisplayAmount: () => naturalAmount,
|
|
62
|
+
/** Get amount formatted with symbol */
|
|
63
|
+
getFormattedAmount: () => {
|
|
64
|
+
const symbol = asset === "native" ? "SOL" : asset === "usdc" ? "USDC" : asset === "usdt" ? "USDT" : "tokens";
|
|
65
|
+
return `${naturalAmount.toFixed(decimals > 6 ? 4 : 2)} ${symbol}`;
|
|
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}`;
|
|
357
93
|
}
|
|
358
|
-
}
|
|
359
|
-
return null;
|
|
94
|
+
};
|
|
360
95
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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" };
|
|
96
|
+
function createPaymentReference() {
|
|
97
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
98
|
+
return crypto.randomUUID();
|
|
475
99
|
}
|
|
476
|
-
}
|
|
477
|
-
function isNativeAsset(asset) {
|
|
478
|
-
return asset === "native";
|
|
100
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
479
101
|
}
|
|
480
102
|
var DEFAULT_COMPUTE_UNITS = 2e5;
|
|
481
103
|
var DEFAULT_MICRO_LAMPORTS = 1e3;
|
|
@@ -491,33 +113,11 @@ function createPriorityFeeInstructions(config2 = {}) {
|
|
|
491
113
|
instructions.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: price }));
|
|
492
114
|
return instructions;
|
|
493
115
|
}
|
|
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
116
|
async function buildVersionedTransaction(config2) {
|
|
516
117
|
const {
|
|
517
118
|
connection,
|
|
518
119
|
payer,
|
|
519
120
|
instructions,
|
|
520
|
-
lookupTables = [],
|
|
521
121
|
priorityFee,
|
|
522
122
|
recentBlockhash
|
|
523
123
|
} = config2;
|
|
@@ -538,7 +138,7 @@ async function buildVersionedTransaction(config2) {
|
|
|
538
138
|
payerKey: payer,
|
|
539
139
|
recentBlockhash: blockhash,
|
|
540
140
|
instructions: allInstructions
|
|
541
|
-
}).compileToV0Message(
|
|
141
|
+
}).compileToV0Message([]);
|
|
542
142
|
const transaction = new VersionedTransaction(message);
|
|
543
143
|
return {
|
|
544
144
|
transaction,
|
|
@@ -546,20 +146,129 @@ async function buildVersionedTransaction(config2) {
|
|
|
546
146
|
lastValidBlockHeight
|
|
547
147
|
};
|
|
548
148
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
149
|
+
|
|
150
|
+
// src/agent/agentPayment.ts
|
|
151
|
+
var WALLET_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
152
|
+
function isValidWalletAddress(address) {
|
|
153
|
+
if (!address || typeof address !== "string") return false;
|
|
154
|
+
return WALLET_REGEX.test(address);
|
|
155
|
+
}
|
|
156
|
+
async function executeAgentPayment(params) {
|
|
157
|
+
const {
|
|
158
|
+
connection,
|
|
159
|
+
agentKeypair,
|
|
160
|
+
recipientAddress,
|
|
161
|
+
amountLamports,
|
|
162
|
+
priorityFee,
|
|
163
|
+
confirmationTimeout = 6e4
|
|
164
|
+
} = params;
|
|
165
|
+
if (!isValidWalletAddress(recipientAddress)) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: "Invalid recipient address format"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (amountLamports <= 0n) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: "Amount must be greater than 0"
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const recipientPubkey = new PublicKey(recipientAddress);
|
|
179
|
+
const transferInstruction = SystemProgram.transfer({
|
|
180
|
+
fromPubkey: agentKeypair.publicKey,
|
|
181
|
+
toPubkey: recipientPubkey,
|
|
182
|
+
lamports: amountLamports
|
|
183
|
+
});
|
|
184
|
+
const { transaction, lastValidBlockHeight } = await buildVersionedTransaction({
|
|
185
|
+
connection,
|
|
186
|
+
payer: agentKeypair.publicKey,
|
|
187
|
+
instructions: [transferInstruction],
|
|
188
|
+
priorityFee
|
|
189
|
+
});
|
|
190
|
+
transaction.sign([agentKeypair]);
|
|
191
|
+
const signature = await connection.sendTransaction(transaction, {
|
|
192
|
+
maxRetries: 3,
|
|
193
|
+
skipPreflight: false
|
|
194
|
+
});
|
|
195
|
+
const confirmationPromise = connection.confirmTransaction(
|
|
196
|
+
{
|
|
197
|
+
signature,
|
|
198
|
+
lastValidBlockHeight,
|
|
199
|
+
blockhash: transaction.message.recentBlockhash
|
|
200
|
+
},
|
|
201
|
+
"confirmed"
|
|
202
|
+
);
|
|
203
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
204
|
+
setTimeout(() => reject(new Error("Confirmation timeout")), confirmationTimeout);
|
|
205
|
+
});
|
|
206
|
+
const confirmation = await Promise.race([confirmationPromise, timeoutPromise]);
|
|
207
|
+
if (confirmation.value.err) {
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
signature,
|
|
211
|
+
error: "Transaction failed on-chain"
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const txDetails = await connection.getTransaction(signature, {
|
|
215
|
+
commitment: "confirmed",
|
|
216
|
+
maxSupportedTransactionVersion: 0
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
success: true,
|
|
220
|
+
signature,
|
|
221
|
+
confirmedAt: txDetails?.blockTime ?? Math.floor(Date.now() / 1e3),
|
|
222
|
+
slot: txDetails?.slot ?? confirmation.context.slot,
|
|
223
|
+
amountLamports,
|
|
224
|
+
amountSol: Number(amountLamports) / LAMPORTS_PER_SOL
|
|
225
|
+
};
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: errorMessage
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function getAgentBalance(connection, agentKeypair) {
|
|
235
|
+
const balance = await connection.getBalance(agentKeypair.publicKey);
|
|
236
|
+
return {
|
|
237
|
+
balance: BigInt(balance),
|
|
238
|
+
balanceSol: balance / LAMPORTS_PER_SOL
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
async function hasAgentSufficientBalance(connection, agentKeypair, requiredLamports) {
|
|
242
|
+
const { balance } = await getAgentBalance(connection, agentKeypair);
|
|
243
|
+
const totalRequired = requiredLamports + 10000n;
|
|
244
|
+
return {
|
|
245
|
+
sufficient: balance >= totalRequired,
|
|
246
|
+
balance,
|
|
247
|
+
required: totalRequired
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function keypairFromBase58(base58Secret) {
|
|
251
|
+
const bytes = Buffer.from(base58Secret, "base64");
|
|
252
|
+
if (bytes.length !== 64) {
|
|
253
|
+
const parts = base58Secret.split(",").map((n) => parseInt(n.trim(), 10));
|
|
254
|
+
if (parts.length === 64) {
|
|
255
|
+
return Keypair.fromSecretKey(Uint8Array.from(parts));
|
|
555
256
|
}
|
|
257
|
+
throw new Error("Invalid secret key format. Expected base58 string or comma-separated bytes.");
|
|
556
258
|
}
|
|
557
|
-
return
|
|
259
|
+
return Keypair.fromSecretKey(bytes);
|
|
558
260
|
}
|
|
559
|
-
function
|
|
560
|
-
|
|
261
|
+
function generateAgentKeypair() {
|
|
262
|
+
const keypair = Keypair.generate();
|
|
263
|
+
const secretBytes = Array.from(keypair.secretKey);
|
|
264
|
+
return {
|
|
265
|
+
keypair,
|
|
266
|
+
secretBase58: secretBytes.join(","),
|
|
267
|
+
// Comma-separated for easy storage
|
|
268
|
+
publicKey: keypair.publicKey.toBase58()
|
|
269
|
+
};
|
|
561
270
|
}
|
|
562
|
-
var
|
|
271
|
+
var MAX_CREDITS = 1e3;
|
|
563
272
|
var MIN_SECRET_LENGTH = 32;
|
|
564
273
|
function getSecretKey(secret) {
|
|
565
274
|
if (!secret || typeof secret !== "string") {
|
|
@@ -575,659 +284,159 @@ function validateWalletAddress(address) {
|
|
|
575
284
|
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
576
285
|
return base58Regex.test(address);
|
|
577
286
|
}
|
|
578
|
-
function
|
|
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) {
|
|
287
|
+
async function createCreditSession(walletAddress, purchaseId, config2) {
|
|
585
288
|
if (!validateWalletAddress(walletAddress)) {
|
|
586
289
|
throw new Error("Invalid wallet address format");
|
|
587
290
|
}
|
|
588
|
-
if (
|
|
589
|
-
throw new Error(
|
|
291
|
+
if (config2.initialCredits <= 0 || config2.initialCredits > MAX_CREDITS) {
|
|
292
|
+
throw new Error(`Credits must be between 1 and ${MAX_CREDITS}`);
|
|
590
293
|
}
|
|
591
|
-
if (!config2.durationHours || config2.durationHours <= 0 || config2.durationHours >
|
|
592
|
-
throw new Error("Session duration must be between 1 and
|
|
294
|
+
if (!config2.durationHours || config2.durationHours <= 0 || config2.durationHours > 8760) {
|
|
295
|
+
throw new Error("Session duration must be between 1 and 8760 hours (1 year)");
|
|
593
296
|
}
|
|
594
297
|
const sessionId = v4();
|
|
595
298
|
const now = Math.floor(Date.now() / 1e3);
|
|
596
299
|
const expiresAt = now + config2.durationHours * 3600;
|
|
300
|
+
const bundleExpiry = config2.bundleExpiryHours ? now + config2.bundleExpiryHours * 3600 : expiresAt;
|
|
597
301
|
const session = {
|
|
598
302
|
id: sessionId,
|
|
599
303
|
walletAddress,
|
|
600
|
-
unlockedArticles: [
|
|
601
|
-
siteWideUnlock:
|
|
304
|
+
unlockedArticles: [purchaseId],
|
|
305
|
+
siteWideUnlock: false,
|
|
602
306
|
createdAt: now,
|
|
603
|
-
expiresAt
|
|
307
|
+
expiresAt,
|
|
308
|
+
credits: config2.initialCredits,
|
|
309
|
+
bundleExpiry,
|
|
310
|
+
bundleType: config2.bundleType
|
|
604
311
|
};
|
|
605
312
|
const payload = {
|
|
606
313
|
sub: walletAddress,
|
|
607
314
|
sid: sessionId,
|
|
608
315
|
articles: session.unlockedArticles,
|
|
609
|
-
siteWide:
|
|
316
|
+
siteWide: false,
|
|
317
|
+
credits: config2.initialCredits,
|
|
318
|
+
bundleExpiry,
|
|
319
|
+
bundleType: config2.bundleType,
|
|
610
320
|
iat: now,
|
|
611
321
|
exp: expiresAt
|
|
612
322
|
};
|
|
613
323
|
const token = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config2.durationHours}h`).sign(getSecretKey(config2.secret));
|
|
614
324
|
return { token, session };
|
|
615
325
|
}
|
|
616
|
-
async function
|
|
326
|
+
async function validateCreditSession(token, secret) {
|
|
617
327
|
if (!token || typeof token !== "string") {
|
|
618
328
|
return { valid: false, reason: "Invalid token format" };
|
|
619
329
|
}
|
|
620
330
|
try {
|
|
621
331
|
const { payload } = await jwtVerify(token, getSecretKey(secret));
|
|
622
|
-
const
|
|
623
|
-
if (!
|
|
332
|
+
const creditPayload = payload;
|
|
333
|
+
if (!creditPayload.sub || !creditPayload.sid || !creditPayload.exp) {
|
|
624
334
|
return { valid: false, reason: "Malformed session payload" };
|
|
625
335
|
}
|
|
626
336
|
const now = Math.floor(Date.now() / 1e3);
|
|
627
|
-
if (
|
|
337
|
+
if (creditPayload.exp < now) {
|
|
628
338
|
return { valid: false, reason: "Session expired" };
|
|
629
339
|
}
|
|
630
|
-
if (
|
|
340
|
+
if (creditPayload.bundleExpiry && creditPayload.bundleExpiry < now) {
|
|
341
|
+
return { valid: false, reason: "Bundle expired" };
|
|
342
|
+
}
|
|
343
|
+
if (!validateWalletAddress(creditPayload.sub)) {
|
|
631
344
|
return { valid: false, reason: "Invalid session data" };
|
|
632
345
|
}
|
|
633
346
|
const session = {
|
|
634
|
-
id:
|
|
635
|
-
walletAddress:
|
|
636
|
-
unlockedArticles: Array.isArray(
|
|
637
|
-
siteWideUnlock: Boolean(
|
|
638
|
-
createdAt:
|
|
639
|
-
expiresAt:
|
|
347
|
+
id: creditPayload.sid,
|
|
348
|
+
walletAddress: creditPayload.sub,
|
|
349
|
+
unlockedArticles: Array.isArray(creditPayload.articles) ? creditPayload.articles : [],
|
|
350
|
+
siteWideUnlock: Boolean(creditPayload.siteWide),
|
|
351
|
+
createdAt: creditPayload.iat ?? 0,
|
|
352
|
+
expiresAt: creditPayload.exp,
|
|
353
|
+
credits: creditPayload.credits ?? 0,
|
|
354
|
+
bundleExpiry: creditPayload.bundleExpiry,
|
|
355
|
+
bundleType: creditPayload.bundleType
|
|
640
356
|
};
|
|
641
357
|
return { valid: true, session };
|
|
642
|
-
} catch
|
|
358
|
+
} catch {
|
|
643
359
|
return { valid: false, reason: "Invalid session" };
|
|
644
360
|
}
|
|
645
361
|
}
|
|
646
|
-
async function
|
|
647
|
-
if (
|
|
648
|
-
return
|
|
362
|
+
async function useCredit(token, secret, creditsToUse = 1) {
|
|
363
|
+
if (creditsToUse <= 0) {
|
|
364
|
+
return { success: false, remainingCredits: 0, error: "Invalid credit amount" };
|
|
649
365
|
}
|
|
650
|
-
const validation = await
|
|
366
|
+
const validation = await validateCreditSession(token, secret);
|
|
651
367
|
if (!validation.valid || !validation.session) {
|
|
652
|
-
return
|
|
368
|
+
return {
|
|
369
|
+
success: false,
|
|
370
|
+
remainingCredits: 0,
|
|
371
|
+
error: validation.reason || "Invalid session"
|
|
372
|
+
};
|
|
653
373
|
}
|
|
654
374
|
const session = validation.session;
|
|
655
|
-
if (session.
|
|
656
|
-
return {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
375
|
+
if (session.credits < creditsToUse) {
|
|
376
|
+
return {
|
|
377
|
+
success: false,
|
|
378
|
+
remainingCredits: session.credits,
|
|
379
|
+
error: "Insufficient credits"
|
|
380
|
+
};
|
|
660
381
|
}
|
|
661
|
-
const
|
|
382
|
+
const newCredits = session.credits - creditsToUse;
|
|
662
383
|
const payload = {
|
|
663
384
|
sub: session.walletAddress,
|
|
664
385
|
sid: session.id,
|
|
665
|
-
articles:
|
|
386
|
+
articles: session.unlockedArticles,
|
|
666
387
|
siteWide: session.siteWideUnlock,
|
|
388
|
+
credits: newCredits,
|
|
389
|
+
bundleExpiry: session.bundleExpiry,
|
|
390
|
+
bundleType: session.bundleType,
|
|
667
391
|
iat: session.createdAt,
|
|
668
392
|
exp: session.expiresAt
|
|
669
393
|
};
|
|
670
394
|
const newToken = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(getSecretKey(secret));
|
|
671
395
|
return {
|
|
672
|
-
|
|
673
|
-
|
|
396
|
+
success: true,
|
|
397
|
+
remainingCredits: newCredits,
|
|
398
|
+
newToken
|
|
674
399
|
};
|
|
675
400
|
}
|
|
676
|
-
async function
|
|
677
|
-
if (
|
|
678
|
-
return false;
|
|
401
|
+
async function addCredits(token, secret, creditsToAdd) {
|
|
402
|
+
if (creditsToAdd <= 0 || creditsToAdd > MAX_CREDITS) {
|
|
403
|
+
return { success: false, error: "Invalid credit amount" };
|
|
679
404
|
}
|
|
680
|
-
const validation = await
|
|
405
|
+
const validation = await validateCreditSession(token, secret);
|
|
681
406
|
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" };
|
|
407
|
+
return { success: false, error: validation.reason || "Invalid session" };
|
|
798
408
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
}
|
|
409
|
+
const session = validation.session;
|
|
410
|
+
const newCredits = Math.min(session.credits + creditsToAdd, MAX_CREDITS);
|
|
411
|
+
const payload = {
|
|
412
|
+
sub: session.walletAddress,
|
|
413
|
+
sid: session.id,
|
|
414
|
+
articles: session.unlockedArticles,
|
|
415
|
+
siteWide: session.siteWideUnlock,
|
|
416
|
+
credits: newCredits,
|
|
417
|
+
bundleExpiry: session.bundleExpiry,
|
|
418
|
+
bundleType: session.bundleType,
|
|
419
|
+
iat: session.createdAt,
|
|
420
|
+
exp: session.expiresAt
|
|
935
421
|
};
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// src/store/redis.ts
|
|
939
|
-
function createRedisStore(options) {
|
|
940
|
-
const { client, keyPrefix = "micropay:sig:" } = options;
|
|
941
|
-
const buildKey = (signature) => `${keyPrefix}${signature}`;
|
|
422
|
+
const newToken = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(getSecretKey(secret));
|
|
942
423
|
return {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
-
}
|
|
424
|
+
success: true,
|
|
425
|
+
newToken,
|
|
426
|
+
totalCredits: newCredits
|
|
976
427
|
};
|
|
977
428
|
}
|
|
978
|
-
|
|
979
|
-
|
|
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);
|
|
429
|
+
async function getRemainingCredits(token, secret) {
|
|
430
|
+
const validation = await validateCreditSession(token, secret);
|
|
1002
431
|
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
|
-
}
|
|
1087
|
-
|
|
1088
|
-
// src/utils/retry.ts
|
|
1089
|
-
function sleep(ms) {
|
|
1090
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1091
|
-
}
|
|
1092
|
-
function calculateDelay(attempt, options) {
|
|
1093
|
-
const { baseDelay, maxDelay, jitter } = options;
|
|
1094
|
-
let delay = baseDelay * Math.pow(2, attempt);
|
|
1095
|
-
delay = Math.min(delay, maxDelay);
|
|
1096
|
-
if (jitter) {
|
|
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
|
-
}
|
|
1149
|
-
|
|
1150
|
-
// src/client/payment.ts
|
|
1151
|
-
function buildSolanaPayUrl(params) {
|
|
1152
|
-
const { recipient, amount, splToken, reference, label, message } = params;
|
|
1153
|
-
const url = new URL(`solana:${recipient}`);
|
|
1154
|
-
if (amount !== void 0) {
|
|
1155
|
-
url.searchParams.set("amount", amount.toString());
|
|
1156
|
-
}
|
|
1157
|
-
if (splToken) {
|
|
1158
|
-
url.searchParams.set("spl-token", splToken);
|
|
1159
|
-
}
|
|
1160
|
-
if (reference) {
|
|
1161
|
-
url.searchParams.set("reference", reference);
|
|
1162
|
-
}
|
|
1163
|
-
if (label) {
|
|
1164
|
-
url.searchParams.set("label", label);
|
|
1165
|
-
}
|
|
1166
|
-
if (message) {
|
|
1167
|
-
url.searchParams.set("message", message);
|
|
432
|
+
return { credits: 0, valid: false };
|
|
1168
433
|
}
|
|
1169
|
-
return url.toString();
|
|
1170
|
-
}
|
|
1171
|
-
function createPaymentFlow(config2) {
|
|
1172
|
-
const { network, recipientWallet, amount, asset = "native", memo } = config2;
|
|
1173
|
-
let decimals = 9;
|
|
1174
|
-
let mintAddress;
|
|
1175
|
-
if (asset === "usdc") {
|
|
1176
|
-
decimals = 6;
|
|
1177
|
-
mintAddress = network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
1178
|
-
} else if (asset === "usdt") {
|
|
1179
|
-
decimals = 6;
|
|
1180
|
-
mintAddress = TOKEN_MINTS.USDT_MAINNET;
|
|
1181
|
-
} else if (typeof asset === "object" && "mint" in asset) {
|
|
1182
|
-
decimals = asset.decimals ?? 6;
|
|
1183
|
-
mintAddress = asset.mint;
|
|
1184
|
-
}
|
|
1185
|
-
const naturalAmount = Number(amount) / Math.pow(10, decimals);
|
|
1186
434
|
return {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
getDisplayAmount: () => naturalAmount,
|
|
1191
|
-
/** Get amount formatted with symbol */
|
|
1192
|
-
getFormattedAmount: () => {
|
|
1193
|
-
const symbol = asset === "native" ? "SOL" : asset === "usdc" ? "USDC" : asset === "usdt" ? "USDT" : "tokens";
|
|
1194
|
-
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
|
-
}
|
|
435
|
+
credits: validation.session.credits,
|
|
436
|
+
valid: true,
|
|
437
|
+
bundleExpiry: validation.session.bundleExpiry
|
|
1223
438
|
};
|
|
1224
439
|
}
|
|
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
440
|
|
|
1232
441
|
// src/pricing/index.ts
|
|
1233
442
|
var cachedPrice = null;
|
|
@@ -1359,6 +568,6 @@ function getProviders() {
|
|
|
1359
568
|
return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
|
|
1360
569
|
}
|
|
1361
570
|
|
|
1362
|
-
export {
|
|
571
|
+
export { addCredits, buildSolanaPayUrl, clearPriceCache, configurePricing, createCreditSession, createPaymentFlow, createPaymentReference, executeAgentPayment, formatPriceDisplay, formatPriceSync, generateAgentKeypair, getAgentBalance, getProviders, getRemainingCredits, getSolPrice, hasAgentSufficientBalance, keypairFromBase58, lamportsToUsd, usdToLamports, useCredit, validateCreditSession };
|
|
1363
572
|
//# sourceMappingURL=index.js.map
|
|
1364
573
|
//# sourceMappingURL=index.js.map
|