@dexterai/x402 3.0.0 → 3.1.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/dist/adapters/index.cjs +1 -954
- package/dist/adapters/index.js +1 -903
- package/dist/client/index.cjs +1 -1907
- package/dist/client/index.d.cts +2 -223
- package/dist/client/index.d.ts +2 -223
- package/dist/client/index.js +1 -1871
- package/dist/react/index.cjs +1 -1820
- package/dist/react/index.js +1 -1792
- package/dist/server/index.cjs +23 -1901
- package/dist/server/index.js +23 -1831
- package/dist/utils/index.cjs +1 -111
- package/dist/utils/index.js +1 -80
- package/package.json +3 -2
package/dist/react/index.js
CHANGED
|
@@ -1,1792 +1 @@
|
|
|
1
|
-
// src/react/useX402Payment.ts
|
|
2
|
-
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
3
|
-
|
|
4
|
-
// src/types.ts
|
|
5
|
-
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
6
|
-
var BASE_MAINNET_NETWORK = "eip155:8453";
|
|
7
|
-
var USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
8
|
-
var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
9
|
-
var X402Error = class _X402Error extends Error {
|
|
10
|
-
/** Error code for programmatic handling */
|
|
11
|
-
code;
|
|
12
|
-
/** Additional error details */
|
|
13
|
-
details;
|
|
14
|
-
constructor(code, message, details) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.name = "X402Error";
|
|
17
|
-
this.code = code;
|
|
18
|
-
this.details = details;
|
|
19
|
-
Object.setPrototypeOf(this, _X402Error.prototype);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// src/adapters/solana.ts
|
|
24
|
-
import {
|
|
25
|
-
PublicKey,
|
|
26
|
-
Connection,
|
|
27
|
-
TransactionMessage,
|
|
28
|
-
VersionedTransaction,
|
|
29
|
-
ComputeBudgetProgram
|
|
30
|
-
} from "@solana/web3.js";
|
|
31
|
-
import {
|
|
32
|
-
getAssociatedTokenAddress,
|
|
33
|
-
getAccount,
|
|
34
|
-
createTransferCheckedInstruction,
|
|
35
|
-
getMint,
|
|
36
|
-
TOKEN_PROGRAM_ID,
|
|
37
|
-
TOKEN_2022_PROGRAM_ID
|
|
38
|
-
} from "@solana/spl-token";
|
|
39
|
-
var SOLANA_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
40
|
-
var SOLANA_DEVNET = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
41
|
-
var SOLANA_TESTNET = "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z";
|
|
42
|
-
var DEFAULT_RPC_URLS = {
|
|
43
|
-
[SOLANA_MAINNET]: "https://api.dexter.cash/api/solana/rpc",
|
|
44
|
-
[SOLANA_DEVNET]: "https://api.devnet.solana.com",
|
|
45
|
-
[SOLANA_TESTNET]: "https://api.testnet.solana.com"
|
|
46
|
-
};
|
|
47
|
-
var DEFAULT_COMPUTE_UNIT_LIMIT = 12e3;
|
|
48
|
-
var DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
|
|
49
|
-
function isSolanaWallet(wallet) {
|
|
50
|
-
if (!wallet || typeof wallet !== "object") return false;
|
|
51
|
-
const w = wallet;
|
|
52
|
-
return "publicKey" in w && "signTransaction" in w && typeof w.signTransaction === "function";
|
|
53
|
-
}
|
|
54
|
-
var SolanaAdapter = class {
|
|
55
|
-
name = "Solana";
|
|
56
|
-
networks = [SOLANA_MAINNET, SOLANA_DEVNET, SOLANA_TESTNET];
|
|
57
|
-
config;
|
|
58
|
-
log;
|
|
59
|
-
constructor(config = {}) {
|
|
60
|
-
this.config = config;
|
|
61
|
-
this.log = config.verbose ? console.log.bind(console, "[x402:solana]") : () => {
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
canHandle(network) {
|
|
65
|
-
if (this.networks.includes(network)) return true;
|
|
66
|
-
if (network === "solana") return true;
|
|
67
|
-
if (network === "solana-devnet") return true;
|
|
68
|
-
if (network === "solana-testnet") return true;
|
|
69
|
-
if (network.startsWith("solana:")) return true;
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
getDefaultRpcUrl(network) {
|
|
73
|
-
if (this.config.rpcUrls?.[network]) {
|
|
74
|
-
return this.config.rpcUrls[network];
|
|
75
|
-
}
|
|
76
|
-
if (DEFAULT_RPC_URLS[network]) {
|
|
77
|
-
return DEFAULT_RPC_URLS[network];
|
|
78
|
-
}
|
|
79
|
-
if (network === "solana") return DEFAULT_RPC_URLS[SOLANA_MAINNET];
|
|
80
|
-
if (network === "solana-devnet") return DEFAULT_RPC_URLS[SOLANA_DEVNET];
|
|
81
|
-
if (network === "solana-testnet") return DEFAULT_RPC_URLS[SOLANA_TESTNET];
|
|
82
|
-
return DEFAULT_RPC_URLS[SOLANA_MAINNET];
|
|
83
|
-
}
|
|
84
|
-
getAddress(wallet) {
|
|
85
|
-
if (!isSolanaWallet(wallet)) return null;
|
|
86
|
-
return wallet.publicKey?.toBase58() ?? null;
|
|
87
|
-
}
|
|
88
|
-
isConnected(wallet) {
|
|
89
|
-
if (!isSolanaWallet(wallet)) return false;
|
|
90
|
-
return wallet.publicKey !== null;
|
|
91
|
-
}
|
|
92
|
-
async getBalance(accept, wallet, rpcUrl) {
|
|
93
|
-
if (!isSolanaWallet(wallet) || !wallet.publicKey) {
|
|
94
|
-
return 0;
|
|
95
|
-
}
|
|
96
|
-
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
97
|
-
const connection = new Connection(url, "confirmed");
|
|
98
|
-
const userPubkey = new PublicKey(wallet.publicKey.toBase58());
|
|
99
|
-
const mintPubkey = new PublicKey(accept.asset);
|
|
100
|
-
try {
|
|
101
|
-
const mintInfo = await connection.getAccountInfo(mintPubkey, "confirmed");
|
|
102
|
-
const programId = mintInfo?.owner.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58() ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
|
|
103
|
-
const ata = await getAssociatedTokenAddress(
|
|
104
|
-
mintPubkey,
|
|
105
|
-
userPubkey,
|
|
106
|
-
false,
|
|
107
|
-
programId
|
|
108
|
-
);
|
|
109
|
-
const account = await getAccount(connection, ata, void 0, programId);
|
|
110
|
-
const decimals = accept.extra?.decimals ?? 6;
|
|
111
|
-
return Number(account.amount) / Math.pow(10, decimals);
|
|
112
|
-
} catch (err) {
|
|
113
|
-
if (err && typeof err === "object" && "name" in err && (err.name === "TokenAccountNotFoundError" || err.name === "TokenInvalidAccountOwnerError")) {
|
|
114
|
-
return 0;
|
|
115
|
-
}
|
|
116
|
-
throw err;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
async buildTransaction(accept, wallet, rpcUrl) {
|
|
120
|
-
if (!isSolanaWallet(wallet)) {
|
|
121
|
-
throw new Error("Invalid Solana wallet");
|
|
122
|
-
}
|
|
123
|
-
if (!wallet.publicKey) {
|
|
124
|
-
throw new Error("Wallet not connected");
|
|
125
|
-
}
|
|
126
|
-
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
127
|
-
const connection = new Connection(url, "confirmed");
|
|
128
|
-
const userPubkey = new PublicKey(wallet.publicKey.toBase58());
|
|
129
|
-
const { payTo, asset, extra } = accept;
|
|
130
|
-
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
131
|
-
if (!amount) {
|
|
132
|
-
throw new Error("Missing amount in payment requirements");
|
|
133
|
-
}
|
|
134
|
-
if (!extra?.feePayer) {
|
|
135
|
-
throw new Error("Missing feePayer in payment requirements");
|
|
136
|
-
}
|
|
137
|
-
const feePayerPubkey = new PublicKey(extra.feePayer);
|
|
138
|
-
const mintPubkey = new PublicKey(asset);
|
|
139
|
-
const destinationPubkey = new PublicKey(payTo);
|
|
140
|
-
this.log("Building transaction:", {
|
|
141
|
-
from: userPubkey.toBase58(),
|
|
142
|
-
to: payTo,
|
|
143
|
-
amount,
|
|
144
|
-
asset,
|
|
145
|
-
feePayer: extra.feePayer
|
|
146
|
-
});
|
|
147
|
-
const instructions = [];
|
|
148
|
-
instructions.push(
|
|
149
|
-
ComputeBudgetProgram.setComputeUnitLimit({
|
|
150
|
-
units: DEFAULT_COMPUTE_UNIT_LIMIT
|
|
151
|
-
})
|
|
152
|
-
);
|
|
153
|
-
instructions.push(
|
|
154
|
-
ComputeBudgetProgram.setComputeUnitPrice({
|
|
155
|
-
microLamports: DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS
|
|
156
|
-
})
|
|
157
|
-
);
|
|
158
|
-
const mintInfo = await connection.getAccountInfo(mintPubkey, "confirmed");
|
|
159
|
-
if (!mintInfo) {
|
|
160
|
-
throw new Error(`Token mint ${asset} not found`);
|
|
161
|
-
}
|
|
162
|
-
const programId = mintInfo.owner.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58() ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
|
|
163
|
-
const mint = await getMint(connection, mintPubkey, void 0, programId);
|
|
164
|
-
if (typeof extra?.decimals === "number" && mint.decimals !== extra.decimals) {
|
|
165
|
-
this.log(
|
|
166
|
-
`Decimals mismatch: requirements say ${extra.decimals}, mint says ${mint.decimals}`
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
const sourceAta = await getAssociatedTokenAddress(
|
|
170
|
-
mintPubkey,
|
|
171
|
-
userPubkey,
|
|
172
|
-
false,
|
|
173
|
-
programId
|
|
174
|
-
);
|
|
175
|
-
const destinationAta = await getAssociatedTokenAddress(
|
|
176
|
-
mintPubkey,
|
|
177
|
-
destinationPubkey,
|
|
178
|
-
false,
|
|
179
|
-
programId
|
|
180
|
-
);
|
|
181
|
-
const sourceAtaInfo = await connection.getAccountInfo(sourceAta, "confirmed");
|
|
182
|
-
if (!sourceAtaInfo) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
`No token account found for ${asset}. Please ensure you have USDC in your wallet.`
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
const destAtaInfo = await connection.getAccountInfo(destinationAta, "confirmed");
|
|
188
|
-
if (!destAtaInfo) {
|
|
189
|
-
throw new Error(
|
|
190
|
-
`Seller token account not found. The seller (${payTo}) must have a USDC account.`
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
const amountBigInt = BigInt(amount);
|
|
194
|
-
instructions.push(
|
|
195
|
-
createTransferCheckedInstruction(
|
|
196
|
-
sourceAta,
|
|
197
|
-
mintPubkey,
|
|
198
|
-
destinationAta,
|
|
199
|
-
userPubkey,
|
|
200
|
-
amountBigInt,
|
|
201
|
-
mint.decimals,
|
|
202
|
-
[],
|
|
203
|
-
programId
|
|
204
|
-
)
|
|
205
|
-
);
|
|
206
|
-
const { blockhash } = await connection.getLatestBlockhash("confirmed");
|
|
207
|
-
const message = new TransactionMessage({
|
|
208
|
-
payerKey: feePayerPubkey,
|
|
209
|
-
recentBlockhash: blockhash,
|
|
210
|
-
instructions
|
|
211
|
-
}).compileToV0Message();
|
|
212
|
-
const transaction = new VersionedTransaction(message);
|
|
213
|
-
const signedTx = await wallet.signTransaction(transaction);
|
|
214
|
-
this.log("Transaction signed successfully");
|
|
215
|
-
return {
|
|
216
|
-
serialized: Buffer.from(signedTx.serialize()).toString("base64")
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
function createSolanaAdapter(config) {
|
|
221
|
-
return new SolanaAdapter(config);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// src/adapters/evm.ts
|
|
225
|
-
var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
226
|
-
var X402_EXACT_PERMIT2_PROXY = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001";
|
|
227
|
-
var PERMIT2_WITNESS_TYPES = {
|
|
228
|
-
PermitWitnessTransferFrom: [
|
|
229
|
-
{ name: "permitted", type: "TokenPermissions" },
|
|
230
|
-
{ name: "spender", type: "address" },
|
|
231
|
-
{ name: "nonce", type: "uint256" },
|
|
232
|
-
{ name: "deadline", type: "uint256" },
|
|
233
|
-
{ name: "witness", type: "Witness" }
|
|
234
|
-
],
|
|
235
|
-
TokenPermissions: [
|
|
236
|
-
{ name: "token", type: "address" },
|
|
237
|
-
{ name: "amount", type: "uint256" }
|
|
238
|
-
],
|
|
239
|
-
Witness: [
|
|
240
|
-
{ name: "to", type: "address" },
|
|
241
|
-
{ name: "validAfter", type: "uint256" }
|
|
242
|
-
]
|
|
243
|
-
};
|
|
244
|
-
var MAX_UINT256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
|
245
|
-
var BASE_MAINNET = "eip155:8453";
|
|
246
|
-
var BASE_SEPOLIA = "eip155:84532";
|
|
247
|
-
var ARBITRUM_ONE = "eip155:42161";
|
|
248
|
-
var POLYGON = "eip155:137";
|
|
249
|
-
var OPTIMISM = "eip155:10";
|
|
250
|
-
var AVALANCHE = "eip155:43114";
|
|
251
|
-
var BSC_MAINNET = "eip155:56";
|
|
252
|
-
var SKALE_BASE = "eip155:1187947933";
|
|
253
|
-
var SKALE_BASE_SEPOLIA = "eip155:324705682";
|
|
254
|
-
var ETHEREUM_MAINNET = "eip155:1";
|
|
255
|
-
var BSC_USDT = "0x55d398326f99059fF775485246999027B3197955";
|
|
256
|
-
var BSC_USDC = "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d";
|
|
257
|
-
var CHAIN_IDS = {
|
|
258
|
-
[BSC_MAINNET]: 56,
|
|
259
|
-
[BASE_MAINNET]: 8453,
|
|
260
|
-
[BASE_SEPOLIA]: 84532,
|
|
261
|
-
[ARBITRUM_ONE]: 42161,
|
|
262
|
-
[POLYGON]: 137,
|
|
263
|
-
[OPTIMISM]: 10,
|
|
264
|
-
[AVALANCHE]: 43114,
|
|
265
|
-
[SKALE_BASE]: 1187947933,
|
|
266
|
-
[SKALE_BASE_SEPOLIA]: 324705682,
|
|
267
|
-
[ETHEREUM_MAINNET]: 1
|
|
268
|
-
};
|
|
269
|
-
var DEFAULT_RPC_URLS2 = {
|
|
270
|
-
[BSC_MAINNET]: "https://bsc-dataseed1.binance.org",
|
|
271
|
-
[BASE_MAINNET]: "https://api.dexter.cash/api/base/rpc",
|
|
272
|
-
[BASE_SEPOLIA]: "https://sepolia.base.org",
|
|
273
|
-
[ARBITRUM_ONE]: "https://arb1.arbitrum.io/rpc",
|
|
274
|
-
[POLYGON]: "https://polygon-rpc.com",
|
|
275
|
-
[OPTIMISM]: "https://mainnet.optimism.io",
|
|
276
|
-
[AVALANCHE]: "https://api.avax.network/ext/bc/C/rpc",
|
|
277
|
-
[SKALE_BASE]: "https://skale-base.skalenodes.com/v1/base",
|
|
278
|
-
[SKALE_BASE_SEPOLIA]: "https://base-sepolia-testnet.skalenodes.com/v1/jubilant-horrible-ancha",
|
|
279
|
-
[ETHEREUM_MAINNET]: "https://eth.llamarpc.com"
|
|
280
|
-
};
|
|
281
|
-
var USDC_ADDRESSES = {
|
|
282
|
-
[BSC_MAINNET]: BSC_USDC,
|
|
283
|
-
[BASE_MAINNET]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
284
|
-
[BASE_SEPOLIA]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
285
|
-
[ARBITRUM_ONE]: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
286
|
-
[POLYGON]: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
287
|
-
[OPTIMISM]: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",
|
|
288
|
-
[AVALANCHE]: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
|
|
289
|
-
[SKALE_BASE]: "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20",
|
|
290
|
-
[SKALE_BASE_SEPOLIA]: "0x2e08028E3C4c2356572E096d8EF835cD5C6030bD",
|
|
291
|
-
[ETHEREUM_MAINNET]: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
|
292
|
-
};
|
|
293
|
-
var BSC_STABLECOIN_ADDRESSES = {
|
|
294
|
-
[BSC_USDT]: { symbol: "USDT", decimals: 18 },
|
|
295
|
-
[BSC_USDC]: { symbol: "USDC", decimals: 18 }
|
|
296
|
-
};
|
|
297
|
-
function isEvmWallet(wallet) {
|
|
298
|
-
if (!wallet || typeof wallet !== "object") return false;
|
|
299
|
-
const w = wallet;
|
|
300
|
-
return "address" in w && typeof w.address === "string" && w.address.startsWith("0x");
|
|
301
|
-
}
|
|
302
|
-
var EvmAdapter = class {
|
|
303
|
-
name = "EVM";
|
|
304
|
-
networks = [BSC_MAINNET, BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE];
|
|
305
|
-
config;
|
|
306
|
-
log;
|
|
307
|
-
constructor(config = {}) {
|
|
308
|
-
this.config = config;
|
|
309
|
-
this.log = config.verbose ? console.log.bind(console, "[x402:evm]") : () => {
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
canHandle(network) {
|
|
313
|
-
if (this.networks.includes(network)) return true;
|
|
314
|
-
if (network === "base") return true;
|
|
315
|
-
if (network === "bsc") return true;
|
|
316
|
-
if (network === "ethereum") return true;
|
|
317
|
-
if (network === "arbitrum") return true;
|
|
318
|
-
if (network.startsWith("eip155:")) return true;
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
getDefaultRpcUrl(network) {
|
|
322
|
-
if (this.config.rpcUrls?.[network]) {
|
|
323
|
-
return this.config.rpcUrls[network];
|
|
324
|
-
}
|
|
325
|
-
if (DEFAULT_RPC_URLS2[network]) {
|
|
326
|
-
return DEFAULT_RPC_URLS2[network];
|
|
327
|
-
}
|
|
328
|
-
if (network === "base") return DEFAULT_RPC_URLS2[BASE_MAINNET];
|
|
329
|
-
if (network === "bsc") return DEFAULT_RPC_URLS2[BSC_MAINNET];
|
|
330
|
-
if (network === "ethereum") return DEFAULT_RPC_URLS2[ETHEREUM_MAINNET];
|
|
331
|
-
if (network === "arbitrum") return DEFAULT_RPC_URLS2[ARBITRUM_ONE];
|
|
332
|
-
return DEFAULT_RPC_URLS2[BASE_MAINNET];
|
|
333
|
-
}
|
|
334
|
-
getAddress(wallet) {
|
|
335
|
-
if (!isEvmWallet(wallet)) return null;
|
|
336
|
-
return wallet.address;
|
|
337
|
-
}
|
|
338
|
-
isConnected(wallet) {
|
|
339
|
-
if (!isEvmWallet(wallet)) return false;
|
|
340
|
-
return !!wallet.address;
|
|
341
|
-
}
|
|
342
|
-
getChainId(network) {
|
|
343
|
-
if (CHAIN_IDS[network]) return CHAIN_IDS[network];
|
|
344
|
-
if (network.startsWith("eip155:")) {
|
|
345
|
-
const chainIdStr = network.split(":")[1];
|
|
346
|
-
return parseInt(chainIdStr, 10);
|
|
347
|
-
}
|
|
348
|
-
if (network === "base") return 8453;
|
|
349
|
-
if (network === "bsc") return 56;
|
|
350
|
-
if (network === "ethereum") return 1;
|
|
351
|
-
if (network === "arbitrum") return 42161;
|
|
352
|
-
return 8453;
|
|
353
|
-
}
|
|
354
|
-
async getBalance(accept, wallet, rpcUrl) {
|
|
355
|
-
if (!isEvmWallet(wallet) || !wallet.address) {
|
|
356
|
-
return 0;
|
|
357
|
-
}
|
|
358
|
-
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
359
|
-
try {
|
|
360
|
-
const data = this.encodeBalanceOf(wallet.address);
|
|
361
|
-
const response = await fetch(url, {
|
|
362
|
-
method: "POST",
|
|
363
|
-
headers: { "Content-Type": "application/json" },
|
|
364
|
-
body: JSON.stringify({
|
|
365
|
-
jsonrpc: "2.0",
|
|
366
|
-
id: 1,
|
|
367
|
-
method: "eth_call",
|
|
368
|
-
params: [
|
|
369
|
-
{
|
|
370
|
-
to: accept.asset,
|
|
371
|
-
data
|
|
372
|
-
},
|
|
373
|
-
"latest"
|
|
374
|
-
]
|
|
375
|
-
})
|
|
376
|
-
});
|
|
377
|
-
if (!response.ok) {
|
|
378
|
-
throw new Error(`RPC request failed: ${response.status}`);
|
|
379
|
-
}
|
|
380
|
-
const result = await response.json();
|
|
381
|
-
if (result.error) {
|
|
382
|
-
throw new Error(`RPC error: ${JSON.stringify(result.error)}`);
|
|
383
|
-
}
|
|
384
|
-
if (!result.result || result.result === "0x") {
|
|
385
|
-
return 0;
|
|
386
|
-
}
|
|
387
|
-
const balance = BigInt(result.result);
|
|
388
|
-
const decimals = accept.extra?.decimals ?? 6;
|
|
389
|
-
return Number(balance) / Math.pow(10, decimals);
|
|
390
|
-
} catch (err) {
|
|
391
|
-
throw err;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
encodeBalanceOf(address) {
|
|
395
|
-
const selector = "0x70a08231";
|
|
396
|
-
const paddedAddress = address.slice(2).toLowerCase().padStart(64, "0");
|
|
397
|
-
return selector + paddedAddress;
|
|
398
|
-
}
|
|
399
|
-
async buildTransaction(accept, wallet, rpcUrl) {
|
|
400
|
-
if (!isEvmWallet(wallet)) {
|
|
401
|
-
throw new Error("Invalid EVM wallet");
|
|
402
|
-
}
|
|
403
|
-
if (!wallet.address) {
|
|
404
|
-
throw new Error("Wallet not connected");
|
|
405
|
-
}
|
|
406
|
-
if (accept.scheme === "exact-approval") {
|
|
407
|
-
return this.buildApprovalTransaction(accept, wallet, rpcUrl);
|
|
408
|
-
}
|
|
409
|
-
if (accept.extra?.assetTransferMethod === "permit2") {
|
|
410
|
-
return this.buildPermit2Transaction(accept, wallet, rpcUrl);
|
|
411
|
-
}
|
|
412
|
-
const { payTo, asset, extra } = accept;
|
|
413
|
-
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
414
|
-
if (!amount) {
|
|
415
|
-
throw new Error("Missing amount in payment requirements");
|
|
416
|
-
}
|
|
417
|
-
this.log("Building EVM transaction:", {
|
|
418
|
-
from: wallet.address,
|
|
419
|
-
to: payTo,
|
|
420
|
-
amount,
|
|
421
|
-
asset,
|
|
422
|
-
network: accept.network
|
|
423
|
-
});
|
|
424
|
-
const chainId = this.getChainId(accept.network);
|
|
425
|
-
const domain = {
|
|
426
|
-
name: extra?.name ?? "USD Coin",
|
|
427
|
-
version: extra?.version ?? "2",
|
|
428
|
-
chainId: BigInt(chainId),
|
|
429
|
-
verifyingContract: asset
|
|
430
|
-
};
|
|
431
|
-
const types = {
|
|
432
|
-
TransferWithAuthorization: [
|
|
433
|
-
{ name: "from", type: "address" },
|
|
434
|
-
{ name: "to", type: "address" },
|
|
435
|
-
{ name: "value", type: "uint256" },
|
|
436
|
-
{ name: "validAfter", type: "uint256" },
|
|
437
|
-
{ name: "validBefore", type: "uint256" },
|
|
438
|
-
{ name: "nonce", type: "bytes32" }
|
|
439
|
-
]
|
|
440
|
-
};
|
|
441
|
-
const nonceBytes = new Uint8Array(32);
|
|
442
|
-
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(nonceBytes);
|
|
443
|
-
const nonce = "0x" + [...nonceBytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
444
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
445
|
-
const authorization = {
|
|
446
|
-
from: wallet.address,
|
|
447
|
-
to: payTo,
|
|
448
|
-
value: amount,
|
|
449
|
-
// string
|
|
450
|
-
validAfter: String(now - 600),
|
|
451
|
-
// 10 minutes before (matching upstream)
|
|
452
|
-
validBefore: String(now + (accept.maxTimeoutSeconds || 60)),
|
|
453
|
-
nonce
|
|
454
|
-
};
|
|
455
|
-
const message = {
|
|
456
|
-
from: wallet.address,
|
|
457
|
-
to: payTo,
|
|
458
|
-
value: BigInt(amount),
|
|
459
|
-
validAfter: BigInt(now - 600),
|
|
460
|
-
validBefore: BigInt(now + (accept.maxTimeoutSeconds || 60)),
|
|
461
|
-
nonce
|
|
462
|
-
};
|
|
463
|
-
if (!wallet.signTypedData) {
|
|
464
|
-
throw new Error("Wallet does not support signTypedData (EIP-712)");
|
|
465
|
-
}
|
|
466
|
-
const signature = await wallet.signTypedData({
|
|
467
|
-
domain,
|
|
468
|
-
types,
|
|
469
|
-
primaryType: "TransferWithAuthorization",
|
|
470
|
-
message
|
|
471
|
-
});
|
|
472
|
-
this.log("EIP-712 signature obtained");
|
|
473
|
-
const payload = {
|
|
474
|
-
authorization,
|
|
475
|
-
signature
|
|
476
|
-
};
|
|
477
|
-
return {
|
|
478
|
-
serialized: JSON.stringify(payload),
|
|
479
|
-
signature
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
// ===========================================================================
|
|
483
|
-
// exact-approval: BSC and other chains without EIP-3009
|
|
484
|
-
// ===========================================================================
|
|
485
|
-
/**
|
|
486
|
-
* Build a payment transaction for chains that use the approval-based scheme.
|
|
487
|
-
* The facilitator's /supported response provides the EIP-712 domain and types
|
|
488
|
-
* in accept.extra, so the client doesn't hardcode any contract addresses.
|
|
489
|
-
*/
|
|
490
|
-
async buildApprovalTransaction(accept, wallet, rpcUrl) {
|
|
491
|
-
const { payTo, asset, extra } = accept;
|
|
492
|
-
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
493
|
-
if (!amount) {
|
|
494
|
-
throw new Error("Missing amount in payment requirements");
|
|
495
|
-
}
|
|
496
|
-
const facilitatorContract = extra?.facilitatorContract;
|
|
497
|
-
if (!facilitatorContract) {
|
|
498
|
-
throw new Error(
|
|
499
|
-
"exact-approval scheme requires extra.facilitatorContract from the facilitator. The /supported endpoint should provide this."
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
if (!wallet.signTypedData) {
|
|
503
|
-
throw new Error("Wallet does not support signTypedData (EIP-712)");
|
|
504
|
-
}
|
|
505
|
-
this.log("Building approval-based transaction:", {
|
|
506
|
-
from: wallet.address,
|
|
507
|
-
to: payTo,
|
|
508
|
-
amount,
|
|
509
|
-
asset,
|
|
510
|
-
network: accept.network,
|
|
511
|
-
facilitatorContract
|
|
512
|
-
});
|
|
513
|
-
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
514
|
-
const fee = extra?.fee ?? "0";
|
|
515
|
-
const totalNeeded = BigInt(amount) + BigInt(fee);
|
|
516
|
-
const currentAllowance = await this.readAllowance(url, asset, wallet.address, facilitatorContract);
|
|
517
|
-
if (currentAllowance < totalNeeded) {
|
|
518
|
-
if (!wallet.sendTransaction) {
|
|
519
|
-
throw new Error(
|
|
520
|
-
"BSC payments require a wallet that supports sendTransaction for the one-time token approval. Use createEvmKeypairWallet() or a browser wallet with transaction support."
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
const approvalAmount = this.calculateApprovalAmount(amount, fee, extra?.approvalStrategy);
|
|
524
|
-
this.log(`Approving ${approvalAmount} for ${facilitatorContract} (current allowance: ${currentAllowance})`);
|
|
525
|
-
const approveTxHash = await wallet.sendTransaction({
|
|
526
|
-
to: asset,
|
|
527
|
-
data: this.encodeApprove(facilitatorContract, approvalAmount),
|
|
528
|
-
value: 0n
|
|
529
|
-
});
|
|
530
|
-
this.log(`Approval tx sent: ${approveTxHash}`);
|
|
531
|
-
await this.waitForReceipt(url, approveTxHash);
|
|
532
|
-
this.log("Approval confirmed");
|
|
533
|
-
} else {
|
|
534
|
-
this.log("Sufficient allowance, skipping approval");
|
|
535
|
-
}
|
|
536
|
-
const nonceBytes = new Uint8Array(16);
|
|
537
|
-
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(nonceBytes);
|
|
538
|
-
const nonce = [...nonceBytes].reduce((acc, b) => acc * 256n + BigInt(b), 0n).toString();
|
|
539
|
-
const paymentIdBytes = new Uint8Array(32);
|
|
540
|
-
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(paymentIdBytes);
|
|
541
|
-
const paymentId = "0x" + [...paymentIdBytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
542
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
543
|
-
const deadline = now + (accept.maxTimeoutSeconds || 300);
|
|
544
|
-
const eip712Domain = extra?.eip712Domain;
|
|
545
|
-
const domain = eip712Domain ? {
|
|
546
|
-
name: eip712Domain.name,
|
|
547
|
-
version: eip712Domain.version,
|
|
548
|
-
chainId: BigInt(eip712Domain.chainId),
|
|
549
|
-
verifyingContract: eip712Domain.verifyingContract
|
|
550
|
-
} : {
|
|
551
|
-
name: "DexterBSCFacilitator",
|
|
552
|
-
version: "1",
|
|
553
|
-
chainId: BigInt(this.getChainId(accept.network)),
|
|
554
|
-
verifyingContract: facilitatorContract
|
|
555
|
-
};
|
|
556
|
-
const types = extra?.eip712Types ?? {
|
|
557
|
-
Payment: [
|
|
558
|
-
{ name: "from", type: "address" },
|
|
559
|
-
{ name: "to", type: "address" },
|
|
560
|
-
{ name: "token", type: "address" },
|
|
561
|
-
{ name: "amount", type: "uint256" },
|
|
562
|
-
{ name: "fee", type: "uint256" },
|
|
563
|
-
{ name: "nonce", type: "uint256" },
|
|
564
|
-
{ name: "deadline", type: "uint256" },
|
|
565
|
-
{ name: "paymentId", type: "bytes32" }
|
|
566
|
-
]
|
|
567
|
-
};
|
|
568
|
-
const message = {
|
|
569
|
-
from: wallet.address,
|
|
570
|
-
to: payTo,
|
|
571
|
-
token: asset,
|
|
572
|
-
amount: BigInt(amount),
|
|
573
|
-
fee: BigInt(fee),
|
|
574
|
-
nonce: BigInt(nonce),
|
|
575
|
-
deadline: BigInt(deadline),
|
|
576
|
-
paymentId
|
|
577
|
-
};
|
|
578
|
-
const signature = await wallet.signTypedData({
|
|
579
|
-
domain,
|
|
580
|
-
types,
|
|
581
|
-
primaryType: "Payment",
|
|
582
|
-
message
|
|
583
|
-
});
|
|
584
|
-
this.log("EIP-712 Payment signature obtained");
|
|
585
|
-
const payload = {
|
|
586
|
-
from: wallet.address,
|
|
587
|
-
to: payTo,
|
|
588
|
-
token: asset,
|
|
589
|
-
amount,
|
|
590
|
-
fee,
|
|
591
|
-
nonce,
|
|
592
|
-
deadline,
|
|
593
|
-
paymentId,
|
|
594
|
-
signature
|
|
595
|
-
};
|
|
596
|
-
return {
|
|
597
|
-
serialized: JSON.stringify(payload),
|
|
598
|
-
signature
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
// ===========================================================================
|
|
602
|
-
// Permit2: Universal ERC-20 payments via Uniswap's Permit2 contract
|
|
603
|
-
// ===========================================================================
|
|
604
|
-
/**
|
|
605
|
-
* Build a Permit2 payment transaction. Used when the facilitator signals
|
|
606
|
-
* assetTransferMethod: "permit2" in extra (e.g., BSC where EIP-3009 is unavailable).
|
|
607
|
-
*
|
|
608
|
-
* Flow:
|
|
609
|
-
* 1. Check if token has approved the Permit2 contract. If not, approve(Permit2, maxUint256).
|
|
610
|
-
* 2. Sign EIP-712 PermitWitnessTransferFrom against the Permit2 contract.
|
|
611
|
-
* 3. Return { permit2Authorization, signature } payload for the facilitator.
|
|
612
|
-
*/
|
|
613
|
-
async buildPermit2Transaction(accept, wallet, rpcUrl) {
|
|
614
|
-
const { payTo, asset } = accept;
|
|
615
|
-
const amount = accept.amount ?? accept.maxAmountRequired;
|
|
616
|
-
if (!amount) {
|
|
617
|
-
throw new Error("Missing amount in payment requirements");
|
|
618
|
-
}
|
|
619
|
-
if (!wallet.signTypedData) {
|
|
620
|
-
throw new Error("Wallet does not support signTypedData (EIP-712)");
|
|
621
|
-
}
|
|
622
|
-
this.log("Building Permit2 transaction:", {
|
|
623
|
-
from: wallet.address,
|
|
624
|
-
to: payTo,
|
|
625
|
-
amount,
|
|
626
|
-
asset,
|
|
627
|
-
network: accept.network
|
|
628
|
-
});
|
|
629
|
-
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
630
|
-
const currentAllowance = await this.readAllowance(url, asset, wallet.address, PERMIT2_ADDRESS);
|
|
631
|
-
let approvalExtension;
|
|
632
|
-
if (currentAllowance < BigInt(amount)) {
|
|
633
|
-
const approveData = this.encodeApprove(PERMIT2_ADDRESS, MAX_UINT256);
|
|
634
|
-
if (wallet.signTransaction) {
|
|
635
|
-
this.log(`Signing Permit2 approval for relay (current allowance: ${currentAllowance})`);
|
|
636
|
-
const chainId2 = this.getChainId(accept.network);
|
|
637
|
-
const gasPrice = await this.readGasPrice(url);
|
|
638
|
-
const nonce2 = await this.readNonce(url, wallet.address);
|
|
639
|
-
const signedTx = await wallet.signTransaction({
|
|
640
|
-
to: asset,
|
|
641
|
-
data: approveData,
|
|
642
|
-
chainId: chainId2,
|
|
643
|
-
gas: 50000n,
|
|
644
|
-
// standard ERC-20 approve
|
|
645
|
-
gasPrice,
|
|
646
|
-
nonce: nonce2
|
|
647
|
-
});
|
|
648
|
-
approvalExtension = {
|
|
649
|
-
erc20ApprovalGasSponsoring: {
|
|
650
|
-
info: {
|
|
651
|
-
from: wallet.address,
|
|
652
|
-
asset,
|
|
653
|
-
spender: PERMIT2_ADDRESS,
|
|
654
|
-
amount: MAX_UINT256.toString(),
|
|
655
|
-
signedTransaction: signedTx,
|
|
656
|
-
version: "1"
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
};
|
|
660
|
-
this.log("Permit2 approval signed for facilitator relay");
|
|
661
|
-
} else if (wallet.sendTransaction) {
|
|
662
|
-
this.log(`Approving Permit2 directly (current allowance: ${currentAllowance})`);
|
|
663
|
-
const approveTxHash = await wallet.sendTransaction({
|
|
664
|
-
to: asset,
|
|
665
|
-
data: approveData,
|
|
666
|
-
value: 0n
|
|
667
|
-
});
|
|
668
|
-
this.log(`Permit2 approval tx sent: ${approveTxHash}`);
|
|
669
|
-
await this.waitForReceipt(url, approveTxHash);
|
|
670
|
-
this.log("Permit2 approval confirmed");
|
|
671
|
-
} else {
|
|
672
|
-
throw new Error(
|
|
673
|
-
"Permit2 payments require a wallet that supports signTransaction or sendTransaction for the one-time Permit2 approval. Use createEvmKeypairWallet() or a browser wallet with transaction support."
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
} else {
|
|
677
|
-
this.log("Sufficient Permit2 allowance, skipping approval");
|
|
678
|
-
}
|
|
679
|
-
const nonceBytes = new Uint8Array(32);
|
|
680
|
-
(globalThis.crypto ?? (await import("crypto")).webcrypto).getRandomValues(nonceBytes);
|
|
681
|
-
const nonce = [...nonceBytes].reduce((acc, b) => acc * 256n + BigInt(b), 0n);
|
|
682
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
683
|
-
const validAfter = now - 600;
|
|
684
|
-
const deadline = now + (accept.maxTimeoutSeconds || 300);
|
|
685
|
-
const chainId = this.getChainId(accept.network);
|
|
686
|
-
const domain = {
|
|
687
|
-
name: "Permit2",
|
|
688
|
-
chainId: BigInt(chainId),
|
|
689
|
-
verifyingContract: PERMIT2_ADDRESS
|
|
690
|
-
};
|
|
691
|
-
const message = {
|
|
692
|
-
permitted: {
|
|
693
|
-
token: asset,
|
|
694
|
-
amount: BigInt(amount)
|
|
695
|
-
},
|
|
696
|
-
spender: X402_EXACT_PERMIT2_PROXY,
|
|
697
|
-
nonce,
|
|
698
|
-
deadline: BigInt(deadline),
|
|
699
|
-
witness: {
|
|
700
|
-
to: payTo,
|
|
701
|
-
validAfter: BigInt(validAfter)
|
|
702
|
-
}
|
|
703
|
-
};
|
|
704
|
-
const signature = await wallet.signTypedData({
|
|
705
|
-
domain,
|
|
706
|
-
types: PERMIT2_WITNESS_TYPES,
|
|
707
|
-
primaryType: "PermitWitnessTransferFrom",
|
|
708
|
-
message
|
|
709
|
-
});
|
|
710
|
-
this.log("Permit2 PermitWitnessTransferFrom signature obtained");
|
|
711
|
-
const payload = {
|
|
712
|
-
signature,
|
|
713
|
-
permit2Authorization: {
|
|
714
|
-
from: wallet.address,
|
|
715
|
-
permitted: {
|
|
716
|
-
token: asset,
|
|
717
|
-
amount
|
|
718
|
-
},
|
|
719
|
-
spender: X402_EXACT_PERMIT2_PROXY,
|
|
720
|
-
nonce: nonce.toString(),
|
|
721
|
-
deadline: String(deadline),
|
|
722
|
-
witness: {
|
|
723
|
-
to: payTo,
|
|
724
|
-
validAfter: String(validAfter)
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
};
|
|
728
|
-
return {
|
|
729
|
-
serialized: JSON.stringify(payload),
|
|
730
|
-
signature,
|
|
731
|
-
extensions: approvalExtension
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
/**
|
|
735
|
-
* Read ERC-20 allowance via raw eth_call (no viem dependency needed).
|
|
736
|
-
*/
|
|
737
|
-
async readAllowance(rpcUrl, token, owner, spender) {
|
|
738
|
-
const selector = "0xdd62ed3e";
|
|
739
|
-
const paddedOwner = owner.slice(2).toLowerCase().padStart(64, "0");
|
|
740
|
-
const paddedSpender = spender.slice(2).toLowerCase().padStart(64, "0");
|
|
741
|
-
const data = selector + paddedOwner + paddedSpender;
|
|
742
|
-
try {
|
|
743
|
-
const response = await fetch(rpcUrl, {
|
|
744
|
-
method: "POST",
|
|
745
|
-
headers: { "Content-Type": "application/json" },
|
|
746
|
-
body: JSON.stringify({
|
|
747
|
-
jsonrpc: "2.0",
|
|
748
|
-
id: 1,
|
|
749
|
-
method: "eth_call",
|
|
750
|
-
params: [{ to: token, data }, "latest"]
|
|
751
|
-
})
|
|
752
|
-
});
|
|
753
|
-
const result = await response.json();
|
|
754
|
-
if (result.error || !result.result || result.result === "0x") return 0n;
|
|
755
|
-
return BigInt(result.result);
|
|
756
|
-
} catch {
|
|
757
|
-
return 0n;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* Encode ERC-20 approve(address,uint256) calldata.
|
|
762
|
-
*/
|
|
763
|
-
encodeApprove(spender, amount) {
|
|
764
|
-
const selector = "0x095ea7b3";
|
|
765
|
-
const paddedSpender = spender.slice(2).toLowerCase().padStart(64, "0");
|
|
766
|
-
const paddedAmount = amount.toString(16).padStart(64, "0");
|
|
767
|
-
return selector + paddedSpender + paddedAmount;
|
|
768
|
-
}
|
|
769
|
-
/**
|
|
770
|
-
* Wait for a transaction receipt by polling eth_getTransactionReceipt.
|
|
771
|
-
*/
|
|
772
|
-
async waitForReceipt(rpcUrl, txHash, timeoutMs = 3e4) {
|
|
773
|
-
const start = Date.now();
|
|
774
|
-
while (Date.now() - start < timeoutMs) {
|
|
775
|
-
try {
|
|
776
|
-
const response = await fetch(rpcUrl, {
|
|
777
|
-
method: "POST",
|
|
778
|
-
headers: { "Content-Type": "application/json" },
|
|
779
|
-
body: JSON.stringify({
|
|
780
|
-
jsonrpc: "2.0",
|
|
781
|
-
id: 1,
|
|
782
|
-
method: "eth_getTransactionReceipt",
|
|
783
|
-
params: [txHash]
|
|
784
|
-
})
|
|
785
|
-
});
|
|
786
|
-
const result = await response.json();
|
|
787
|
-
if (result.result) {
|
|
788
|
-
if (result.result.status === "0x0") {
|
|
789
|
-
throw new Error(`Approval transaction reverted: ${txHash}`);
|
|
790
|
-
}
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
} catch (err) {
|
|
794
|
-
if (err instanceof Error && err.message.includes("reverted")) throw err;
|
|
795
|
-
}
|
|
796
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
797
|
-
}
|
|
798
|
-
throw new Error(`Approval transaction receipt timeout after ${timeoutMs}ms: ${txHash}`);
|
|
799
|
-
}
|
|
800
|
-
/**
|
|
801
|
-
* Read gas price via eth_gasPrice RPC call.
|
|
802
|
-
*/
|
|
803
|
-
async readGasPrice(rpcUrl) {
|
|
804
|
-
try {
|
|
805
|
-
const response = await fetch(rpcUrl, {
|
|
806
|
-
method: "POST",
|
|
807
|
-
headers: { "Content-Type": "application/json" },
|
|
808
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_gasPrice", params: [] })
|
|
809
|
-
});
|
|
810
|
-
const result = await response.json();
|
|
811
|
-
return result.result ? BigInt(result.result) : 50000000n;
|
|
812
|
-
} catch {
|
|
813
|
-
return 50000000n;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Read transaction count (nonce) via eth_getTransactionCount RPC call.
|
|
818
|
-
*/
|
|
819
|
-
async readNonce(rpcUrl, address) {
|
|
820
|
-
try {
|
|
821
|
-
const response = await fetch(rpcUrl, {
|
|
822
|
-
method: "POST",
|
|
823
|
-
headers: { "Content-Type": "application/json" },
|
|
824
|
-
body: JSON.stringify({
|
|
825
|
-
jsonrpc: "2.0",
|
|
826
|
-
id: 1,
|
|
827
|
-
method: "eth_getTransactionCount",
|
|
828
|
-
params: [address, "latest"]
|
|
829
|
-
})
|
|
830
|
-
});
|
|
831
|
-
const result = await response.json();
|
|
832
|
-
return result.result ? parseInt(result.result, 16) : 0;
|
|
833
|
-
} catch {
|
|
834
|
-
return 0;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
/**
|
|
838
|
-
* Calculate how much to approve based on the facilitator's approval strategy.
|
|
839
|
-
* Buffered approvals reduce the number of on-chain approval txs for micropayments.
|
|
840
|
-
*/
|
|
841
|
-
calculateApprovalAmount(paymentAmount, fee, strategy) {
|
|
842
|
-
const total = BigInt(paymentAmount) + BigInt(fee);
|
|
843
|
-
if (!strategy || strategy.mode === "exact") {
|
|
844
|
-
return total;
|
|
845
|
-
}
|
|
846
|
-
const multiple = BigInt(strategy.defaultMultiple ?? 10);
|
|
847
|
-
const buffered = total * multiple;
|
|
848
|
-
if (strategy.maxCapUsd) {
|
|
849
|
-
const decimals = this.inferDecimals(paymentAmount);
|
|
850
|
-
const maxCap = BigInt(Math.floor(strategy.maxCapUsd * Math.pow(10, decimals)));
|
|
851
|
-
if (buffered > maxCap) return maxCap;
|
|
852
|
-
}
|
|
853
|
-
if (strategy.exactAboveUsd) {
|
|
854
|
-
const decimals = this.inferDecimals(paymentAmount);
|
|
855
|
-
const threshold = BigInt(Math.floor(strategy.exactAboveUsd * Math.pow(10, decimals)));
|
|
856
|
-
if (BigInt(paymentAmount) > threshold) return total;
|
|
857
|
-
}
|
|
858
|
-
return buffered;
|
|
859
|
-
}
|
|
860
|
-
/**
|
|
861
|
-
* Infer token decimals from payment amount magnitude.
|
|
862
|
-
* BSC stablecoins use 18 decimals, all others use 6.
|
|
863
|
-
* A $1 payment is 1000000 (6 dec) or 1000000000000000000 (18 dec).
|
|
864
|
-
* If the amount has > 12 digits, it's almost certainly 18 decimals.
|
|
865
|
-
*/
|
|
866
|
-
inferDecimals(amount) {
|
|
867
|
-
return amount.length > 12 ? 18 : 6;
|
|
868
|
-
}
|
|
869
|
-
};
|
|
870
|
-
function createEvmAdapter(config) {
|
|
871
|
-
return new EvmAdapter(config);
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// src/adapters/index.ts
|
|
875
|
-
function isKnownUSDC(asset) {
|
|
876
|
-
if (asset === "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") return true;
|
|
877
|
-
if (asset === "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU") return true;
|
|
878
|
-
const lc = asset.toLowerCase();
|
|
879
|
-
for (const addr of Object.values(USDC_ADDRESSES)) {
|
|
880
|
-
if (addr.toLowerCase() === lc) return true;
|
|
881
|
-
}
|
|
882
|
-
for (const addr of Object.keys(BSC_STABLECOIN_ADDRESSES)) {
|
|
883
|
-
if (addr.toLowerCase() === lc) return true;
|
|
884
|
-
}
|
|
885
|
-
return false;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// src/client/x402-client.ts
|
|
889
|
-
var receiptStore = /* @__PURE__ */ new WeakMap();
|
|
890
|
-
function getPaymentReceipt(response) {
|
|
891
|
-
return receiptStore.get(response);
|
|
892
|
-
}
|
|
893
|
-
function createX402Client(config) {
|
|
894
|
-
const {
|
|
895
|
-
adapters = [createSolanaAdapter({ verbose: config.verbose }), createEvmAdapter({ verbose: config.verbose })],
|
|
896
|
-
wallets: walletSet,
|
|
897
|
-
wallet: legacyWallet,
|
|
898
|
-
preferredNetwork,
|
|
899
|
-
rpcUrls = {},
|
|
900
|
-
maxAmountAtomic,
|
|
901
|
-
fetch: customFetch = globalThis.fetch,
|
|
902
|
-
verbose = false,
|
|
903
|
-
accessPass: accessPassConfig,
|
|
904
|
-
onPaymentRequired,
|
|
905
|
-
maxRetries = 0,
|
|
906
|
-
retryDelayMs = 500
|
|
907
|
-
} = config;
|
|
908
|
-
const log = verbose ? console.log.bind(console, "[x402]") : () => {
|
|
909
|
-
};
|
|
910
|
-
async function fetchWithRetry(input, init) {
|
|
911
|
-
let lastError;
|
|
912
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
913
|
-
try {
|
|
914
|
-
const response = await customFetch(input, init);
|
|
915
|
-
if (response.status >= 502 && response.status <= 504 && attempt < maxRetries) {
|
|
916
|
-
log(`Retry ${attempt + 1}/${maxRetries}: server returned ${response.status}`);
|
|
917
|
-
await new Promise((r) => setTimeout(r, retryDelayMs * Math.pow(2, attempt)));
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
return response;
|
|
921
|
-
} catch (err) {
|
|
922
|
-
lastError = err;
|
|
923
|
-
if (attempt < maxRetries) {
|
|
924
|
-
log(`Retry ${attempt + 1}/${maxRetries}: ${err instanceof Error ? err.message : "network error"}`);
|
|
925
|
-
await new Promise((r) => setTimeout(r, retryDelayMs * Math.pow(2, attempt)));
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
throw lastError;
|
|
930
|
-
}
|
|
931
|
-
const passCache = /* @__PURE__ */ new Map();
|
|
932
|
-
function getCachedPass(url) {
|
|
933
|
-
try {
|
|
934
|
-
const host = new URL(url).host;
|
|
935
|
-
const cached = passCache.get(host);
|
|
936
|
-
if (cached && cached.expiresAt > Date.now() / 1e3 + 10) {
|
|
937
|
-
return cached.jwt;
|
|
938
|
-
}
|
|
939
|
-
if (cached) {
|
|
940
|
-
passCache.delete(host);
|
|
941
|
-
}
|
|
942
|
-
} catch {
|
|
943
|
-
}
|
|
944
|
-
return null;
|
|
945
|
-
}
|
|
946
|
-
function cachePass(url, jwt) {
|
|
947
|
-
try {
|
|
948
|
-
const host = new URL(url).host;
|
|
949
|
-
const parts = jwt.split(".");
|
|
950
|
-
if (parts.length === 3) {
|
|
951
|
-
const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
952
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
953
|
-
const maxExp = now + 86400;
|
|
954
|
-
const expiresAt = Math.min(typeof payload.exp === "number" ? payload.exp : now, maxExp);
|
|
955
|
-
passCache.set(host, { jwt, expiresAt });
|
|
956
|
-
log("Access pass cached for", host, "| expires:", new Date(expiresAt * 1e3).toISOString());
|
|
957
|
-
}
|
|
958
|
-
} catch {
|
|
959
|
-
log("Failed to cache access pass");
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
const wallets = walletSet || {};
|
|
963
|
-
if (legacyWallet && !wallets.solana && isSolanaWallet(legacyWallet)) {
|
|
964
|
-
wallets.solana = legacyWallet;
|
|
965
|
-
}
|
|
966
|
-
if (legacyWallet && !wallets.evm && isEvmWallet(legacyWallet)) {
|
|
967
|
-
wallets.evm = legacyWallet;
|
|
968
|
-
}
|
|
969
|
-
function findPaymentOption(accepts) {
|
|
970
|
-
const candidates = [];
|
|
971
|
-
for (const accept of accepts) {
|
|
972
|
-
const adapter = adapters.find((a) => a.canHandle(accept.network));
|
|
973
|
-
if (!adapter) continue;
|
|
974
|
-
let wallet;
|
|
975
|
-
if (adapter.name === "Solana") {
|
|
976
|
-
wallet = wallets.solana;
|
|
977
|
-
} else if (adapter.name === "EVM") {
|
|
978
|
-
wallet = wallets.evm;
|
|
979
|
-
}
|
|
980
|
-
if (wallet && adapter.isConnected(wallet)) {
|
|
981
|
-
candidates.push({ accept, adapter, wallet });
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
if (candidates.length === 0) {
|
|
985
|
-
return null;
|
|
986
|
-
}
|
|
987
|
-
if (preferredNetwork) {
|
|
988
|
-
const preferred = candidates.find((c) => c.accept.network === preferredNetwork);
|
|
989
|
-
if (preferred) return preferred;
|
|
990
|
-
}
|
|
991
|
-
return candidates[0];
|
|
992
|
-
}
|
|
993
|
-
function getChainDisplayName(network, adapterName) {
|
|
994
|
-
const names = {
|
|
995
|
-
"eip155:56": "BSC",
|
|
996
|
-
"eip155:8453": "Base",
|
|
997
|
-
"eip155:84532": "Base Sepolia",
|
|
998
|
-
"eip155:42161": "Arbitrum",
|
|
999
|
-
"eip155:137": "Polygon",
|
|
1000
|
-
"eip155:10": "Optimism",
|
|
1001
|
-
"eip155:43114": "Avalanche",
|
|
1002
|
-
"eip155:1": "Ethereum"
|
|
1003
|
-
};
|
|
1004
|
-
return names[network] || adapterName;
|
|
1005
|
-
}
|
|
1006
|
-
function getRpcUrl(network, adapter) {
|
|
1007
|
-
return rpcUrls[network] || adapter.getDefaultRpcUrl(network);
|
|
1008
|
-
}
|
|
1009
|
-
async function purchaseAccessPass(input, init, originalResponse, passInfo, url) {
|
|
1010
|
-
let tierQuery = "";
|
|
1011
|
-
if (accessPassConfig?.preferTier && passInfo.tiers) {
|
|
1012
|
-
const match2 = passInfo.tiers.find((t) => t.id === accessPassConfig.preferTier);
|
|
1013
|
-
if (match2) {
|
|
1014
|
-
if (accessPassConfig.maxSpend && parseFloat(match2.price) > parseFloat(accessPassConfig.maxSpend)) {
|
|
1015
|
-
throw new X402Error(
|
|
1016
|
-
"access_pass_exceeds_max_spend",
|
|
1017
|
-
`Access pass tier "${match2.id}" costs $${match2.price}, exceeds max spend $${accessPassConfig.maxSpend}`
|
|
1018
|
-
);
|
|
1019
|
-
}
|
|
1020
|
-
tierQuery = `tier=${match2.id}`;
|
|
1021
|
-
}
|
|
1022
|
-
} else if (accessPassConfig?.preferDuration && passInfo.ratePerHour) {
|
|
1023
|
-
tierQuery = `duration=${accessPassConfig.preferDuration}`;
|
|
1024
|
-
} else if (passInfo.tiers && passInfo.tiers.length > 0) {
|
|
1025
|
-
const cheapest = passInfo.tiers[0];
|
|
1026
|
-
if (accessPassConfig?.maxSpend && parseFloat(cheapest.price) > parseFloat(accessPassConfig.maxSpend)) {
|
|
1027
|
-
throw new X402Error(
|
|
1028
|
-
"access_pass_exceeds_max_spend",
|
|
1029
|
-
`Cheapest access pass costs $${cheapest.price}, exceeds max spend $${accessPassConfig?.maxSpend}`
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
tierQuery = `tier=${cheapest.id}`;
|
|
1033
|
-
}
|
|
1034
|
-
const passUrl = tierQuery ? url.includes("?") ? `${url}&${tierQuery}` : `${url}?${tierQuery}` : url;
|
|
1035
|
-
log("Purchasing access pass:", tierQuery || "default tier");
|
|
1036
|
-
const paymentRequiredHeader = originalResponse.headers.get("PAYMENT-REQUIRED");
|
|
1037
|
-
if (!paymentRequiredHeader) return null;
|
|
1038
|
-
let requirements;
|
|
1039
|
-
try {
|
|
1040
|
-
requirements = JSON.parse(atob(paymentRequiredHeader));
|
|
1041
|
-
} catch {
|
|
1042
|
-
return null;
|
|
1043
|
-
}
|
|
1044
|
-
const match = findPaymentOption(requirements.accepts);
|
|
1045
|
-
if (!match) return null;
|
|
1046
|
-
const { accept, adapter, wallet } = match;
|
|
1047
|
-
if (adapter.name === "Solana" && !accept.extra?.feePayer) return null;
|
|
1048
|
-
const decimals = accept.extra?.decimals ?? (isKnownUSDC(accept.asset) ? 6 : void 0);
|
|
1049
|
-
if (typeof decimals !== "number") return null;
|
|
1050
|
-
const paymentAmount = accept.amount ?? accept.maxAmountRequired;
|
|
1051
|
-
if (!paymentAmount) return null;
|
|
1052
|
-
const rpcUrl = getRpcUrl(accept.network, adapter);
|
|
1053
|
-
try {
|
|
1054
|
-
const balance = await adapter.getBalance(accept, wallet, rpcUrl);
|
|
1055
|
-
const requiredAmount = Number(paymentAmount) / Math.pow(10, decimals);
|
|
1056
|
-
if (balance < requiredAmount) {
|
|
1057
|
-
throw new X402Error(
|
|
1058
|
-
"insufficient_balance",
|
|
1059
|
-
`Insufficient balance for access pass. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1062
|
-
} catch (err) {
|
|
1063
|
-
if (err instanceof X402Error) throw err;
|
|
1064
|
-
}
|
|
1065
|
-
const signedTx = await adapter.buildTransaction(accept, wallet, rpcUrl);
|
|
1066
|
-
let payload;
|
|
1067
|
-
if (adapter.name === "EVM") {
|
|
1068
|
-
payload = JSON.parse(signedTx.serialized);
|
|
1069
|
-
} else {
|
|
1070
|
-
payload = { transaction: signedTx.serialized };
|
|
1071
|
-
}
|
|
1072
|
-
const originalUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1073
|
-
let resolvedResource = requirements.resource;
|
|
1074
|
-
if (typeof requirements.resource === "string") {
|
|
1075
|
-
try {
|
|
1076
|
-
const resolved = new URL(requirements.resource, originalUrl);
|
|
1077
|
-
if (["http:", "https:"].includes(resolved.protocol)) {
|
|
1078
|
-
resolvedResource = resolved.toString();
|
|
1079
|
-
}
|
|
1080
|
-
} catch {
|
|
1081
|
-
}
|
|
1082
|
-
} else if (requirements.resource && typeof requirements.resource === "object" && "url" in requirements.resource) {
|
|
1083
|
-
const rObj = requirements.resource;
|
|
1084
|
-
try {
|
|
1085
|
-
const resolved = new URL(rObj.url, originalUrl);
|
|
1086
|
-
if (["http:", "https:"].includes(resolved.protocol)) {
|
|
1087
|
-
resolvedResource = { ...rObj, url: resolved.toString() };
|
|
1088
|
-
}
|
|
1089
|
-
} catch {
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
const paymentSignature = {
|
|
1093
|
-
x402Version: accept.x402Version ?? 2,
|
|
1094
|
-
resource: resolvedResource,
|
|
1095
|
-
accepted: accept,
|
|
1096
|
-
payload
|
|
1097
|
-
};
|
|
1098
|
-
if (signedTx.extensions) {
|
|
1099
|
-
paymentSignature.extensions = signedTx.extensions;
|
|
1100
|
-
}
|
|
1101
|
-
const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
|
|
1102
|
-
const passResponse = await customFetch(passUrl, {
|
|
1103
|
-
...init,
|
|
1104
|
-
method: "POST",
|
|
1105
|
-
headers: {
|
|
1106
|
-
...init?.headers || {},
|
|
1107
|
-
"Content-Type": "application/json",
|
|
1108
|
-
"PAYMENT-SIGNATURE": paymentSignatureHeader
|
|
1109
|
-
}
|
|
1110
|
-
});
|
|
1111
|
-
if (!passResponse.ok) {
|
|
1112
|
-
log("Pass purchase failed:", passResponse.status);
|
|
1113
|
-
return null;
|
|
1114
|
-
}
|
|
1115
|
-
const accessPassJwt = passResponse.headers.get("ACCESS-PASS");
|
|
1116
|
-
if (accessPassJwt) {
|
|
1117
|
-
cachePass(url, accessPassJwt);
|
|
1118
|
-
log("Access pass purchased and cached");
|
|
1119
|
-
}
|
|
1120
|
-
return passResponse;
|
|
1121
|
-
}
|
|
1122
|
-
async function x402Fetch(input, init) {
|
|
1123
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1124
|
-
log("Making request:", url);
|
|
1125
|
-
if (accessPassConfig) {
|
|
1126
|
-
const cachedJwt = getCachedPass(url);
|
|
1127
|
-
if (cachedJwt) {
|
|
1128
|
-
log("Using cached access pass");
|
|
1129
|
-
const passResponse = await customFetch(input, {
|
|
1130
|
-
...init,
|
|
1131
|
-
headers: {
|
|
1132
|
-
...init?.headers || {},
|
|
1133
|
-
"Authorization": `Bearer ${cachedJwt}`
|
|
1134
|
-
}
|
|
1135
|
-
});
|
|
1136
|
-
if (passResponse.status !== 401 && passResponse.status !== 402) {
|
|
1137
|
-
return passResponse;
|
|
1138
|
-
}
|
|
1139
|
-
log("Cached pass rejected (status", passResponse.status, "), purchasing new pass");
|
|
1140
|
-
try {
|
|
1141
|
-
passCache.delete(new URL(url).host);
|
|
1142
|
-
} catch {
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
const response = await fetchWithRetry(input, init);
|
|
1147
|
-
if (response.status !== 402) {
|
|
1148
|
-
return response;
|
|
1149
|
-
}
|
|
1150
|
-
log("Received 402 Payment Required");
|
|
1151
|
-
const passTiersHeader = response.headers.get("X-ACCESS-PASS-TIERS");
|
|
1152
|
-
if (accessPassConfig && passTiersHeader) {
|
|
1153
|
-
log("Server offers access passes, purchasing...");
|
|
1154
|
-
try {
|
|
1155
|
-
const passInfo = JSON.parse(atob(passTiersHeader));
|
|
1156
|
-
const passResponse = await purchaseAccessPass(input, init, response, passInfo, url);
|
|
1157
|
-
if (passResponse) return passResponse;
|
|
1158
|
-
} catch (e) {
|
|
1159
|
-
log("Access pass purchase failed, falling back to per-request payment:", e);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
const paymentRequiredHeader = response.headers.get("PAYMENT-REQUIRED");
|
|
1163
|
-
if (!paymentRequiredHeader) {
|
|
1164
|
-
throw new X402Error(
|
|
1165
|
-
"missing_payment_required_header",
|
|
1166
|
-
"Server returned 402 but no PAYMENT-REQUIRED header"
|
|
1167
|
-
);
|
|
1168
|
-
}
|
|
1169
|
-
let requirements;
|
|
1170
|
-
try {
|
|
1171
|
-
const decoded = atob(paymentRequiredHeader);
|
|
1172
|
-
requirements = JSON.parse(decoded);
|
|
1173
|
-
} catch {
|
|
1174
|
-
throw new X402Error(
|
|
1175
|
-
"invalid_payment_required",
|
|
1176
|
-
"Failed to decode PAYMENT-REQUIRED header"
|
|
1177
|
-
);
|
|
1178
|
-
}
|
|
1179
|
-
log("Payment requirements:", requirements);
|
|
1180
|
-
const quoteHash = response.headers.get("X-Quote-Hash");
|
|
1181
|
-
if (quoteHash) {
|
|
1182
|
-
log("Quote hash received:", quoteHash);
|
|
1183
|
-
}
|
|
1184
|
-
const match = findPaymentOption(requirements.accepts);
|
|
1185
|
-
if (!match) {
|
|
1186
|
-
const availableNetworks = requirements.accepts.map((a) => a.network).join(", ");
|
|
1187
|
-
throw new X402Error(
|
|
1188
|
-
"no_matching_payment_option",
|
|
1189
|
-
`No connected wallet for any available network: ${availableNetworks}`
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
const { accept, adapter, wallet } = match;
|
|
1193
|
-
log(`Using ${adapter.name} for ${accept.network}`);
|
|
1194
|
-
if (adapter.name === "Solana" && !accept.extra?.feePayer) {
|
|
1195
|
-
throw new X402Error(
|
|
1196
|
-
"missing_fee_payer",
|
|
1197
|
-
"Solana payment option missing feePayer in extra"
|
|
1198
|
-
);
|
|
1199
|
-
}
|
|
1200
|
-
const decimals = accept.extra?.decimals ?? (isKnownUSDC(accept.asset) ? 6 : void 0);
|
|
1201
|
-
if (typeof decimals !== "number") {
|
|
1202
|
-
throw new X402Error(
|
|
1203
|
-
"missing_decimals",
|
|
1204
|
-
"Payment option missing decimals - provide in extra or use a known stablecoin"
|
|
1205
|
-
);
|
|
1206
|
-
}
|
|
1207
|
-
const paymentAmount = accept.amount ?? accept.maxAmountRequired;
|
|
1208
|
-
if (!paymentAmount) {
|
|
1209
|
-
throw new X402Error("missing_amount", "Payment option missing amount");
|
|
1210
|
-
}
|
|
1211
|
-
if (maxAmountAtomic && BigInt(paymentAmount) > BigInt(maxAmountAtomic)) {
|
|
1212
|
-
throw new X402Error(
|
|
1213
|
-
"amount_exceeds_max",
|
|
1214
|
-
`Payment amount ${paymentAmount} exceeds maximum ${maxAmountAtomic}`
|
|
1215
|
-
);
|
|
1216
|
-
}
|
|
1217
|
-
const rpcUrl = getRpcUrl(accept.network, adapter);
|
|
1218
|
-
log("Checking balance...");
|
|
1219
|
-
try {
|
|
1220
|
-
const balance = await adapter.getBalance(accept, wallet, rpcUrl);
|
|
1221
|
-
const requiredAmount = Number(paymentAmount) / Math.pow(10, decimals);
|
|
1222
|
-
if (balance < requiredAmount) {
|
|
1223
|
-
const chainName = getChainDisplayName(accept.network, adapter.name);
|
|
1224
|
-
throw new X402Error(
|
|
1225
|
-
"insufficient_balance",
|
|
1226
|
-
`Insufficient balance on ${chainName}. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
log(`Balance OK: $${balance.toFixed(4)} >= $${requiredAmount.toFixed(4)}`);
|
|
1230
|
-
} catch (err) {
|
|
1231
|
-
if (err instanceof X402Error) throw err;
|
|
1232
|
-
log("Balance check failed (RPC error), proceeding with transaction attempt");
|
|
1233
|
-
}
|
|
1234
|
-
if (onPaymentRequired) {
|
|
1235
|
-
const approved = await onPaymentRequired(accept);
|
|
1236
|
-
if (!approved) {
|
|
1237
|
-
throw new X402Error("payment_rejected", "Payment rejected by onPaymentRequired callback");
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
log("Building transaction...");
|
|
1241
|
-
const signedTx = await adapter.buildTransaction(accept, wallet, rpcUrl);
|
|
1242
|
-
log("Transaction signed");
|
|
1243
|
-
let payload;
|
|
1244
|
-
if (adapter.name === "EVM") {
|
|
1245
|
-
payload = JSON.parse(signedTx.serialized);
|
|
1246
|
-
} else {
|
|
1247
|
-
payload = { transaction: signedTx.serialized };
|
|
1248
|
-
}
|
|
1249
|
-
const originalUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1250
|
-
let resolvedResource = requirements.resource;
|
|
1251
|
-
if (typeof requirements.resource === "string") {
|
|
1252
|
-
try {
|
|
1253
|
-
const resolvedUrl = new URL(requirements.resource, originalUrl).toString();
|
|
1254
|
-
if (resolvedUrl !== requirements.resource) {
|
|
1255
|
-
log("Resolved relative resource URL:", requirements.resource, "\u2192", resolvedUrl);
|
|
1256
|
-
}
|
|
1257
|
-
resolvedResource = resolvedUrl;
|
|
1258
|
-
} catch {
|
|
1259
|
-
resolvedResource = requirements.resource;
|
|
1260
|
-
}
|
|
1261
|
-
} else if (requirements.resource && typeof requirements.resource === "object" && "url" in requirements.resource) {
|
|
1262
|
-
const resourceObj = requirements.resource;
|
|
1263
|
-
try {
|
|
1264
|
-
const resolvedUrl = new URL(resourceObj.url, originalUrl).toString();
|
|
1265
|
-
if (resolvedUrl !== resourceObj.url) {
|
|
1266
|
-
log("Resolved relative resource URL:", resourceObj.url, "\u2192", resolvedUrl);
|
|
1267
|
-
resolvedResource = { ...resourceObj, url: resolvedUrl };
|
|
1268
|
-
}
|
|
1269
|
-
} catch {
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
const paymentSignature = {
|
|
1273
|
-
x402Version: accept.x402Version ?? 2,
|
|
1274
|
-
// Echo version from 402 response, default to 2
|
|
1275
|
-
resource: resolvedResource,
|
|
1276
|
-
accepted: accept,
|
|
1277
|
-
payload
|
|
1278
|
-
};
|
|
1279
|
-
if (signedTx.extensions) {
|
|
1280
|
-
paymentSignature.extensions = signedTx.extensions;
|
|
1281
|
-
}
|
|
1282
|
-
const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
|
|
1283
|
-
log("Retrying request with payment...");
|
|
1284
|
-
const retryResponse = await fetchWithRetry(input, {
|
|
1285
|
-
...init,
|
|
1286
|
-
headers: {
|
|
1287
|
-
...init?.headers || {},
|
|
1288
|
-
"PAYMENT-SIGNATURE": paymentSignatureHeader,
|
|
1289
|
-
// Forward quote hash for dynamic pricing validation
|
|
1290
|
-
...quoteHash ? { "X-Quote-Hash": quoteHash } : {}
|
|
1291
|
-
}
|
|
1292
|
-
});
|
|
1293
|
-
log("Retry response status:", retryResponse.status);
|
|
1294
|
-
if (retryResponse.status === 402) {
|
|
1295
|
-
let reason = "unknown";
|
|
1296
|
-
try {
|
|
1297
|
-
const body = await retryResponse.clone().json();
|
|
1298
|
-
reason = String(body.error || body.message || JSON.stringify(body));
|
|
1299
|
-
log("Rejection reason:", reason);
|
|
1300
|
-
} catch {
|
|
1301
|
-
}
|
|
1302
|
-
throw new X402Error(
|
|
1303
|
-
"payment_rejected",
|
|
1304
|
-
`Payment was rejected by the server: ${reason}`
|
|
1305
|
-
);
|
|
1306
|
-
}
|
|
1307
|
-
const paymentResponseHeader = retryResponse.headers.get("PAYMENT-RESPONSE");
|
|
1308
|
-
if (paymentResponseHeader) {
|
|
1309
|
-
try {
|
|
1310
|
-
const receipt = JSON.parse(atob(paymentResponseHeader));
|
|
1311
|
-
receiptStore.set(retryResponse, receipt);
|
|
1312
|
-
retryResponse["_x402"] = receipt;
|
|
1313
|
-
if (receipt.extensions) {
|
|
1314
|
-
log("Settlement extensions:", Object.keys(receipt.extensions).join(", "));
|
|
1315
|
-
}
|
|
1316
|
-
} catch {
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
return retryResponse;
|
|
1320
|
-
}
|
|
1321
|
-
return {
|
|
1322
|
-
fetch: x402Fetch
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
// src/utils.ts
|
|
1327
|
-
function isSolanaNetwork(network) {
|
|
1328
|
-
return network.startsWith("solana:") || network === "solana";
|
|
1329
|
-
}
|
|
1330
|
-
function isEvmNetwork(network) {
|
|
1331
|
-
return network.startsWith("eip155:") || ["base", "ethereum", "arbitrum"].includes(network);
|
|
1332
|
-
}
|
|
1333
|
-
function getChainFamily(network) {
|
|
1334
|
-
if (isSolanaNetwork(network)) return "solana";
|
|
1335
|
-
if (isEvmNetwork(network)) return "evm";
|
|
1336
|
-
return "unknown";
|
|
1337
|
-
}
|
|
1338
|
-
function getChainName(network) {
|
|
1339
|
-
const mapping = {
|
|
1340
|
-
[SOLANA_MAINNET_NETWORK]: "Solana",
|
|
1341
|
-
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": "Solana Devnet",
|
|
1342
|
-
"solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z": "Solana Testnet",
|
|
1343
|
-
"solana": "Solana",
|
|
1344
|
-
[BASE_MAINNET_NETWORK]: "Base",
|
|
1345
|
-
"eip155:84532": "Base Sepolia",
|
|
1346
|
-
"eip155:1": "Ethereum",
|
|
1347
|
-
"eip155:42161": "Arbitrum One",
|
|
1348
|
-
"base": "Base",
|
|
1349
|
-
"ethereum": "Ethereum",
|
|
1350
|
-
"arbitrum": "Arbitrum"
|
|
1351
|
-
};
|
|
1352
|
-
return mapping[network] || network;
|
|
1353
|
-
}
|
|
1354
|
-
function getExplorerUrl(txSignature, network) {
|
|
1355
|
-
const family = getChainFamily(network);
|
|
1356
|
-
if (family === "solana") {
|
|
1357
|
-
const isDevnet = network.includes("devnet") || network === "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
1358
|
-
if (isDevnet) {
|
|
1359
|
-
return `https://solscan.io/tx/${txSignature}?cluster=devnet`;
|
|
1360
|
-
}
|
|
1361
|
-
return `https://www.orbmarkets.io/tx/${txSignature}`;
|
|
1362
|
-
}
|
|
1363
|
-
if (family === "evm") {
|
|
1364
|
-
let chainId = "8453";
|
|
1365
|
-
if (network.startsWith("eip155:")) {
|
|
1366
|
-
chainId = network.split(":")[1];
|
|
1367
|
-
} else if (network === "ethereum") {
|
|
1368
|
-
chainId = "1";
|
|
1369
|
-
} else if (network === "arbitrum") {
|
|
1370
|
-
chainId = "42161";
|
|
1371
|
-
}
|
|
1372
|
-
switch (chainId) {
|
|
1373
|
-
case "8453":
|
|
1374
|
-
return `https://basescan.org/tx/${txSignature}`;
|
|
1375
|
-
case "84532":
|
|
1376
|
-
return `https://sepolia.basescan.org/tx/${txSignature}`;
|
|
1377
|
-
case "1":
|
|
1378
|
-
return `https://etherscan.io/tx/${txSignature}`;
|
|
1379
|
-
case "42161":
|
|
1380
|
-
return `https://arbiscan.io/tx/${txSignature}`;
|
|
1381
|
-
default:
|
|
1382
|
-
return `https://basescan.org/tx/${txSignature}`;
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
return `https://solscan.io/tx/${txSignature}`;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
// src/client/sponsored-access.ts
|
|
1389
|
-
function getSponsoredAccessInfo(response) {
|
|
1390
|
-
const receipt = getPaymentReceipt(response);
|
|
1391
|
-
if (!receipt?.extensions?.["sponsored-access"]) return void 0;
|
|
1392
|
-
return receipt.extensions["sponsored-access"];
|
|
1393
|
-
}
|
|
1394
|
-
function getSponsoredRecommendations(response) {
|
|
1395
|
-
const info = getSponsoredAccessInfo(response);
|
|
1396
|
-
if (!info?.recommendations?.length) return void 0;
|
|
1397
|
-
return info.recommendations;
|
|
1398
|
-
}
|
|
1399
|
-
async function fireImpressionBeacon(response) {
|
|
1400
|
-
const info = getSponsoredAccessInfo(response);
|
|
1401
|
-
const beaconUrl = info?.tracking?.impressionBeacon;
|
|
1402
|
-
if (!beaconUrl) return false;
|
|
1403
|
-
try {
|
|
1404
|
-
await fetch(beaconUrl, { method: "GET" });
|
|
1405
|
-
} catch {
|
|
1406
|
-
}
|
|
1407
|
-
return true;
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
// src/react/useX402Payment.ts
|
|
1411
|
-
function useX402Payment(config) {
|
|
1412
|
-
const {
|
|
1413
|
-
wallets: walletSet,
|
|
1414
|
-
wallet: legacyWallet,
|
|
1415
|
-
preferredNetwork,
|
|
1416
|
-
rpcUrls = {},
|
|
1417
|
-
verbose = false
|
|
1418
|
-
} = config;
|
|
1419
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
1420
|
-
const [status, setStatus] = useState("idle");
|
|
1421
|
-
const [error, setError] = useState(null);
|
|
1422
|
-
const [transactionId, setTransactionId] = useState(null);
|
|
1423
|
-
const [transactionNetwork, setTransactionNetwork] = useState(null);
|
|
1424
|
-
const [balances, setBalances] = useState([]);
|
|
1425
|
-
const [sponsoredRecommendations, setSponsoredRecommendations] = useState(null);
|
|
1426
|
-
const log = useCallback((...args) => {
|
|
1427
|
-
if (verbose) console.log("[useX402Payment]", ...args);
|
|
1428
|
-
}, [verbose]);
|
|
1429
|
-
const wallets = useMemo(() => {
|
|
1430
|
-
const w = { ...walletSet };
|
|
1431
|
-
if (legacyWallet && !w.solana && isSolanaWallet(legacyWallet)) {
|
|
1432
|
-
w.solana = legacyWallet;
|
|
1433
|
-
}
|
|
1434
|
-
if (legacyWallet && !w.evm && isEvmWallet(legacyWallet)) {
|
|
1435
|
-
w.evm = legacyWallet;
|
|
1436
|
-
}
|
|
1437
|
-
return w;
|
|
1438
|
-
}, [walletSet, legacyWallet]);
|
|
1439
|
-
const adapters = useMemo(() => [
|
|
1440
|
-
createSolanaAdapter({ verbose, rpcUrls }),
|
|
1441
|
-
createEvmAdapter({ verbose, rpcUrls })
|
|
1442
|
-
], [verbose, rpcUrls]);
|
|
1443
|
-
const connectedChains = useMemo(() => ({
|
|
1444
|
-
solana: wallets.solana ? isSolanaWallet(wallets.solana) && adapters[0].isConnected(wallets.solana) : false,
|
|
1445
|
-
evm: wallets.evm ? isEvmWallet(wallets.evm) && adapters[1].isConnected(wallets.evm) : false
|
|
1446
|
-
}), [wallets, adapters]);
|
|
1447
|
-
const isAnyWalletConnected = connectedChains.solana || connectedChains.evm;
|
|
1448
|
-
const refreshBalances = useCallback(async () => {
|
|
1449
|
-
const newBalances = [];
|
|
1450
|
-
if (connectedChains.solana && wallets.solana) {
|
|
1451
|
-
try {
|
|
1452
|
-
const solanaAdapter = adapters.find((a) => a.name === "Solana");
|
|
1453
|
-
if (solanaAdapter) {
|
|
1454
|
-
const accept = {
|
|
1455
|
-
scheme: "exact",
|
|
1456
|
-
network: SOLANA_MAINNET_NETWORK,
|
|
1457
|
-
amount: "0",
|
|
1458
|
-
asset: USDC_MINT,
|
|
1459
|
-
payTo: "",
|
|
1460
|
-
maxTimeoutSeconds: 60,
|
|
1461
|
-
extra: { feePayer: "", decimals: 6 }
|
|
1462
|
-
};
|
|
1463
|
-
const balance = await solanaAdapter.getBalance(accept, wallets.solana);
|
|
1464
|
-
newBalances.push({
|
|
1465
|
-
network: SOLANA_MAINNET_NETWORK,
|
|
1466
|
-
chainName: getChainName(SOLANA_MAINNET_NETWORK),
|
|
1467
|
-
balance,
|
|
1468
|
-
asset: "USDC"
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
} catch (e) {
|
|
1472
|
-
log("Failed to fetch Solana balance:", e);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
if (connectedChains.evm && wallets.evm) {
|
|
1476
|
-
try {
|
|
1477
|
-
const evmAdapter = adapters.find((a) => a.name === "EVM");
|
|
1478
|
-
if (evmAdapter) {
|
|
1479
|
-
const accept = {
|
|
1480
|
-
scheme: "exact",
|
|
1481
|
-
network: BASE_MAINNET_NETWORK,
|
|
1482
|
-
amount: "0",
|
|
1483
|
-
asset: USDC_BASE,
|
|
1484
|
-
payTo: "",
|
|
1485
|
-
maxTimeoutSeconds: 60,
|
|
1486
|
-
extra: { feePayer: "", decimals: 6 }
|
|
1487
|
-
};
|
|
1488
|
-
const balance = await evmAdapter.getBalance(accept, wallets.evm);
|
|
1489
|
-
newBalances.push({
|
|
1490
|
-
network: BASE_MAINNET_NETWORK,
|
|
1491
|
-
chainName: getChainName(BASE_MAINNET_NETWORK),
|
|
1492
|
-
balance,
|
|
1493
|
-
asset: "USDC"
|
|
1494
|
-
});
|
|
1495
|
-
}
|
|
1496
|
-
} catch (e) {
|
|
1497
|
-
log("Failed to fetch Base balance:", e);
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
setBalances(newBalances);
|
|
1501
|
-
}, [connectedChains, wallets, adapters, log]);
|
|
1502
|
-
useEffect(() => {
|
|
1503
|
-
refreshBalances();
|
|
1504
|
-
const interval = setInterval(refreshBalances, 3e4);
|
|
1505
|
-
return () => clearInterval(interval);
|
|
1506
|
-
}, [refreshBalances]);
|
|
1507
|
-
const reset = useCallback(() => {
|
|
1508
|
-
setIsLoading(false);
|
|
1509
|
-
setStatus("idle");
|
|
1510
|
-
setError(null);
|
|
1511
|
-
setTransactionId(null);
|
|
1512
|
-
setTransactionNetwork(null);
|
|
1513
|
-
setSponsoredRecommendations(null);
|
|
1514
|
-
}, []);
|
|
1515
|
-
const client = useMemo(() => createX402Client({
|
|
1516
|
-
adapters,
|
|
1517
|
-
wallets,
|
|
1518
|
-
preferredNetwork,
|
|
1519
|
-
rpcUrls,
|
|
1520
|
-
verbose
|
|
1521
|
-
}), [adapters, wallets, preferredNetwork, rpcUrls, verbose]);
|
|
1522
|
-
const fetchWithPayment = useCallback(async (input, init) => {
|
|
1523
|
-
setIsLoading(true);
|
|
1524
|
-
setStatus("pending");
|
|
1525
|
-
setError(null);
|
|
1526
|
-
setTransactionId(null);
|
|
1527
|
-
setTransactionNetwork(null);
|
|
1528
|
-
if (!isAnyWalletConnected) {
|
|
1529
|
-
const connError = new X402Error("wallet_not_connected", "No wallet connected");
|
|
1530
|
-
setError(connError);
|
|
1531
|
-
setStatus("error");
|
|
1532
|
-
setIsLoading(false);
|
|
1533
|
-
throw connError;
|
|
1534
|
-
}
|
|
1535
|
-
try {
|
|
1536
|
-
const response = await client.fetch(input, init);
|
|
1537
|
-
const paymentResponse = response.headers.get("PAYMENT-RESPONSE");
|
|
1538
|
-
if (paymentResponse) {
|
|
1539
|
-
try {
|
|
1540
|
-
const decoded = JSON.parse(atob(paymentResponse));
|
|
1541
|
-
if (decoded.transaction) {
|
|
1542
|
-
setTransactionId(decoded.transaction);
|
|
1543
|
-
}
|
|
1544
|
-
if (decoded.network) {
|
|
1545
|
-
setTransactionNetwork(decoded.network);
|
|
1546
|
-
}
|
|
1547
|
-
} catch {
|
|
1548
|
-
log("Could not parse PAYMENT-RESPONSE header");
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
const recs = getSponsoredRecommendations(response);
|
|
1552
|
-
setSponsoredRecommendations(recs ?? null);
|
|
1553
|
-
if (recs) {
|
|
1554
|
-
log("Sponsored recommendations received:", recs.length);
|
|
1555
|
-
fireImpressionBeacon(response).catch(() => {
|
|
1556
|
-
});
|
|
1557
|
-
}
|
|
1558
|
-
setStatus("success");
|
|
1559
|
-
return response;
|
|
1560
|
-
} catch (err) {
|
|
1561
|
-
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
1562
|
-
setError(error2);
|
|
1563
|
-
setStatus("error");
|
|
1564
|
-
throw err;
|
|
1565
|
-
} finally {
|
|
1566
|
-
setIsLoading(false);
|
|
1567
|
-
setTimeout(refreshBalances, 2e3);
|
|
1568
|
-
}
|
|
1569
|
-
}, [client, isAnyWalletConnected, log, refreshBalances]);
|
|
1570
|
-
const transactionUrl = useMemo(() => {
|
|
1571
|
-
if (!transactionId) return null;
|
|
1572
|
-
const network = transactionNetwork || preferredNetwork || SOLANA_MAINNET_NETWORK;
|
|
1573
|
-
return getExplorerUrl(transactionId, network);
|
|
1574
|
-
}, [transactionId, transactionNetwork, preferredNetwork]);
|
|
1575
|
-
return {
|
|
1576
|
-
fetch: fetchWithPayment,
|
|
1577
|
-
isLoading,
|
|
1578
|
-
status,
|
|
1579
|
-
error,
|
|
1580
|
-
transactionId,
|
|
1581
|
-
transactionNetwork,
|
|
1582
|
-
transactionUrl,
|
|
1583
|
-
balances,
|
|
1584
|
-
connectedChains,
|
|
1585
|
-
isAnyWalletConnected,
|
|
1586
|
-
reset,
|
|
1587
|
-
refreshBalances,
|
|
1588
|
-
accessPass: null,
|
|
1589
|
-
// Access pass state managed by useAccessPass hook for granular control
|
|
1590
|
-
sponsoredRecommendations
|
|
1591
|
-
};
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
// src/react/useAccessPass.ts
|
|
1595
|
-
import { useState as useState2, useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef } from "react";
|
|
1596
|
-
function useAccessPass(config) {
|
|
1597
|
-
const {
|
|
1598
|
-
wallets: walletSet,
|
|
1599
|
-
wallet: legacyWallet,
|
|
1600
|
-
preferredNetwork,
|
|
1601
|
-
rpcUrls = {},
|
|
1602
|
-
resourceUrl,
|
|
1603
|
-
autoConnect = true,
|
|
1604
|
-
verbose = false
|
|
1605
|
-
} = config;
|
|
1606
|
-
const storageKey = `x402-access-pass:${resourceUrl}`;
|
|
1607
|
-
function loadPersistedPass() {
|
|
1608
|
-
if (typeof sessionStorage === "undefined") return null;
|
|
1609
|
-
try {
|
|
1610
|
-
const stored = sessionStorage.getItem(storageKey);
|
|
1611
|
-
if (!stored) return null;
|
|
1612
|
-
const parsed = JSON.parse(stored);
|
|
1613
|
-
if (new Date(parsed.expiresAt).getTime() <= Date.now()) {
|
|
1614
|
-
sessionStorage.removeItem(storageKey);
|
|
1615
|
-
return null;
|
|
1616
|
-
}
|
|
1617
|
-
return parsed;
|
|
1618
|
-
} catch {
|
|
1619
|
-
return null;
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
function persistPass(jwt, tier, expiresAt) {
|
|
1623
|
-
if (typeof sessionStorage === "undefined") return;
|
|
1624
|
-
try {
|
|
1625
|
-
sessionStorage.setItem(storageKey, JSON.stringify({ jwt, tier, expiresAt }));
|
|
1626
|
-
} catch {
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
const persisted = loadPersistedPass();
|
|
1630
|
-
const [tiers, setTiers] = useState2(null);
|
|
1631
|
-
const [customRatePerHour, setCustomRatePerHour] = useState2(null);
|
|
1632
|
-
const [isLoadingTiers, setIsLoadingTiers] = useState2(false);
|
|
1633
|
-
const [passJwt, setPassJwt] = useState2(persisted?.jwt || null);
|
|
1634
|
-
const [passInfo, setPassInfo] = useState2(
|
|
1635
|
-
persisted ? { tier: persisted.tier, expiresAt: persisted.expiresAt } : null
|
|
1636
|
-
);
|
|
1637
|
-
const [isPurchasing, setIsPurchasing] = useState2(false);
|
|
1638
|
-
const [purchaseError, setPurchaseError] = useState2(null);
|
|
1639
|
-
const passJwtRef = useRef(passJwt);
|
|
1640
|
-
useEffect2(() => {
|
|
1641
|
-
passJwtRef.current = passJwt;
|
|
1642
|
-
}, [passJwt]);
|
|
1643
|
-
const log = useCallback2((...args) => {
|
|
1644
|
-
if (verbose) console.log("[useAccessPass]", ...args);
|
|
1645
|
-
}, [verbose]);
|
|
1646
|
-
const wallets = useMemo2(() => {
|
|
1647
|
-
const w = { ...walletSet };
|
|
1648
|
-
if (legacyWallet && !w.solana && isSolanaWallet(legacyWallet)) {
|
|
1649
|
-
w.solana = legacyWallet;
|
|
1650
|
-
}
|
|
1651
|
-
if (legacyWallet && !w.evm && isEvmWallet(legacyWallet)) {
|
|
1652
|
-
w.evm = legacyWallet;
|
|
1653
|
-
}
|
|
1654
|
-
return w;
|
|
1655
|
-
}, [walletSet, legacyWallet]);
|
|
1656
|
-
const client = useMemo2(() => createX402Client({
|
|
1657
|
-
adapters: [createSolanaAdapter({ verbose, rpcUrls }), createEvmAdapter({ verbose, rpcUrls })],
|
|
1658
|
-
wallets,
|
|
1659
|
-
preferredNetwork,
|
|
1660
|
-
rpcUrls,
|
|
1661
|
-
verbose,
|
|
1662
|
-
accessPass: { enabled: true, autoRenew: true }
|
|
1663
|
-
}), [wallets, preferredNetwork, rpcUrls, verbose]);
|
|
1664
|
-
const [tick, setTick] = useState2(0);
|
|
1665
|
-
const pass = useMemo2(() => {
|
|
1666
|
-
void tick;
|
|
1667
|
-
if (!passJwt || !passInfo) return null;
|
|
1668
|
-
const expiresAtMs = new Date(passInfo.expiresAt).getTime();
|
|
1669
|
-
const remaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1e3));
|
|
1670
|
-
if (remaining <= 0) return null;
|
|
1671
|
-
return { jwt: passJwt, tier: passInfo.tier, expiresAt: passInfo.expiresAt, remainingSeconds: remaining };
|
|
1672
|
-
}, [passJwt, passInfo, tick]);
|
|
1673
|
-
const isPassValid = pass !== null && pass.remainingSeconds > 0;
|
|
1674
|
-
useEffect2(() => {
|
|
1675
|
-
if (!passJwt || !passInfo) return;
|
|
1676
|
-
const interval = setInterval(() => setTick((t) => t + 1), 1e3);
|
|
1677
|
-
return () => clearInterval(interval);
|
|
1678
|
-
}, [isPassValid]);
|
|
1679
|
-
const fetchTiers = useCallback2(async () => {
|
|
1680
|
-
setIsLoadingTiers(true);
|
|
1681
|
-
try {
|
|
1682
|
-
const res = await fetch(resourceUrl);
|
|
1683
|
-
if (res.status === 402) {
|
|
1684
|
-
const tiersHeader = res.headers.get("X-ACCESS-PASS-TIERS");
|
|
1685
|
-
if (tiersHeader) {
|
|
1686
|
-
const info = JSON.parse(atob(tiersHeader));
|
|
1687
|
-
setTiers(info.tiers || null);
|
|
1688
|
-
setCustomRatePerHour(info.ratePerHour || null);
|
|
1689
|
-
log("Tier info loaded:", info);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
} catch (e) {
|
|
1693
|
-
log("Failed to fetch tiers:", e);
|
|
1694
|
-
} finally {
|
|
1695
|
-
setIsLoadingTiers(false);
|
|
1696
|
-
}
|
|
1697
|
-
}, [resourceUrl, log]);
|
|
1698
|
-
useEffect2(() => {
|
|
1699
|
-
if (autoConnect) fetchTiers();
|
|
1700
|
-
}, [autoConnect, fetchTiers]);
|
|
1701
|
-
const purchasePass = useCallback2(async (tier, durationSeconds) => {
|
|
1702
|
-
setIsPurchasing(true);
|
|
1703
|
-
setPurchaseError(null);
|
|
1704
|
-
try {
|
|
1705
|
-
let url = resourceUrl;
|
|
1706
|
-
if (tier) url += (url.includes("?") ? "&" : "?") + `tier=${tier}`;
|
|
1707
|
-
else if (durationSeconds) url += (url.includes("?") ? "&" : "?") + `duration=${durationSeconds}`;
|
|
1708
|
-
const res = await client.fetch(url, { method: "POST" });
|
|
1709
|
-
const jwt = res.headers.get("ACCESS-PASS");
|
|
1710
|
-
log("ACCESS-PASS header:", jwt ? "found" : "NOT FOUND");
|
|
1711
|
-
if (jwt) {
|
|
1712
|
-
setPassJwt(jwt);
|
|
1713
|
-
let passTier = tier || "unknown";
|
|
1714
|
-
let passExpiresAt = "";
|
|
1715
|
-
try {
|
|
1716
|
-
const body = await res.json();
|
|
1717
|
-
passTier = body.accessPass?.tier || passTier;
|
|
1718
|
-
passExpiresAt = body.accessPass?.expiresAt || "";
|
|
1719
|
-
} catch {
|
|
1720
|
-
}
|
|
1721
|
-
if (!passExpiresAt) {
|
|
1722
|
-
try {
|
|
1723
|
-
const parts = jwt.split(".");
|
|
1724
|
-
if (parts.length === 3) {
|
|
1725
|
-
const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
1726
|
-
passTier = payload.tier || passTier;
|
|
1727
|
-
passExpiresAt = new Date(payload.exp * 1e3).toISOString();
|
|
1728
|
-
}
|
|
1729
|
-
} catch {
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
setPassInfo({ tier: passTier, expiresAt: passExpiresAt });
|
|
1733
|
-
persistPass(jwt, passTier, passExpiresAt);
|
|
1734
|
-
log("Pass purchased and persisted:", passTier, passExpiresAt);
|
|
1735
|
-
}
|
|
1736
|
-
} catch (err) {
|
|
1737
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1738
|
-
setPurchaseError(error);
|
|
1739
|
-
throw error;
|
|
1740
|
-
} finally {
|
|
1741
|
-
setIsPurchasing(false);
|
|
1742
|
-
}
|
|
1743
|
-
}, [resourceUrl, client, log]);
|
|
1744
|
-
const fetchWithPass = useCallback2(async (path, init) => {
|
|
1745
|
-
const url = !path || path === "" ? resourceUrl : path.startsWith("http") ? path : `${resourceUrl.replace(/\/$/, "")}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
1746
|
-
const currentJwt = passJwtRef.current;
|
|
1747
|
-
if (currentJwt) {
|
|
1748
|
-
try {
|
|
1749
|
-
const parts = currentJwt.split(".");
|
|
1750
|
-
if (parts.length === 3) {
|
|
1751
|
-
const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
1752
|
-
if (payload.exp && payload.exp > Date.now() / 1e3) {
|
|
1753
|
-
return fetch(url, {
|
|
1754
|
-
...init,
|
|
1755
|
-
headers: {
|
|
1756
|
-
...init?.headers || {},
|
|
1757
|
-
"Authorization": `Bearer ${currentJwt}`
|
|
1758
|
-
}
|
|
1759
|
-
});
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
} catch {
|
|
1763
|
-
}
|
|
1764
|
-
setPassJwt(null);
|
|
1765
|
-
setPassInfo(null);
|
|
1766
|
-
try {
|
|
1767
|
-
sessionStorage.removeItem(storageKey);
|
|
1768
|
-
} catch {
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
return client.fetch(url, init);
|
|
1772
|
-
}, [resourceUrl, client, storageKey]);
|
|
1773
|
-
return {
|
|
1774
|
-
tiers,
|
|
1775
|
-
customRatePerHour,
|
|
1776
|
-
isLoadingTiers,
|
|
1777
|
-
pass,
|
|
1778
|
-
isPassValid,
|
|
1779
|
-
fetchTiers,
|
|
1780
|
-
purchasePass,
|
|
1781
|
-
isPurchasing,
|
|
1782
|
-
purchaseError,
|
|
1783
|
-
fetch: fetchWithPass
|
|
1784
|
-
};
|
|
1785
|
-
}
|
|
1786
|
-
export {
|
|
1787
|
-
X402Error,
|
|
1788
|
-
fireImpressionBeacon,
|
|
1789
|
-
getSponsoredRecommendations,
|
|
1790
|
-
useAccessPass,
|
|
1791
|
-
useX402Payment
|
|
1792
|
-
};
|
|
1
|
+
import{useState as ae,useCallback as ve,useEffect as lt,useMemo as ge}from"react";var ne="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",oe="eip155:8453";var qe="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",Le="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";var U=class r extends Error{code;details;constructor(e,t,s){super(t),this.name="X402Error",this.code=e,this.details=s,Object.setPrototypeOf(this,r.prototype)}};import{PublicKey as ie,Connection as je,TransactionMessage as Qe,VersionedTransaction as Ze,ComputeBudgetProgram as Fe}from"@solana/web3.js";import{getAssociatedTokenAddress as Ie,getAccount as et,createTransferCheckedInstruction as tt,getMint as nt,TOKEN_PROGRAM_ID as Je,TOKEN_2022_PROGRAM_ID as he}from"@solana/spl-token";var de="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",ye="solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",we="solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z",ce={[de]:"https://api.dexter.cash/api/solana/rpc",[ye]:"https://api.devnet.solana.com",[we]:"https://api.testnet.solana.com"},rt=12e3,st=1;function V(r){if(!r||typeof r!="object")return!1;let e=r;return"publicKey"in e&&"signTransaction"in e&&typeof e.signTransaction=="function"}var Se=class{name="Solana";networks=[de,ye,we];config;log;constructor(e={}){this.config=e,this.log=e.verbose?console.log.bind(console,"[x402:solana]"):()=>{}}canHandle(e){return!!(this.networks.includes(e)||e==="solana"||e==="solana-devnet"||e==="solana-testnet"||e.startsWith("solana:"))}getDefaultRpcUrl(e){return this.config.rpcUrls?.[e]?this.config.rpcUrls[e]:ce[e]?ce[e]:e==="solana"?ce[de]:e==="solana-devnet"?ce[ye]:e==="solana-testnet"?ce[we]:ce[de]}getAddress(e){return V(e)?e.publicKey?.toBase58()??null:null}isConnected(e){return V(e)?e.publicKey!==null:!1}async getBalance(e,t,s){if(!V(t)||!t.publicKey)return 0;let l=s||this.getDefaultRpcUrl(e.network),n=new je(l,"confirmed"),f=new ie(t.publicKey.toBase58()),a=new ie(e.asset);try{let m=(await n.getAccountInfo(a,"confirmed"))?.owner.toBase58()===he.toBase58()?he:Je,P=await Ie(a,f,!1,m),E=await et(n,P,void 0,m),T=e.extra?.decimals??6;return Number(E.amount)/Math.pow(10,T)}catch(p){if(p&&typeof p=="object"&&"name"in p&&(p.name==="TokenAccountNotFoundError"||p.name==="TokenInvalidAccountOwnerError"))return 0;throw p}}async buildTransaction(e,t,s){if(!V(t))throw new Error("Invalid Solana wallet");if(!t.publicKey)throw new Error("Wallet not connected");let l=s||this.getDefaultRpcUrl(e.network),n=new je(l,"confirmed"),f=new ie(t.publicKey.toBase58()),{payTo:a,asset:p,extra:m}=e,P=e.amount??e.maxAmountRequired;if(!P)throw new Error("Missing amount in payment requirements");if(!m?.feePayer)throw new Error("Missing feePayer in payment requirements");let E=new ie(m.feePayer),T=new ie(p),o=new ie(a);this.log("Building transaction:",{from:f.toBase58(),to:a,amount:P,asset:p,feePayer:m.feePayer});let D=[];D.push(Fe.setComputeUnitLimit({units:rt})),D.push(Fe.setComputeUnitPrice({microLamports:st}));let k=await n.getAccountInfo(T,"confirmed");if(!k)throw new Error(`Token mint ${p} not found`);let O=k.owner.toBase58()===he.toBase58()?he:Je,J=await nt(n,T,void 0,O);typeof m?.decimals=="number"&&J.decimals!==m.decimals&&this.log(`Decimals mismatch: requirements say ${m.decimals}, mint says ${J.decimals}`);let R=await Ie(T,f,!1,O),M=await Ie(T,o,!1,O);if(!await n.getAccountInfo(R,"confirmed"))throw new Error(`No token account found for ${p}. Please ensure you have USDC in your wallet.`);if(!await n.getAccountInfo(M,"confirmed"))throw new Error(`Seller token account not found. The seller (${a}) must have a USDC account.`);let W=BigInt(P);D.push(tt(R,T,M,f,W,J.decimals,[],O));let{blockhash:$}=await n.getLatestBlockhash("confirmed"),i=new Qe({payerKey:E,recentBlockhash:$,instructions:D}).compileToV0Message(),c=new Ze(i),S=await t.signTransaction(c);return this.log("Transaction signed successfully"),{serialized:Buffer.from(S.serialize()).toString("base64")}}};function G(r){return new Se(r)}var fe="0x000000000022D473030F116dDEE9F6B43aC78BA3",_e="0x402085c248EeA27D92E8b30b2C58ed07f9E20001",at={PermitWitnessTransferFrom:[{name:"permitted",type:"TokenPermissions"},{name:"spender",type:"address"},{name:"nonce",type:"uint256"},{name:"deadline",type:"uint256"},{name:"witness",type:"Witness"}],TokenPermissions:[{name:"token",type:"address"},{name:"amount",type:"uint256"}],Witness:[{name:"to",type:"address"},{name:"validAfter",type:"uint256"}]},Xe=BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),se="eip155:8453",me="eip155:84532",le="eip155:42161",xe="eip155:137",be="eip155:10",Pe="eip155:43114",pe="eip155:56",Ee="eip155:1187947933",Re="eip155:324705682",ue="eip155:1",Ve="0x55d398326f99059fF775485246999027B3197955",Ne="0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",Ke={[pe]:56,[se]:8453,[me]:84532,[le]:42161,[xe]:137,[be]:10,[Pe]:43114,[Ee]:1187947933,[Re]:324705682,[ue]:1},re={[pe]:"https://bsc-dataseed1.binance.org",[se]:"https://api.dexter.cash/api/base/rpc",[me]:"https://sepolia.base.org",[le]:"https://arb1.arbitrum.io/rpc",[xe]:"https://polygon-rpc.com",[be]:"https://mainnet.optimism.io",[Pe]:"https://api.avax.network/ext/bc/C/rpc",[Ee]:"https://skale-base.skalenodes.com/v1/base",[Re]:"https://base-sepolia-testnet.skalenodes.com/v1/jubilant-horrible-ancha",[ue]:"https://eth.llamarpc.com"},Be={[pe]:Ne,[se]:"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",[me]:"0x036CbD53842c5426634e7929541eC2318f3dCF7e",[le]:"0xaf88d065e77c8cC2239327C5EDb3A432268e5831",[xe]:"0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",[be]:"0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85",[Pe]:"0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",[Ee]:"0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20",[Re]:"0x2e08028E3C4c2356572E096d8EF835cD5C6030bD",[ue]:"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"},Ue={[Ve]:{symbol:"USDT",decimals:18},[Ne]:{symbol:"USDC",decimals:18}};function H(r){if(!r||typeof r!="object")return!1;let e=r;return"address"in e&&typeof e.address=="string"&&e.address.startsWith("0x")}var Ae=class{name="EVM";networks=[pe,se,me,ue,le];config;log;constructor(e={}){this.config=e,this.log=e.verbose?console.log.bind(console,"[x402:evm]"):()=>{}}canHandle(e){return!!(this.networks.includes(e)||e==="base"||e==="bsc"||e==="ethereum"||e==="arbitrum"||e.startsWith("eip155:"))}getDefaultRpcUrl(e){return this.config.rpcUrls?.[e]?this.config.rpcUrls[e]:re[e]?re[e]:e==="base"?re[se]:e==="bsc"?re[pe]:e==="ethereum"?re[ue]:e==="arbitrum"?re[le]:re[se]}getAddress(e){return H(e)?e.address:null}isConnected(e){return H(e)?!!e.address:!1}getChainId(e){if(Ke[e])return Ke[e];if(e.startsWith("eip155:")){let t=e.split(":")[1];return parseInt(t,10)}return e==="base"?8453:e==="bsc"?56:e==="ethereum"?1:e==="arbitrum"?42161:8453}async getBalance(e,t,s){if(!H(t)||!t.address)return 0;let l=s||this.getDefaultRpcUrl(e.network);try{let n=this.encodeBalanceOf(t.address),f=await fetch(l,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:1,method:"eth_call",params:[{to:e.asset,data:n},"latest"]})});if(!f.ok)throw new Error(`RPC request failed: ${f.status}`);let a=await f.json();if(a.error)throw new Error(`RPC error: ${JSON.stringify(a.error)}`);if(!a.result||a.result==="0x")return 0;let p=BigInt(a.result),m=e.extra?.decimals??6;return Number(p)/Math.pow(10,m)}catch(n){throw n}}encodeBalanceOf(e){let t="0x70a08231",s=e.slice(2).toLowerCase().padStart(64,"0");return t+s}async buildTransaction(e,t,s){if(!H(t))throw new Error("Invalid EVM wallet");if(!t.address)throw new Error("Wallet not connected");if(e.scheme==="exact-approval")return this.buildApprovalTransaction(e,t,s);if(e.extra?.assetTransferMethod==="permit2")return this.buildPermit2Transaction(e,t,s);let{payTo:l,asset:n,extra:f}=e,a=e.amount??e.maxAmountRequired;if(!a)throw new Error("Missing amount in payment requirements");this.log("Building EVM transaction:",{from:t.address,to:l,amount:a,asset:n,network:e.network});let p=this.getChainId(e.network),m={name:f?.name??"USD Coin",version:f?.version??"2",chainId:BigInt(p),verifyingContract:n},P={TransferWithAuthorization:[{name:"from",type:"address"},{name:"to",type:"address"},{name:"value",type:"uint256"},{name:"validAfter",type:"uint256"},{name:"validBefore",type:"uint256"},{name:"nonce",type:"bytes32"}]},E=new Uint8Array(32);(globalThis.crypto??(await import("crypto")).webcrypto).getRandomValues(E);let T="0x"+[...E].map(R=>R.toString(16).padStart(2,"0")).join(""),o=Math.floor(Date.now()/1e3),D={from:t.address,to:l,value:a,validAfter:String(o-600),validBefore:String(o+(e.maxTimeoutSeconds||60)),nonce:T},k={from:t.address,to:l,value:BigInt(a),validAfter:BigInt(o-600),validBefore:BigInt(o+(e.maxTimeoutSeconds||60)),nonce:T};if(!t.signTypedData)throw new Error("Wallet does not support signTypedData (EIP-712)");let O=await t.signTypedData({domain:m,types:P,primaryType:"TransferWithAuthorization",message:k});return this.log("EIP-712 signature obtained"),{serialized:JSON.stringify({authorization:D,signature:O}),signature:O}}async buildApprovalTransaction(e,t,s){let{payTo:l,asset:n,extra:f}=e,a=e.amount??e.maxAmountRequired;if(!a)throw new Error("Missing amount in payment requirements");let p=f?.facilitatorContract;if(!p)throw new Error("exact-approval scheme requires extra.facilitatorContract from the facilitator. The /supported endpoint should provide this.");if(!t.signTypedData)throw new Error("Wallet does not support signTypedData (EIP-712)");this.log("Building approval-based transaction:",{from:t.address,to:l,amount:a,asset:n,network:e.network,facilitatorContract:p});let m=s||this.getDefaultRpcUrl(e.network),P=f?.fee??"0",E=BigInt(a)+BigInt(P),T=await this.readAllowance(m,n,t.address,p);if(T<E){if(!t.sendTransaction)throw new Error("BSC payments require a wallet that supports sendTransaction for the one-time token approval. Use createEvmKeypairWallet() or a browser wallet with transaction support.");let c=this.calculateApprovalAmount(a,P,f?.approvalStrategy);this.log(`Approving ${c} for ${p} (current allowance: ${T})`);let S=await t.sendTransaction({to:n,data:this.encodeApprove(p,c),value:0n});this.log(`Approval tx sent: ${S}`),await this.waitForReceipt(m,S),this.log("Approval confirmed")}else this.log("Sufficient allowance, skipping approval");let o=new Uint8Array(16);(globalThis.crypto??(await import("crypto")).webcrypto).getRandomValues(o);let D=[...o].reduce((c,S)=>c*256n+BigInt(S),0n).toString(),k=new Uint8Array(32);(globalThis.crypto??(await import("crypto")).webcrypto).getRandomValues(k);let O="0x"+[...k].map(c=>c.toString(16).padStart(2,"0")).join(""),R=Math.floor(Date.now()/1e3)+(e.maxTimeoutSeconds||300),M=f?.eip712Domain,B=M?{name:M.name,version:M.version,chainId:BigInt(M.chainId),verifyingContract:M.verifyingContract}:{name:"DexterBSCFacilitator",version:"1",chainId:BigInt(this.getChainId(e.network)),verifyingContract:p},x=f?.eip712Types??{Payment:[{name:"from",type:"address"},{name:"to",type:"address"},{name:"token",type:"address"},{name:"amount",type:"uint256"},{name:"fee",type:"uint256"},{name:"nonce",type:"uint256"},{name:"deadline",type:"uint256"},{name:"paymentId",type:"bytes32"}]},W={from:t.address,to:l,token:n,amount:BigInt(a),fee:BigInt(P),nonce:BigInt(D),deadline:BigInt(R),paymentId:O},$=await t.signTypedData({domain:B,types:x,primaryType:"Payment",message:W});this.log("EIP-712 Payment signature obtained");let i={from:t.address,to:l,token:n,amount:a,fee:P,nonce:D,deadline:R,paymentId:O,signature:$};return{serialized:JSON.stringify(i),signature:$}}async buildPermit2Transaction(e,t,s){let{payTo:l,asset:n}=e,f=e.amount??e.maxAmountRequired;if(!f)throw new Error("Missing amount in payment requirements");if(!t.signTypedData)throw new Error("Wallet does not support signTypedData (EIP-712)");this.log("Building Permit2 transaction:",{from:t.address,to:l,amount:f,asset:n,network:e.network});let a=s||this.getDefaultRpcUrl(e.network),p=await this.readAllowance(a,n,t.address,fe),m;if(p<BigInt(f)){let B=this.encodeApprove(fe,Xe);if(t.signTransaction){this.log(`Signing Permit2 approval for relay (current allowance: ${p})`);let x=this.getChainId(e.network),W=await this.readGasPrice(a),$=await this.readNonce(a,t.address),i=await t.signTransaction({to:n,data:B,chainId:x,gas:50000n,gasPrice:W,nonce:$});m={erc20ApprovalGasSponsoring:{info:{from:t.address,asset:n,spender:fe,amount:Xe.toString(),signedTransaction:i,version:"1"}}},this.log("Permit2 approval signed for facilitator relay")}else if(t.sendTransaction){this.log(`Approving Permit2 directly (current allowance: ${p})`);let x=await t.sendTransaction({to:n,data:B,value:0n});this.log(`Permit2 approval tx sent: ${x}`),await this.waitForReceipt(a,x),this.log("Permit2 approval confirmed")}else throw new Error("Permit2 payments require a wallet that supports signTransaction or sendTransaction for the one-time Permit2 approval. Use createEvmKeypairWallet() or a browser wallet with transaction support.")}else this.log("Sufficient Permit2 allowance, skipping approval");let P=new Uint8Array(32);(globalThis.crypto??(await import("crypto")).webcrypto).getRandomValues(P);let E=[...P].reduce((B,x)=>B*256n+BigInt(x),0n),T=Math.floor(Date.now()/1e3),o=T-600,D=T+(e.maxTimeoutSeconds||300),k=this.getChainId(e.network),O={name:"Permit2",chainId:BigInt(k),verifyingContract:fe},J={permitted:{token:n,amount:BigInt(f)},spender:_e,nonce:E,deadline:BigInt(D),witness:{to:l,validAfter:BigInt(o)}},R=await t.signTypedData({domain:O,types:at,primaryType:"PermitWitnessTransferFrom",message:J});this.log("Permit2 PermitWitnessTransferFrom signature obtained");let M={signature:R,permit2Authorization:{from:t.address,permitted:{token:n,amount:f},spender:_e,nonce:E.toString(),deadline:String(D),witness:{to:l,validAfter:String(o)}}};return{serialized:JSON.stringify(M),signature:R,extensions:m}}async readAllowance(e,t,s,l){let n="0xdd62ed3e",f=s.slice(2).toLowerCase().padStart(64,"0"),a=l.slice(2).toLowerCase().padStart(64,"0"),p=n+f+a;try{let P=await(await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:1,method:"eth_call",params:[{to:t,data:p},"latest"]})})).json();return P.error||!P.result||P.result==="0x"?0n:BigInt(P.result)}catch{return 0n}}encodeApprove(e,t){let s="0x095ea7b3",l=e.slice(2).toLowerCase().padStart(64,"0"),n=t.toString(16).padStart(64,"0");return s+l+n}async waitForReceipt(e,t,s=3e4){let l=Date.now();for(;Date.now()-l<s;){try{let f=await(await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:1,method:"eth_getTransactionReceipt",params:[t]})})).json();if(f.result){if(f.result.status==="0x0")throw new Error(`Approval transaction reverted: ${t}`);return}}catch(n){if(n instanceof Error&&n.message.includes("reverted"))throw n}await new Promise(n=>setTimeout(n,2e3))}throw new Error(`Approval transaction receipt timeout after ${s}ms: ${t}`)}async readGasPrice(e){try{let s=await(await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:1,method:"eth_gasPrice",params:[]})})).json();return s.result?BigInt(s.result):50000000n}catch{return 50000000n}}async readNonce(e,t){try{let l=await(await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:1,method:"eth_getTransactionCount",params:[t,"latest"]})})).json();return l.result?parseInt(l.result,16):0}catch{return 0}}calculateApprovalAmount(e,t,s){let l=BigInt(e)+BigInt(t);if(!s||s.mode==="exact")return l;let n=BigInt(s.defaultMultiple??10),f=l*n;if(s.maxCapUsd){let a=this.inferDecimals(e),p=BigInt(Math.floor(s.maxCapUsd*Math.pow(10,a)));if(f>p)return p}if(s.exactAboveUsd){let a=this.inferDecimals(e),p=BigInt(Math.floor(s.exactAboveUsd*Math.pow(10,a)));if(BigInt(e)>p)return l}return f}inferDecimals(e){return e.length>12?18:6}};function Q(r){return new Ae(r)}function De(r){if(r==="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"||r==="4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU")return!0;let e=r.toLowerCase();for(let t of Object.values(Be))if(t.toLowerCase()===e)return!0;for(let t of Object.keys(Ue))if(t.toLowerCase()===e)return!0;return!1}var He=new WeakMap;function Ye(r){return He.get(r)}function Te(r){let{adapters:e=[G({verbose:r.verbose}),Q({verbose:r.verbose})],wallets:t,wallet:s,preferredNetwork:l,rpcUrls:n={},maxAmountAtomic:f,fetch:a=globalThis.fetch,verbose:p=!1,accessPass:m,onPaymentRequired:P,maxRetries:E=0,retryDelayMs:T=500}=r,o=p?console.log.bind(console,"[x402]"):()=>{};async function D(i,c){let S;for(let u=0;u<=E;u++)try{let A=await a(i,c);if(A.status>=502&&A.status<=504&&u<E){o(`Retry ${u+1}/${E}: server returned ${A.status}`),await new Promise(v=>setTimeout(v,T*Math.pow(2,u)));continue}return A}catch(A){S=A,u<E&&(o(`Retry ${u+1}/${E}: ${A instanceof Error?A.message:"network error"}`),await new Promise(v=>setTimeout(v,T*Math.pow(2,u))))}throw S}let k=new Map;function O(i){try{let c=new URL(i).host,S=k.get(c);if(S&&S.expiresAt>Date.now()/1e3+10)return S.jwt;S&&k.delete(c)}catch{}return null}function J(i,c){try{let S=new URL(i).host,u=c.split(".");if(u.length===3){let A=JSON.parse(atob(u[1].replace(/-/g,"+").replace(/_/g,"/"))),v=Math.floor(Date.now()/1e3),y=v+86400,N=Math.min(typeof A.exp=="number"?A.exp:v,y);k.set(S,{jwt:c,expiresAt:N}),o("Access pass cached for",S,"| expires:",new Date(N*1e3).toISOString())}}catch{o("Failed to cache access pass")}}let R=t||{};s&&!R.solana&&V(s)&&(R.solana=s),s&&!R.evm&&H(s)&&(R.evm=s);function M(i){let c=[];for(let S of i){let u=e.find(v=>v.canHandle(S.network));if(!u)continue;let A;u.name==="Solana"?A=R.solana:u.name==="EVM"&&(A=R.evm),A&&u.isConnected(A)&&c.push({accept:S,adapter:u,wallet:A})}if(c.length===0)return null;if(l){let S=c.find(u=>u.accept.network===l);if(S)return S}return c[0]}function B(i,c){return{"eip155:56":"BSC","eip155:8453":"Base","eip155:84532":"Base Sepolia","eip155:42161":"Arbitrum","eip155:137":"Polygon","eip155:10":"Optimism","eip155:43114":"Avalanche","eip155:1":"Ethereum"}[i]||c}function x(i,c){return n[i]||c.getDefaultRpcUrl(i)}async function W(i,c,S,u,A){let v="";if(m?.preferTier&&u.tiers){let d=u.tiers.find(z=>z.id===m.preferTier);if(d){if(m.maxSpend&&parseFloat(d.price)>parseFloat(m.maxSpend))throw new U("access_pass_exceeds_max_spend",`Access pass tier "${d.id}" costs $${d.price}, exceeds max spend $${m.maxSpend}`);v=`tier=${d.id}`}}else if(m?.preferDuration&&u.ratePerHour)v=`duration=${m.preferDuration}`;else if(u.tiers&&u.tiers.length>0){let d=u.tiers[0];if(m?.maxSpend&&parseFloat(d.price)>parseFloat(m.maxSpend))throw new U("access_pass_exceeds_max_spend",`Cheapest access pass costs $${d.price}, exceeds max spend $${m?.maxSpend}`);v=`tier=${d.id}`}let y=v?A.includes("?")?`${A}&${v}`:`${A}?${v}`:A;o("Purchasing access pass:",v||"default tier");let N=S.headers.get("PAYMENT-REQUIRED");if(!N)return null;let w;try{w=JSON.parse(atob(N))}catch{return null}let b=M(w.accepts);if(!b)return null;let{accept:C,adapter:j,wallet:ee}=b;if(j.name==="Solana"&&!C.extra?.feePayer)return null;let g=C.extra?.decimals??(De(C.asset)?6:void 0);if(typeof g!="number")return null;let _=C.amount??C.maxAmountRequired;if(!_)return null;let I=x(C.network,j);try{let d=await j.getBalance(C,ee,I),z=Number(_)/Math.pow(10,g);if(d<z)throw new U("insufficient_balance",`Insufficient balance for access pass. Have $${d.toFixed(4)}, need $${z.toFixed(4)}`)}catch(d){if(d instanceof U)throw d}let q=await j.buildTransaction(C,ee,I),F;j.name==="EVM"?F=JSON.parse(q.serialized):F={transaction:q.serialized};let L=typeof i=="string"?i:i instanceof URL?i.href:i.url,X=w.resource;if(typeof w.resource=="string")try{let d=new URL(w.resource,L);["http:","https:"].includes(d.protocol)&&(X=d.toString())}catch{}else if(w.resource&&typeof w.resource=="object"&&"url"in w.resource){let d=w.resource;try{let z=new URL(d.url,L);["http:","https:"].includes(z.protocol)&&(X={...d,url:z.toString()})}catch{}}let Y={x402Version:C.x402Version??2,resource:X,accepted:C,payload:F};q.extensions&&(Y.extensions=q.extensions);let K=btoa(JSON.stringify(Y)),te=await a(y,{...c,method:"POST",headers:{...c?.headers||{},"Content-Type":"application/json","PAYMENT-SIGNATURE":K}});if(!te.ok)return o("Pass purchase failed:",te.status),null;let h=te.headers.get("ACCESS-PASS");return h&&(J(A,h),o("Access pass purchased and cached")),te}async function $(i,c){let S=typeof i=="string"?i:i instanceof URL?i.href:i.url;if(o("Making request:",S),m){let h=O(S);if(h){o("Using cached access pass");let d=await a(i,{...c,headers:{...c?.headers||{},Authorization:`Bearer ${h}`}});if(d.status!==401&&d.status!==402)return d;o("Cached pass rejected (status",d.status,"), purchasing new pass");try{k.delete(new URL(S).host)}catch{}}}let u=await D(i,c);if(u.status!==402)return u;o("Received 402 Payment Required");let A=u.headers.get("X-ACCESS-PASS-TIERS");if(m&&A){o("Server offers access passes, purchasing...");try{let h=JSON.parse(atob(A)),d=await W(i,c,u,h,S);if(d)return d}catch(h){o("Access pass purchase failed, falling back to per-request payment:",h)}}let v=u.headers.get("PAYMENT-REQUIRED");if(!v)throw new U("missing_payment_required_header","Server returned 402 but no PAYMENT-REQUIRED header");let y;try{let h=atob(v);y=JSON.parse(h)}catch{throw new U("invalid_payment_required","Failed to decode PAYMENT-REQUIRED header")}o("Payment requirements:",y);let N=u.headers.get("X-Quote-Hash");N&&o("Quote hash received:",N);let w=M(y.accepts);if(!w){let h=y.accepts.map(d=>d.network).join(", ");throw new U("no_matching_payment_option",`No connected wallet for any available network: ${h}`)}let{accept:b,adapter:C,wallet:j}=w;if(o(`Using ${C.name} for ${b.network}`),C.name==="Solana"&&!b.extra?.feePayer)throw new U("missing_fee_payer","Solana payment option missing feePayer in extra");let ee=b.extra?.decimals??(De(b.asset)?6:void 0);if(typeof ee!="number")throw new U("missing_decimals","Payment option missing decimals - provide in extra or use a known stablecoin");let g=b.amount??b.maxAmountRequired;if(!g)throw new U("missing_amount","Payment option missing amount");if(f&&BigInt(g)>BigInt(f))throw new U("amount_exceeds_max",`Payment amount ${g} exceeds maximum ${f}`);let _=x(b.network,C);o("Checking balance...");try{let h=await C.getBalance(b,j,_),d=Number(g)/Math.pow(10,ee);if(h<d){let z=B(b.network,C.name);throw new U("insufficient_balance",`Insufficient balance on ${z}. Have $${h.toFixed(4)}, need $${d.toFixed(4)}`)}o(`Balance OK: $${h.toFixed(4)} >= $${d.toFixed(4)}`)}catch(h){if(h instanceof U)throw h;o("Balance check failed (RPC error), proceeding with transaction attempt")}if(P&&!await P(b))throw new U("payment_rejected","Payment rejected by onPaymentRequired callback");o("Building transaction...");let I=await C.buildTransaction(b,j,_);o("Transaction signed");let q;C.name==="EVM"?q=JSON.parse(I.serialized):q={transaction:I.serialized};let F=typeof i=="string"?i:i instanceof URL?i.href:i.url,L=y.resource;if(typeof y.resource=="string")try{let h=new URL(y.resource,F).toString();h!==y.resource&&o("Resolved relative resource URL:",y.resource,"\u2192",h),L=h}catch{L=y.resource}else if(y.resource&&typeof y.resource=="object"&&"url"in y.resource){let h=y.resource;try{let d=new URL(h.url,F).toString();d!==h.url&&(o("Resolved relative resource URL:",h.url,"\u2192",d),L={...h,url:d})}catch{}}let X={x402Version:b.x402Version??2,resource:L,accepted:b,payload:q};I.extensions&&(X.extensions=I.extensions);let Y=btoa(JSON.stringify(X));o("Retrying request with payment...");let K=await D(i,{...c,headers:{...c?.headers||{},"PAYMENT-SIGNATURE":Y,...N?{"X-Quote-Hash":N}:{}}});if(o("Retry response status:",K.status),K.status===402){let h="unknown";try{let d=await K.clone().json();h=String(d.error||d.message||JSON.stringify(d)),o("Rejection reason:",h)}catch{}throw new U("payment_rejected",`Payment was rejected by the server: ${h}`)}let te=K.headers.get("PAYMENT-RESPONSE");if(te)try{let h=JSON.parse(atob(te));He.set(K,h),K._x402=h,h.extensions&&o("Settlement extensions:",Object.keys(h.extensions).join(", "))}catch{}return K}return{fetch:$}}function ot(r){return r.startsWith("solana:")||r==="solana"}function it(r){return r.startsWith("eip155:")||["base","ethereum","arbitrum"].includes(r)}function ct(r){return ot(r)?"solana":it(r)?"evm":"unknown"}function ke(r){return{[ne]:"Solana","solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1":"Solana Devnet","solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z":"Solana Testnet",solana:"Solana",[oe]:"Base","eip155:84532":"Base Sepolia","eip155:1":"Ethereum","eip155:42161":"Arbitrum One",base:"Base",ethereum:"Ethereum",arbitrum:"Arbitrum"}[r]||r}function ze(r,e){let t=ct(e);if(t==="solana")return e.includes("devnet")||e==="solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"?`https://solscan.io/tx/${r}?cluster=devnet`:`https://www.orbmarkets.io/tx/${r}`;if(t==="evm"){let s="8453";switch(e.startsWith("eip155:")?s=e.split(":")[1]:e==="ethereum"?s="1":e==="arbitrum"&&(s="42161"),s){case"8453":return`https://basescan.org/tx/${r}`;case"84532":return`https://sepolia.basescan.org/tx/${r}`;case"1":return`https://etherscan.io/tx/${r}`;case"42161":return`https://arbiscan.io/tx/${r}`;default:return`https://basescan.org/tx/${r}`}}return`https://solscan.io/tx/${r}`}function Ge(r){let e=Ye(r);if(e?.extensions?.["sponsored-access"])return e.extensions["sponsored-access"]}function Me(r){let e=Ge(r);if(e?.recommendations?.length)return e.recommendations}async function Oe(r){let t=Ge(r)?.tracking?.impressionBeacon;if(!t)return!1;try{await fetch(t,{method:"GET"})}catch{}return!0}function pt(r){let{wallets:e,wallet:t,preferredNetwork:s,rpcUrls:l={},verbose:n=!1}=r,[f,a]=ae(!1),[p,m]=ae("idle"),[P,E]=ae(null),[T,o]=ae(null),[D,k]=ae(null),[O,J]=ae([]),[R,M]=ae(null),B=ve((...y)=>{n&&console.log("[useX402Payment]",...y)},[n]),x=ge(()=>{let y={...e};return t&&!y.solana&&V(t)&&(y.solana=t),t&&!y.evm&&H(t)&&(y.evm=t),y},[e,t]),W=ge(()=>[G({verbose:n,rpcUrls:l}),Q({verbose:n,rpcUrls:l})],[n,l]),$=ge(()=>({solana:x.solana?V(x.solana)&&W[0].isConnected(x.solana):!1,evm:x.evm?H(x.evm)&&W[1].isConnected(x.evm):!1}),[x,W]),i=$.solana||$.evm,c=ve(async()=>{let y=[];if($.solana&&x.solana)try{let N=W.find(w=>w.name==="Solana");if(N){let w={scheme:"exact",network:ne,amount:"0",asset:qe,payTo:"",maxTimeoutSeconds:60,extra:{feePayer:"",decimals:6}},b=await N.getBalance(w,x.solana);y.push({network:ne,chainName:ke(ne),balance:b,asset:"USDC"})}}catch(N){B("Failed to fetch Solana balance:",N)}if($.evm&&x.evm)try{let N=W.find(w=>w.name==="EVM");if(N){let w={scheme:"exact",network:oe,amount:"0",asset:Le,payTo:"",maxTimeoutSeconds:60,extra:{feePayer:"",decimals:6}},b=await N.getBalance(w,x.evm);y.push({network:oe,chainName:ke(oe),balance:b,asset:"USDC"})}}catch(N){B("Failed to fetch Base balance:",N)}J(y)},[$,x,W,B]);lt(()=>{c();let y=setInterval(c,3e4);return()=>clearInterval(y)},[c]);let S=ve(()=>{a(!1),m("idle"),E(null),o(null),k(null),M(null)},[]),u=ge(()=>Te({adapters:W,wallets:x,preferredNetwork:s,rpcUrls:l,verbose:n}),[W,x,s,l,n]),A=ve(async(y,N)=>{if(a(!0),m("pending"),E(null),o(null),k(null),!i){let w=new U("wallet_not_connected","No wallet connected");throw E(w),m("error"),a(!1),w}try{let w=await u.fetch(y,N),b=w.headers.get("PAYMENT-RESPONSE");if(b)try{let j=JSON.parse(atob(b));j.transaction&&o(j.transaction),j.network&&k(j.network)}catch{B("Could not parse PAYMENT-RESPONSE header")}let C=Me(w);return M(C??null),C&&(B("Sponsored recommendations received:",C.length),Oe(w).catch(()=>{})),m("success"),w}catch(w){let b=w instanceof Error?w:new Error(String(w));throw E(b),m("error"),w}finally{a(!1),setTimeout(c,2e3)}},[u,i,B,c]),v=ge(()=>T?ze(T,D||s||ne):null,[T,D,s]);return{fetch:A,isLoading:f,status:p,error:P,transactionId:T,transactionNetwork:D,transactionUrl:v,balances:O,connectedChains:$,isAnyWalletConnected:i,reset:S,refreshBalances:c,accessPass:null,sponsoredRecommendations:R}}import{useState as Z,useCallback as Ce,useEffect as We,useMemo as $e,useRef as ut}from"react";function dt(r){let{wallets:e,wallet:t,preferredNetwork:s,rpcUrls:l={},resourceUrl:n,autoConnect:f=!0,verbose:a=!1}=r,p=`x402-access-pass:${n}`;function m(){if(typeof sessionStorage>"u")return null;try{let g=sessionStorage.getItem(p);if(!g)return null;let _=JSON.parse(g);return new Date(_.expiresAt).getTime()<=Date.now()?(sessionStorage.removeItem(p),null):_}catch{return null}}function P(g,_,I){if(!(typeof sessionStorage>"u"))try{sessionStorage.setItem(p,JSON.stringify({jwt:g,tier:_,expiresAt:I}))}catch{}}let E=m(),[T,o]=Z(null),[D,k]=Z(null),[O,J]=Z(!1),[R,M]=Z(E?.jwt||null),[B,x]=Z(E?{tier:E.tier,expiresAt:E.expiresAt}:null),[W,$]=Z(!1),[i,c]=Z(null),S=ut(R);We(()=>{S.current=R},[R]);let u=Ce((...g)=>{a&&console.log("[useAccessPass]",...g)},[a]),A=$e(()=>{let g={...e};return t&&!g.solana&&V(t)&&(g.solana=t),t&&!g.evm&&H(t)&&(g.evm=t),g},[e,t]),v=$e(()=>Te({adapters:[G({verbose:a,rpcUrls:l}),Q({verbose:a,rpcUrls:l})],wallets:A,preferredNetwork:s,rpcUrls:l,verbose:a,accessPass:{enabled:!0,autoRenew:!0}}),[A,s,l,a]),[y,N]=Z(0),w=$e(()=>{if(!R||!B)return null;let g=new Date(B.expiresAt).getTime(),_=Math.max(0,Math.floor((g-Date.now())/1e3));return _<=0?null:{jwt:R,tier:B.tier,expiresAt:B.expiresAt,remainingSeconds:_}},[R,B,y]),b=w!==null&&w.remainingSeconds>0;We(()=>{if(!R||!B)return;let g=setInterval(()=>N(_=>_+1),1e3);return()=>clearInterval(g)},[b]);let C=Ce(async()=>{J(!0);try{let g=await fetch(n);if(g.status===402){let _=g.headers.get("X-ACCESS-PASS-TIERS");if(_){let I=JSON.parse(atob(_));o(I.tiers||null),k(I.ratePerHour||null),u("Tier info loaded:",I)}}}catch(g){u("Failed to fetch tiers:",g)}finally{J(!1)}},[n,u]);We(()=>{f&&C()},[f,C]);let j=Ce(async(g,_)=>{$(!0),c(null);try{let I=n;g?I+=(I.includes("?")?"&":"?")+`tier=${g}`:_&&(I+=(I.includes("?")?"&":"?")+`duration=${_}`);let q=await v.fetch(I,{method:"POST"}),F=q.headers.get("ACCESS-PASS");if(u("ACCESS-PASS header:",F?"found":"NOT FOUND"),F){M(F);let L=g||"unknown",X="";try{let Y=await q.json();L=Y.accessPass?.tier||L,X=Y.accessPass?.expiresAt||""}catch{}if(!X)try{let Y=F.split(".");if(Y.length===3){let K=JSON.parse(atob(Y[1].replace(/-/g,"+").replace(/_/g,"/")));L=K.tier||L,X=new Date(K.exp*1e3).toISOString()}}catch{}x({tier:L,expiresAt:X}),P(F,L,X),u("Pass purchased and persisted:",L,X)}}catch(I){let q=I instanceof Error?I:new Error(String(I));throw c(q),q}finally{$(!1)}},[n,v,u]),ee=Ce(async(g,_)=>{let I=!g||g===""?n:g.startsWith("http")?g:`${n.replace(/\/$/,"")}${g.startsWith("/")?"":"/"}${g}`,q=S.current;if(q){try{let F=q.split(".");if(F.length===3){let L=JSON.parse(atob(F[1].replace(/-/g,"+").replace(/_/g,"/")));if(L.exp&&L.exp>Date.now()/1e3)return fetch(I,{..._,headers:{..._?.headers||{},Authorization:`Bearer ${q}`}})}}catch{}M(null),x(null);try{sessionStorage.removeItem(p)}catch{}}return v.fetch(I,_)},[n,v,p]);return{tiers:T,customRatePerHour:D,isLoadingTiers:O,pass:w,isPassValid:b,fetchTiers:C,purchasePass:j,isPurchasing:W,purchaseError:i,fetch:ee}}export{U as X402Error,Oe as fireImpressionBeacon,Me as getSponsoredRecommendations,dt as useAccessPass,pt as useX402Payment};
|