@dexterai/x402 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +254 -0
- package/assets/dexter-wordmark.svg +30 -0
- package/dist/adapters/index.cjs +481 -0
- package/dist/adapters/index.cjs.map +1 -0
- package/dist/adapters/index.d.cts +15 -0
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/index.js +473 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/client/index.cjs +602 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +4 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +583 -0
- package/dist/client/index.js.map +1 -0
- package/dist/evm-B4mhmeNZ.d.cts +120 -0
- package/dist/evm-av6iEAW7.d.ts +120 -0
- package/dist/react/index.cjs +827 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +108 -0
- package/dist/react/index.d.ts +108 -0
- package/dist/react/index.js +814 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/index.cjs +295 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +191 -0
- package/dist/server/index.d.ts +191 -0
- package/dist/server/index.js +262 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types-DkTxHOns.d.cts +119 -0
- package/dist/types-DkTxHOns.d.ts +119 -0
- package/dist/types-uljmYAuY.d.ts +118 -0
- package/dist/types-vWD8uj2B.d.cts +118 -0
- package/dist/x402-client-BSWNMJbm.d.ts +84 -0
- package/dist/x402-client-DUipGiRr.d.cts +84 -0
- package/package.json +99 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __esm = (fn, res) => function __init() {
|
|
3
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
// src/adapters/solana.ts
|
|
7
|
+
import {
|
|
8
|
+
PublicKey,
|
|
9
|
+
Connection,
|
|
10
|
+
TransactionMessage,
|
|
11
|
+
VersionedTransaction,
|
|
12
|
+
ComputeBudgetProgram
|
|
13
|
+
} from "@solana/web3.js";
|
|
14
|
+
import {
|
|
15
|
+
getAssociatedTokenAddress,
|
|
16
|
+
getAccount,
|
|
17
|
+
createTransferCheckedInstruction,
|
|
18
|
+
getMint,
|
|
19
|
+
TOKEN_PROGRAM_ID,
|
|
20
|
+
TOKEN_2022_PROGRAM_ID
|
|
21
|
+
} from "@solana/spl-token";
|
|
22
|
+
function isSolanaWallet(wallet) {
|
|
23
|
+
if (!wallet || typeof wallet !== "object") return false;
|
|
24
|
+
const w = wallet;
|
|
25
|
+
return "publicKey" in w && "signTransaction" in w && typeof w.signTransaction === "function";
|
|
26
|
+
}
|
|
27
|
+
function createSolanaAdapter(config) {
|
|
28
|
+
return new SolanaAdapter(config);
|
|
29
|
+
}
|
|
30
|
+
var SOLANA_MAINNET, SOLANA_DEVNET, SOLANA_TESTNET, DEFAULT_RPC_URLS, DEFAULT_COMPUTE_UNIT_LIMIT, DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS, SolanaAdapter;
|
|
31
|
+
var init_solana = __esm({
|
|
32
|
+
"src/adapters/solana.ts"() {
|
|
33
|
+
"use strict";
|
|
34
|
+
SOLANA_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
35
|
+
SOLANA_DEVNET = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
36
|
+
SOLANA_TESTNET = "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z";
|
|
37
|
+
DEFAULT_RPC_URLS = {
|
|
38
|
+
[SOLANA_MAINNET]: "https://api.mainnet-beta.solana.com",
|
|
39
|
+
[SOLANA_DEVNET]: "https://api.devnet.solana.com",
|
|
40
|
+
[SOLANA_TESTNET]: "https://api.testnet.solana.com"
|
|
41
|
+
};
|
|
42
|
+
DEFAULT_COMPUTE_UNIT_LIMIT = 12e3;
|
|
43
|
+
DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
|
|
44
|
+
SolanaAdapter = class {
|
|
45
|
+
name = "Solana";
|
|
46
|
+
networks = [SOLANA_MAINNET, SOLANA_DEVNET, SOLANA_TESTNET];
|
|
47
|
+
config;
|
|
48
|
+
log;
|
|
49
|
+
constructor(config = {}) {
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.log = config.verbose ? console.log.bind(console, "[x402:solana]") : () => {
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
canHandle(network) {
|
|
55
|
+
if (this.networks.includes(network)) return true;
|
|
56
|
+
if (network === "solana") return true;
|
|
57
|
+
if (network === "solana-devnet") return true;
|
|
58
|
+
if (network === "solana-testnet") return true;
|
|
59
|
+
if (network.startsWith("solana:")) return true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
getDefaultRpcUrl(network) {
|
|
63
|
+
if (this.config.rpcUrls?.[network]) {
|
|
64
|
+
return this.config.rpcUrls[network];
|
|
65
|
+
}
|
|
66
|
+
if (DEFAULT_RPC_URLS[network]) {
|
|
67
|
+
return DEFAULT_RPC_URLS[network];
|
|
68
|
+
}
|
|
69
|
+
if (network === "solana") return DEFAULT_RPC_URLS[SOLANA_MAINNET];
|
|
70
|
+
if (network === "solana-devnet") return DEFAULT_RPC_URLS[SOLANA_DEVNET];
|
|
71
|
+
if (network === "solana-testnet") return DEFAULT_RPC_URLS[SOLANA_TESTNET];
|
|
72
|
+
return DEFAULT_RPC_URLS[SOLANA_MAINNET];
|
|
73
|
+
}
|
|
74
|
+
getAddress(wallet) {
|
|
75
|
+
if (!isSolanaWallet(wallet)) return null;
|
|
76
|
+
return wallet.publicKey?.toBase58() ?? null;
|
|
77
|
+
}
|
|
78
|
+
isConnected(wallet) {
|
|
79
|
+
if (!isSolanaWallet(wallet)) return false;
|
|
80
|
+
return wallet.publicKey !== null;
|
|
81
|
+
}
|
|
82
|
+
async getBalance(accept, wallet, rpcUrl) {
|
|
83
|
+
if (!isSolanaWallet(wallet) || !wallet.publicKey) {
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
87
|
+
const connection = new Connection(url, "confirmed");
|
|
88
|
+
const userPubkey = new PublicKey(wallet.publicKey.toBase58());
|
|
89
|
+
const mintPubkey = new PublicKey(accept.asset);
|
|
90
|
+
try {
|
|
91
|
+
const mintInfo = await connection.getAccountInfo(mintPubkey, "confirmed");
|
|
92
|
+
const programId = mintInfo?.owner.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58() ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
|
|
93
|
+
const ata = await getAssociatedTokenAddress(
|
|
94
|
+
mintPubkey,
|
|
95
|
+
userPubkey,
|
|
96
|
+
false,
|
|
97
|
+
programId
|
|
98
|
+
);
|
|
99
|
+
const account = await getAccount(connection, ata, void 0, programId);
|
|
100
|
+
const decimals = accept.extra?.decimals ?? 6;
|
|
101
|
+
return Number(account.amount) / Math.pow(10, decimals);
|
|
102
|
+
} catch {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async buildTransaction(accept, wallet, rpcUrl) {
|
|
107
|
+
if (!isSolanaWallet(wallet)) {
|
|
108
|
+
throw new Error("Invalid Solana wallet");
|
|
109
|
+
}
|
|
110
|
+
if (!wallet.publicKey) {
|
|
111
|
+
throw new Error("Wallet not connected");
|
|
112
|
+
}
|
|
113
|
+
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
114
|
+
const connection = new Connection(url, "confirmed");
|
|
115
|
+
const userPubkey = new PublicKey(wallet.publicKey.toBase58());
|
|
116
|
+
const { payTo, asset, amount, extra } = accept;
|
|
117
|
+
if (!extra?.feePayer) {
|
|
118
|
+
throw new Error("Missing feePayer in payment requirements");
|
|
119
|
+
}
|
|
120
|
+
if (typeof extra?.decimals !== "number") {
|
|
121
|
+
throw new Error("Missing decimals in payment requirements");
|
|
122
|
+
}
|
|
123
|
+
const feePayerPubkey = new PublicKey(extra.feePayer);
|
|
124
|
+
const mintPubkey = new PublicKey(asset);
|
|
125
|
+
const destinationPubkey = new PublicKey(payTo);
|
|
126
|
+
this.log("Building transaction:", {
|
|
127
|
+
from: userPubkey.toBase58(),
|
|
128
|
+
to: payTo,
|
|
129
|
+
amount,
|
|
130
|
+
asset,
|
|
131
|
+
feePayer: extra.feePayer
|
|
132
|
+
});
|
|
133
|
+
const instructions = [];
|
|
134
|
+
instructions.push(
|
|
135
|
+
ComputeBudgetProgram.setComputeUnitLimit({
|
|
136
|
+
units: DEFAULT_COMPUTE_UNIT_LIMIT
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
instructions.push(
|
|
140
|
+
ComputeBudgetProgram.setComputeUnitPrice({
|
|
141
|
+
microLamports: DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
const mintInfo = await connection.getAccountInfo(mintPubkey, "confirmed");
|
|
145
|
+
if (!mintInfo) {
|
|
146
|
+
throw new Error(`Token mint ${asset} not found`);
|
|
147
|
+
}
|
|
148
|
+
const programId = mintInfo.owner.toBase58() === TOKEN_2022_PROGRAM_ID.toBase58() ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID;
|
|
149
|
+
const mint = await getMint(connection, mintPubkey, void 0, programId);
|
|
150
|
+
if (mint.decimals !== extra.decimals) {
|
|
151
|
+
this.log(
|
|
152
|
+
`Decimals mismatch: requirements say ${extra.decimals}, mint says ${mint.decimals}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const sourceAta = await getAssociatedTokenAddress(
|
|
156
|
+
mintPubkey,
|
|
157
|
+
userPubkey,
|
|
158
|
+
false,
|
|
159
|
+
programId
|
|
160
|
+
);
|
|
161
|
+
const destinationAta = await getAssociatedTokenAddress(
|
|
162
|
+
mintPubkey,
|
|
163
|
+
destinationPubkey,
|
|
164
|
+
false,
|
|
165
|
+
programId
|
|
166
|
+
);
|
|
167
|
+
const sourceAtaInfo = await connection.getAccountInfo(sourceAta, "confirmed");
|
|
168
|
+
if (!sourceAtaInfo) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`No token account found for ${asset}. Please ensure you have USDC in your wallet.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const destAtaInfo = await connection.getAccountInfo(destinationAta, "confirmed");
|
|
174
|
+
if (!destAtaInfo) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Seller token account not found. The seller (${payTo}) must have a USDC account.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const amountBigInt = BigInt(amount);
|
|
180
|
+
instructions.push(
|
|
181
|
+
createTransferCheckedInstruction(
|
|
182
|
+
sourceAta,
|
|
183
|
+
mintPubkey,
|
|
184
|
+
destinationAta,
|
|
185
|
+
userPubkey,
|
|
186
|
+
amountBigInt,
|
|
187
|
+
mint.decimals,
|
|
188
|
+
[],
|
|
189
|
+
programId
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
const { blockhash } = await connection.getLatestBlockhash("confirmed");
|
|
193
|
+
const message = new TransactionMessage({
|
|
194
|
+
payerKey: feePayerPubkey,
|
|
195
|
+
recentBlockhash: blockhash,
|
|
196
|
+
instructions
|
|
197
|
+
}).compileToV0Message();
|
|
198
|
+
const transaction = new VersionedTransaction(message);
|
|
199
|
+
const signedTx = await wallet.signTransaction(transaction);
|
|
200
|
+
this.log("Transaction signed successfully");
|
|
201
|
+
return {
|
|
202
|
+
serialized: Buffer.from(signedTx.serialize()).toString("base64")
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// src/adapters/evm.ts
|
|
210
|
+
function isEvmWallet(wallet) {
|
|
211
|
+
if (!wallet || typeof wallet !== "object") return false;
|
|
212
|
+
const w = wallet;
|
|
213
|
+
return "address" in w && typeof w.address === "string" && w.address.startsWith("0x");
|
|
214
|
+
}
|
|
215
|
+
function createEvmAdapter(config) {
|
|
216
|
+
return new EvmAdapter(config);
|
|
217
|
+
}
|
|
218
|
+
var BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE, CHAIN_IDS, DEFAULT_RPC_URLS2, USDC_ADDRESSES, EvmAdapter;
|
|
219
|
+
var init_evm = __esm({
|
|
220
|
+
"src/adapters/evm.ts"() {
|
|
221
|
+
"use strict";
|
|
222
|
+
BASE_MAINNET = "eip155:8453";
|
|
223
|
+
BASE_SEPOLIA = "eip155:84532";
|
|
224
|
+
ETHEREUM_MAINNET = "eip155:1";
|
|
225
|
+
ARBITRUM_ONE = "eip155:42161";
|
|
226
|
+
CHAIN_IDS = {
|
|
227
|
+
[BASE_MAINNET]: 8453,
|
|
228
|
+
[BASE_SEPOLIA]: 84532,
|
|
229
|
+
[ETHEREUM_MAINNET]: 1,
|
|
230
|
+
[ARBITRUM_ONE]: 42161
|
|
231
|
+
};
|
|
232
|
+
DEFAULT_RPC_URLS2 = {
|
|
233
|
+
[BASE_MAINNET]: "https://mainnet.base.org",
|
|
234
|
+
[BASE_SEPOLIA]: "https://sepolia.base.org",
|
|
235
|
+
[ETHEREUM_MAINNET]: "https://eth.llamarpc.com",
|
|
236
|
+
[ARBITRUM_ONE]: "https://arb1.arbitrum.io/rpc"
|
|
237
|
+
};
|
|
238
|
+
USDC_ADDRESSES = {
|
|
239
|
+
[BASE_MAINNET]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
240
|
+
[ETHEREUM_MAINNET]: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
241
|
+
[ARBITRUM_ONE]: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
|
|
242
|
+
};
|
|
243
|
+
EvmAdapter = class {
|
|
244
|
+
name = "EVM";
|
|
245
|
+
networks = [BASE_MAINNET, BASE_SEPOLIA, ETHEREUM_MAINNET, ARBITRUM_ONE];
|
|
246
|
+
config;
|
|
247
|
+
log;
|
|
248
|
+
constructor(config = {}) {
|
|
249
|
+
this.config = config;
|
|
250
|
+
this.log = config.verbose ? console.log.bind(console, "[x402:evm]") : () => {
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
canHandle(network) {
|
|
254
|
+
if (this.networks.includes(network)) return true;
|
|
255
|
+
if (network === "base") return true;
|
|
256
|
+
if (network === "ethereum") return true;
|
|
257
|
+
if (network === "arbitrum") return true;
|
|
258
|
+
if (network.startsWith("eip155:")) return true;
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
getDefaultRpcUrl(network) {
|
|
262
|
+
if (this.config.rpcUrls?.[network]) {
|
|
263
|
+
return this.config.rpcUrls[network];
|
|
264
|
+
}
|
|
265
|
+
if (DEFAULT_RPC_URLS2[network]) {
|
|
266
|
+
return DEFAULT_RPC_URLS2[network];
|
|
267
|
+
}
|
|
268
|
+
if (network === "base") return DEFAULT_RPC_URLS2[BASE_MAINNET];
|
|
269
|
+
if (network === "ethereum") return DEFAULT_RPC_URLS2[ETHEREUM_MAINNET];
|
|
270
|
+
if (network === "arbitrum") return DEFAULT_RPC_URLS2[ARBITRUM_ONE];
|
|
271
|
+
return DEFAULT_RPC_URLS2[BASE_MAINNET];
|
|
272
|
+
}
|
|
273
|
+
getAddress(wallet) {
|
|
274
|
+
if (!isEvmWallet(wallet)) return null;
|
|
275
|
+
return wallet.address;
|
|
276
|
+
}
|
|
277
|
+
isConnected(wallet) {
|
|
278
|
+
if (!isEvmWallet(wallet)) return false;
|
|
279
|
+
return !!wallet.address;
|
|
280
|
+
}
|
|
281
|
+
getChainId(network) {
|
|
282
|
+
if (CHAIN_IDS[network]) return CHAIN_IDS[network];
|
|
283
|
+
if (network.startsWith("eip155:")) {
|
|
284
|
+
const chainIdStr = network.split(":")[1];
|
|
285
|
+
return parseInt(chainIdStr, 10);
|
|
286
|
+
}
|
|
287
|
+
if (network === "base") return 8453;
|
|
288
|
+
if (network === "ethereum") return 1;
|
|
289
|
+
if (network === "arbitrum") return 42161;
|
|
290
|
+
return 8453;
|
|
291
|
+
}
|
|
292
|
+
async getBalance(accept, wallet, rpcUrl) {
|
|
293
|
+
if (!isEvmWallet(wallet) || !wallet.address) {
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
const url = rpcUrl || this.getDefaultRpcUrl(accept.network);
|
|
297
|
+
try {
|
|
298
|
+
const data = this.encodeBalanceOf(wallet.address);
|
|
299
|
+
const response = await fetch(url, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers: { "Content-Type": "application/json" },
|
|
302
|
+
body: JSON.stringify({
|
|
303
|
+
jsonrpc: "2.0",
|
|
304
|
+
id: 1,
|
|
305
|
+
method: "eth_call",
|
|
306
|
+
params: [
|
|
307
|
+
{
|
|
308
|
+
to: accept.asset,
|
|
309
|
+
data
|
|
310
|
+
},
|
|
311
|
+
"latest"
|
|
312
|
+
]
|
|
313
|
+
})
|
|
314
|
+
});
|
|
315
|
+
const result = await response.json();
|
|
316
|
+
if (result.error || !result.result) {
|
|
317
|
+
return 0;
|
|
318
|
+
}
|
|
319
|
+
const balance = BigInt(result.result);
|
|
320
|
+
const decimals = accept.extra?.decimals ?? 6;
|
|
321
|
+
return Number(balance) / Math.pow(10, decimals);
|
|
322
|
+
} catch {
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
encodeBalanceOf(address) {
|
|
327
|
+
const selector = "0x70a08231";
|
|
328
|
+
const paddedAddress = address.slice(2).toLowerCase().padStart(64, "0");
|
|
329
|
+
return selector + paddedAddress;
|
|
330
|
+
}
|
|
331
|
+
async buildTransaction(accept, wallet, _rpcUrl) {
|
|
332
|
+
if (!isEvmWallet(wallet)) {
|
|
333
|
+
throw new Error("Invalid EVM wallet");
|
|
334
|
+
}
|
|
335
|
+
if (!wallet.address) {
|
|
336
|
+
throw new Error("Wallet not connected");
|
|
337
|
+
}
|
|
338
|
+
const { payTo, asset, amount, extra } = accept;
|
|
339
|
+
this.log("Building EVM transaction:", {
|
|
340
|
+
from: wallet.address,
|
|
341
|
+
to: payTo,
|
|
342
|
+
amount,
|
|
343
|
+
asset,
|
|
344
|
+
network: accept.network
|
|
345
|
+
});
|
|
346
|
+
const chainId = this.getChainId(accept.network);
|
|
347
|
+
const domain = {
|
|
348
|
+
name: extra?.name ?? "USD Coin",
|
|
349
|
+
version: extra?.version ?? "2",
|
|
350
|
+
chainId: BigInt(chainId),
|
|
351
|
+
verifyingContract: asset
|
|
352
|
+
};
|
|
353
|
+
const types = {
|
|
354
|
+
TransferWithAuthorization: [
|
|
355
|
+
{ name: "from", type: "address" },
|
|
356
|
+
{ name: "to", type: "address" },
|
|
357
|
+
{ name: "value", type: "uint256" },
|
|
358
|
+
{ name: "validAfter", type: "uint256" },
|
|
359
|
+
{ name: "validBefore", type: "uint256" },
|
|
360
|
+
{ name: "nonce", type: "bytes32" }
|
|
361
|
+
]
|
|
362
|
+
};
|
|
363
|
+
const nonce = "0x" + [...Array(32)].map(() => Math.floor(Math.random() * 256).toString(16).padStart(2, "0")).join("");
|
|
364
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
365
|
+
const authorization = {
|
|
366
|
+
from: wallet.address,
|
|
367
|
+
to: payTo,
|
|
368
|
+
value: amount,
|
|
369
|
+
// string
|
|
370
|
+
validAfter: String(now - 600),
|
|
371
|
+
// 10 minutes before (matching upstream)
|
|
372
|
+
validBefore: String(now + (accept.maxTimeoutSeconds || 60)),
|
|
373
|
+
nonce
|
|
374
|
+
};
|
|
375
|
+
const message = {
|
|
376
|
+
from: wallet.address,
|
|
377
|
+
to: payTo,
|
|
378
|
+
value: BigInt(amount),
|
|
379
|
+
validAfter: BigInt(now - 600),
|
|
380
|
+
validBefore: BigInt(now + (accept.maxTimeoutSeconds || 60)),
|
|
381
|
+
nonce
|
|
382
|
+
};
|
|
383
|
+
if (!wallet.signTypedData) {
|
|
384
|
+
throw new Error("Wallet does not support signTypedData (EIP-712)");
|
|
385
|
+
}
|
|
386
|
+
const signature = await wallet.signTypedData({
|
|
387
|
+
domain,
|
|
388
|
+
types,
|
|
389
|
+
primaryType: "TransferWithAuthorization",
|
|
390
|
+
message
|
|
391
|
+
});
|
|
392
|
+
this.log("EIP-712 signature obtained");
|
|
393
|
+
const payload = {
|
|
394
|
+
authorization,
|
|
395
|
+
signature
|
|
396
|
+
};
|
|
397
|
+
return {
|
|
398
|
+
serialized: JSON.stringify(payload),
|
|
399
|
+
signature
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// src/react/useX402Payment.ts
|
|
407
|
+
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
408
|
+
|
|
409
|
+
// src/types.ts
|
|
410
|
+
var SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
411
|
+
var BASE_MAINNET_NETWORK = "eip155:8453";
|
|
412
|
+
var USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
413
|
+
var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
414
|
+
var X402Error = class _X402Error extends Error {
|
|
415
|
+
/** Error code for programmatic handling */
|
|
416
|
+
code;
|
|
417
|
+
/** Additional error details */
|
|
418
|
+
details;
|
|
419
|
+
constructor(code, message, details) {
|
|
420
|
+
super(message);
|
|
421
|
+
this.name = "X402Error";
|
|
422
|
+
this.code = code;
|
|
423
|
+
this.details = details;
|
|
424
|
+
Object.setPrototypeOf(this, _X402Error.prototype);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// src/adapters/index.ts
|
|
429
|
+
init_solana();
|
|
430
|
+
init_evm();
|
|
431
|
+
|
|
432
|
+
// src/client/x402-client.ts
|
|
433
|
+
function createX402Client(config) {
|
|
434
|
+
const {
|
|
435
|
+
adapters = [createSolanaAdapter({ verbose: config.verbose }), createEvmAdapter({ verbose: config.verbose })],
|
|
436
|
+
wallets: walletSet,
|
|
437
|
+
wallet: legacyWallet,
|
|
438
|
+
preferredNetwork,
|
|
439
|
+
rpcUrls = {},
|
|
440
|
+
maxAmountAtomic,
|
|
441
|
+
fetch: customFetch = globalThis.fetch,
|
|
442
|
+
verbose = false
|
|
443
|
+
} = config;
|
|
444
|
+
const log = verbose ? console.log.bind(console, "[x402]") : () => {
|
|
445
|
+
};
|
|
446
|
+
const wallets = walletSet || {};
|
|
447
|
+
if (legacyWallet && !wallets.solana && isSolanaWallet(legacyWallet)) {
|
|
448
|
+
wallets.solana = legacyWallet;
|
|
449
|
+
}
|
|
450
|
+
if (legacyWallet && !wallets.evm && isEvmWallet(legacyWallet)) {
|
|
451
|
+
wallets.evm = legacyWallet;
|
|
452
|
+
}
|
|
453
|
+
function findPaymentOption(accepts) {
|
|
454
|
+
const candidates = [];
|
|
455
|
+
for (const accept of accepts) {
|
|
456
|
+
const adapter = adapters.find((a) => a.canHandle(accept.network));
|
|
457
|
+
if (!adapter) continue;
|
|
458
|
+
let wallet;
|
|
459
|
+
if (adapter.name === "Solana") {
|
|
460
|
+
wallet = wallets.solana;
|
|
461
|
+
} else if (adapter.name === "EVM") {
|
|
462
|
+
wallet = wallets.evm;
|
|
463
|
+
}
|
|
464
|
+
if (wallet && adapter.isConnected(wallet)) {
|
|
465
|
+
candidates.push({ accept, adapter, wallet });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (candidates.length === 0) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
if (preferredNetwork) {
|
|
472
|
+
const preferred = candidates.find((c) => c.accept.network === preferredNetwork);
|
|
473
|
+
if (preferred) return preferred;
|
|
474
|
+
}
|
|
475
|
+
return candidates[0];
|
|
476
|
+
}
|
|
477
|
+
function getRpcUrl(network, adapter) {
|
|
478
|
+
return rpcUrls[network] || adapter.getDefaultRpcUrl(network);
|
|
479
|
+
}
|
|
480
|
+
async function x402Fetch(input, init) {
|
|
481
|
+
log("Making request:", input);
|
|
482
|
+
const response = await customFetch(input, init);
|
|
483
|
+
if (response.status !== 402) {
|
|
484
|
+
return response;
|
|
485
|
+
}
|
|
486
|
+
log("Received 402 Payment Required");
|
|
487
|
+
const paymentRequiredHeader = response.headers.get("PAYMENT-REQUIRED");
|
|
488
|
+
if (!paymentRequiredHeader) {
|
|
489
|
+
throw new X402Error(
|
|
490
|
+
"missing_payment_required_header",
|
|
491
|
+
"Server returned 402 but no PAYMENT-REQUIRED header"
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
let requirements;
|
|
495
|
+
try {
|
|
496
|
+
const decoded = atob(paymentRequiredHeader);
|
|
497
|
+
requirements = JSON.parse(decoded);
|
|
498
|
+
} catch {
|
|
499
|
+
throw new X402Error(
|
|
500
|
+
"invalid_payment_required",
|
|
501
|
+
"Failed to decode PAYMENT-REQUIRED header"
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
log("Payment requirements:", requirements);
|
|
505
|
+
const match = findPaymentOption(requirements.accepts);
|
|
506
|
+
if (!match) {
|
|
507
|
+
const availableNetworks = requirements.accepts.map((a) => a.network).join(", ");
|
|
508
|
+
throw new X402Error(
|
|
509
|
+
"no_matching_payment_option",
|
|
510
|
+
`No connected wallet for any available network: ${availableNetworks}`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
const { accept, adapter, wallet } = match;
|
|
514
|
+
log(`Using ${adapter.name} for ${accept.network}`);
|
|
515
|
+
if (adapter.name === "Solana" && !accept.extra?.feePayer) {
|
|
516
|
+
throw new X402Error(
|
|
517
|
+
"missing_fee_payer",
|
|
518
|
+
"Solana payment option missing feePayer in extra"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (typeof accept.extra?.decimals !== "number") {
|
|
522
|
+
throw new X402Error(
|
|
523
|
+
"missing_decimals",
|
|
524
|
+
"Payment option missing decimals in extra"
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
if (maxAmountAtomic && BigInt(accept.amount) > BigInt(maxAmountAtomic)) {
|
|
528
|
+
throw new X402Error(
|
|
529
|
+
"amount_exceeds_max",
|
|
530
|
+
`Payment amount ${accept.amount} exceeds maximum ${maxAmountAtomic}`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
log("Building transaction...");
|
|
534
|
+
const rpcUrl = getRpcUrl(accept.network, adapter);
|
|
535
|
+
const signedTx = await adapter.buildTransaction(accept, wallet, rpcUrl);
|
|
536
|
+
log("Transaction signed");
|
|
537
|
+
let payload;
|
|
538
|
+
if (adapter.name === "EVM") {
|
|
539
|
+
payload = JSON.parse(signedTx.serialized);
|
|
540
|
+
} else {
|
|
541
|
+
payload = { transaction: signedTx.serialized };
|
|
542
|
+
}
|
|
543
|
+
const paymentSignature = {
|
|
544
|
+
x402Version: 2,
|
|
545
|
+
resource: requirements.resource,
|
|
546
|
+
accepted: accept,
|
|
547
|
+
payload
|
|
548
|
+
};
|
|
549
|
+
const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
|
|
550
|
+
log("Retrying request with payment...");
|
|
551
|
+
const retryResponse = await customFetch(input, {
|
|
552
|
+
...init,
|
|
553
|
+
headers: {
|
|
554
|
+
...init?.headers || {},
|
|
555
|
+
"PAYMENT-SIGNATURE": paymentSignatureHeader
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
log("Retry response status:", retryResponse.status);
|
|
559
|
+
if (retryResponse.status === 402) {
|
|
560
|
+
let reason = "unknown";
|
|
561
|
+
try {
|
|
562
|
+
const body = await retryResponse.clone().json();
|
|
563
|
+
reason = String(body.error || body.message || JSON.stringify(body));
|
|
564
|
+
log("Rejection reason:", reason);
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
throw new X402Error(
|
|
568
|
+
"payment_rejected",
|
|
569
|
+
`Payment was rejected by the server: ${reason}`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
return retryResponse;
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
fetch: x402Fetch
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/utils.ts
|
|
580
|
+
function getChainFamily(network) {
|
|
581
|
+
if (network.startsWith("solana:") || network === "solana") {
|
|
582
|
+
return "solana";
|
|
583
|
+
}
|
|
584
|
+
if (network.startsWith("eip155:") || ["base", "ethereum", "arbitrum"].includes(network)) {
|
|
585
|
+
return "evm";
|
|
586
|
+
}
|
|
587
|
+
return "unknown";
|
|
588
|
+
}
|
|
589
|
+
function getChainName(network) {
|
|
590
|
+
const mapping = {
|
|
591
|
+
[SOLANA_MAINNET_NETWORK]: "Solana",
|
|
592
|
+
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": "Solana Devnet",
|
|
593
|
+
"solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z": "Solana Testnet",
|
|
594
|
+
"solana": "Solana",
|
|
595
|
+
[BASE_MAINNET_NETWORK]: "Base",
|
|
596
|
+
"eip155:84532": "Base Sepolia",
|
|
597
|
+
"eip155:1": "Ethereum",
|
|
598
|
+
"eip155:42161": "Arbitrum One",
|
|
599
|
+
"base": "Base",
|
|
600
|
+
"ethereum": "Ethereum",
|
|
601
|
+
"arbitrum": "Arbitrum"
|
|
602
|
+
};
|
|
603
|
+
return mapping[network] || network;
|
|
604
|
+
}
|
|
605
|
+
function getExplorerUrl(txSignature, network) {
|
|
606
|
+
const family = getChainFamily(network);
|
|
607
|
+
if (family === "solana") {
|
|
608
|
+
const isDevnet = network.includes("devnet") || network === "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
609
|
+
if (isDevnet) {
|
|
610
|
+
return `https://solscan.io/tx/${txSignature}?cluster=devnet`;
|
|
611
|
+
}
|
|
612
|
+
return `https://www.orbmarkets.io/tx/${txSignature}`;
|
|
613
|
+
}
|
|
614
|
+
if (family === "evm") {
|
|
615
|
+
let chainId = "8453";
|
|
616
|
+
if (network.startsWith("eip155:")) {
|
|
617
|
+
chainId = network.split(":")[1];
|
|
618
|
+
} else if (network === "ethereum") {
|
|
619
|
+
chainId = "1";
|
|
620
|
+
} else if (network === "arbitrum") {
|
|
621
|
+
chainId = "42161";
|
|
622
|
+
}
|
|
623
|
+
switch (chainId) {
|
|
624
|
+
case "8453":
|
|
625
|
+
return `https://basescan.org/tx/${txSignature}`;
|
|
626
|
+
case "84532":
|
|
627
|
+
return `https://sepolia.basescan.org/tx/${txSignature}`;
|
|
628
|
+
case "1":
|
|
629
|
+
return `https://etherscan.io/tx/${txSignature}`;
|
|
630
|
+
case "42161":
|
|
631
|
+
return `https://arbiscan.io/tx/${txSignature}`;
|
|
632
|
+
default:
|
|
633
|
+
return `https://basescan.org/tx/${txSignature}`;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return `https://solscan.io/tx/${txSignature}`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/react/useX402Payment.ts
|
|
640
|
+
function useX402Payment(config) {
|
|
641
|
+
const {
|
|
642
|
+
wallets: walletSet,
|
|
643
|
+
wallet: legacyWallet,
|
|
644
|
+
preferredNetwork,
|
|
645
|
+
rpcUrls = {},
|
|
646
|
+
verbose = false
|
|
647
|
+
} = config;
|
|
648
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
649
|
+
const [status, setStatus] = useState("idle");
|
|
650
|
+
const [error, setError] = useState(null);
|
|
651
|
+
const [transactionId, setTransactionId] = useState(null);
|
|
652
|
+
const [transactionNetwork, setTransactionNetwork] = useState(null);
|
|
653
|
+
const [balances, setBalances] = useState([]);
|
|
654
|
+
const log = useCallback((...args) => {
|
|
655
|
+
if (verbose) console.log("[useX402Payment]", ...args);
|
|
656
|
+
}, [verbose]);
|
|
657
|
+
const wallets = useMemo(() => {
|
|
658
|
+
const w = { ...walletSet };
|
|
659
|
+
if (legacyWallet && !w.solana && isSolanaWallet(legacyWallet)) {
|
|
660
|
+
w.solana = legacyWallet;
|
|
661
|
+
}
|
|
662
|
+
if (legacyWallet && !w.evm && isEvmWallet(legacyWallet)) {
|
|
663
|
+
w.evm = legacyWallet;
|
|
664
|
+
}
|
|
665
|
+
return w;
|
|
666
|
+
}, [walletSet, legacyWallet]);
|
|
667
|
+
const adapters = useMemo(() => [
|
|
668
|
+
createSolanaAdapter({ verbose, rpcUrls }),
|
|
669
|
+
createEvmAdapter({ verbose, rpcUrls })
|
|
670
|
+
], [verbose, rpcUrls]);
|
|
671
|
+
const connectedChains = useMemo(() => ({
|
|
672
|
+
solana: wallets.solana ? isSolanaWallet(wallets.solana) && adapters[0].isConnected(wallets.solana) : false,
|
|
673
|
+
evm: wallets.evm ? isEvmWallet(wallets.evm) && adapters[1].isConnected(wallets.evm) : false
|
|
674
|
+
}), [wallets, adapters]);
|
|
675
|
+
const isAnyWalletConnected = connectedChains.solana || connectedChains.evm;
|
|
676
|
+
const refreshBalances = useCallback(async () => {
|
|
677
|
+
const newBalances = [];
|
|
678
|
+
if (connectedChains.solana && wallets.solana) {
|
|
679
|
+
try {
|
|
680
|
+
const solanaAdapter = adapters.find((a) => a.name === "Solana");
|
|
681
|
+
if (solanaAdapter) {
|
|
682
|
+
const accept = {
|
|
683
|
+
scheme: "exact",
|
|
684
|
+
network: SOLANA_MAINNET_NETWORK,
|
|
685
|
+
amount: "0",
|
|
686
|
+
asset: USDC_MINT,
|
|
687
|
+
payTo: "",
|
|
688
|
+
maxTimeoutSeconds: 60,
|
|
689
|
+
extra: { feePayer: "", decimals: 6 }
|
|
690
|
+
};
|
|
691
|
+
const balance = await solanaAdapter.getBalance(accept, wallets.solana);
|
|
692
|
+
newBalances.push({
|
|
693
|
+
network: SOLANA_MAINNET_NETWORK,
|
|
694
|
+
chainName: getChainName(SOLANA_MAINNET_NETWORK),
|
|
695
|
+
balance,
|
|
696
|
+
asset: "USDC"
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
} catch (e) {
|
|
700
|
+
log("Failed to fetch Solana balance:", e);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (connectedChains.evm && wallets.evm) {
|
|
704
|
+
try {
|
|
705
|
+
const evmAdapter = adapters.find((a) => a.name === "EVM");
|
|
706
|
+
if (evmAdapter) {
|
|
707
|
+
const accept = {
|
|
708
|
+
scheme: "exact",
|
|
709
|
+
network: BASE_MAINNET_NETWORK,
|
|
710
|
+
amount: "0",
|
|
711
|
+
asset: USDC_BASE,
|
|
712
|
+
payTo: "",
|
|
713
|
+
maxTimeoutSeconds: 60,
|
|
714
|
+
extra: { feePayer: "", decimals: 6 }
|
|
715
|
+
};
|
|
716
|
+
const balance = await evmAdapter.getBalance(accept, wallets.evm);
|
|
717
|
+
newBalances.push({
|
|
718
|
+
network: BASE_MAINNET_NETWORK,
|
|
719
|
+
chainName: getChainName(BASE_MAINNET_NETWORK),
|
|
720
|
+
balance,
|
|
721
|
+
asset: "USDC"
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
} catch (e) {
|
|
725
|
+
log("Failed to fetch Base balance:", e);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
setBalances(newBalances);
|
|
729
|
+
}, [connectedChains, wallets, adapters, log]);
|
|
730
|
+
useEffect(() => {
|
|
731
|
+
refreshBalances();
|
|
732
|
+
const interval = setInterval(refreshBalances, 3e4);
|
|
733
|
+
return () => clearInterval(interval);
|
|
734
|
+
}, [refreshBalances]);
|
|
735
|
+
const reset = useCallback(() => {
|
|
736
|
+
setIsLoading(false);
|
|
737
|
+
setStatus("idle");
|
|
738
|
+
setError(null);
|
|
739
|
+
setTransactionId(null);
|
|
740
|
+
setTransactionNetwork(null);
|
|
741
|
+
}, []);
|
|
742
|
+
const client = useMemo(() => createX402Client({
|
|
743
|
+
adapters,
|
|
744
|
+
wallets,
|
|
745
|
+
preferredNetwork,
|
|
746
|
+
rpcUrls,
|
|
747
|
+
verbose
|
|
748
|
+
}), [adapters, wallets, preferredNetwork, rpcUrls, verbose]);
|
|
749
|
+
const fetchWithPayment = useCallback(async (input, init) => {
|
|
750
|
+
setIsLoading(true);
|
|
751
|
+
setStatus("pending");
|
|
752
|
+
setError(null);
|
|
753
|
+
setTransactionId(null);
|
|
754
|
+
setTransactionNetwork(null);
|
|
755
|
+
if (!isAnyWalletConnected) {
|
|
756
|
+
const connError = new X402Error("wallet_not_connected", "No wallet connected");
|
|
757
|
+
setError(connError);
|
|
758
|
+
setStatus("error");
|
|
759
|
+
setIsLoading(false);
|
|
760
|
+
throw connError;
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
const response = await client.fetch(input, init);
|
|
764
|
+
const paymentResponse = response.headers.get("PAYMENT-RESPONSE");
|
|
765
|
+
if (paymentResponse) {
|
|
766
|
+
try {
|
|
767
|
+
const decoded = JSON.parse(atob(paymentResponse));
|
|
768
|
+
if (decoded.transaction) {
|
|
769
|
+
setTransactionId(decoded.transaction);
|
|
770
|
+
}
|
|
771
|
+
if (decoded.network) {
|
|
772
|
+
setTransactionNetwork(decoded.network);
|
|
773
|
+
}
|
|
774
|
+
} catch {
|
|
775
|
+
log("Could not parse PAYMENT-RESPONSE header");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
setStatus("success");
|
|
779
|
+
return response;
|
|
780
|
+
} catch (err) {
|
|
781
|
+
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
782
|
+
setError(error2);
|
|
783
|
+
setStatus("error");
|
|
784
|
+
throw err;
|
|
785
|
+
} finally {
|
|
786
|
+
setIsLoading(false);
|
|
787
|
+
setTimeout(refreshBalances, 2e3);
|
|
788
|
+
}
|
|
789
|
+
}, [client, isAnyWalletConnected, log, refreshBalances]);
|
|
790
|
+
const transactionUrl = useMemo(() => {
|
|
791
|
+
if (!transactionId) return null;
|
|
792
|
+
const network = transactionNetwork || preferredNetwork || SOLANA_MAINNET_NETWORK;
|
|
793
|
+
return getExplorerUrl(transactionId, network);
|
|
794
|
+
}, [transactionId, transactionNetwork, preferredNetwork]);
|
|
795
|
+
return {
|
|
796
|
+
fetch: fetchWithPayment,
|
|
797
|
+
isLoading,
|
|
798
|
+
status,
|
|
799
|
+
error,
|
|
800
|
+
transactionId,
|
|
801
|
+
transactionNetwork,
|
|
802
|
+
transactionUrl,
|
|
803
|
+
balances,
|
|
804
|
+
connectedChains,
|
|
805
|
+
isAnyWalletConnected,
|
|
806
|
+
reset,
|
|
807
|
+
refreshBalances
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
export {
|
|
811
|
+
X402Error,
|
|
812
|
+
useX402Payment
|
|
813
|
+
};
|
|
814
|
+
//# sourceMappingURL=index.js.map
|