@relai-fi/x402 0.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/README.md ADDED
@@ -0,0 +1,314 @@
1
+ <h1 align="center">@relai-fi/x402</h1>
2
+
3
+ <p align="center">
4
+ <strong>Unified x402 payment SDK for Solana, Base, Avalanche, and SKALE Base.</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@relai-fi/x402"><img src="https://img.shields.io/npm/v/@relai-fi/x402.svg" alt="npm"></a>
9
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E=18-brightgreen.svg" alt="Node"></a>
10
+ <a href="https://relai.fi"><img src="https://img.shields.io/badge/Marketplace-relai.fi-blueviolet" alt="Marketplace"></a>
11
+ </p>
12
+
13
+ <p align="center">
14
+ <a href="https://relai.fi"><strong>Browse APIs →</strong></a>
15
+ </p>
16
+
17
+ ---
18
+
19
+ ## What is x402?
20
+
21
+ x402 is a protocol for HTTP-native micropayments. When a server returns HTTP `402 Payment Required`, it includes payment details in the response. The client signs a payment, retries the request, and the server settles the payment and returns the protected content.
22
+
23
+ This SDK handles the entire flow automatically — call `fetch()` and payments happen transparently.
24
+
25
+ ---
26
+
27
+ ## Why This SDK?
28
+
29
+ **Multi-chain.** Solana, Base, Avalanche, and SKALE Base with a single API. Connect your wallets and the SDK picks the right chain and signing method automatically.
30
+
31
+ **Zero gas fees.** The RelAI facilitator sponsors gas — users only pay for content (USDC).
32
+
33
+ **Auto-detects signing method.** EIP-3009 `transferWithAuthorization` for all EVM networks (Base, Avalanche, SKALE Base), native SPL transfer for Solana — all handled internally.
34
+
35
+ **Works out of the box.** Uses the [RelAI facilitator](https://facilitator.x402.fi) by default.
36
+
37
+ ---
38
+
39
+ ## Quick Start
40
+
41
+ ### Install
42
+
43
+ ```bash
44
+ npm install @relai-fi/x402
45
+ ```
46
+
47
+ ### Client (Browser / Node.js)
48
+
49
+ ```typescript
50
+ import { createX402Client } from '@relai-fi/x402/client';
51
+
52
+ const client = createX402Client({
53
+ wallets: {
54
+ solana: solanaWallet, // @solana/wallet-adapter compatible
55
+ evm: evmWallet, // wagmi/viem compatible
56
+ },
57
+ });
58
+
59
+ // 402 responses are handled automatically
60
+ const response = await client.fetch('https://api.example.com/protected');
61
+ const data = await response.json();
62
+ ```
63
+
64
+ ### React Hook
65
+
66
+ Works with [`@solana/wallet-adapter-react`](https://github.com/anza-xyz/wallet-adapter) and [`wagmi`](https://wagmi.sh/):
67
+
68
+ ```tsx
69
+ import { useRelaiPayment } from '@relai-fi/x402/react';
70
+ import { useWallet } from '@solana/wallet-adapter-react';
71
+ import { useAccount, useSignTypedData } from 'wagmi';
72
+
73
+ function PayButton() {
74
+ const solanaWallet = useWallet();
75
+ const { address } = useAccount();
76
+ const { signTypedDataAsync } = useSignTypedData();
77
+
78
+ const {
79
+ fetch,
80
+ isLoading,
81
+ status,
82
+ transactionUrl,
83
+ transactionNetworkLabel,
84
+ } = useRelaiPayment({
85
+ wallets: {
86
+ solana: solanaWallet,
87
+ evm: address ? { address, signTypedData: signTypedDataAsync } : undefined,
88
+ },
89
+ });
90
+
91
+ return (
92
+ <div>
93
+ <button onClick={() => fetch('/api/protected')} disabled={isLoading}>
94
+ {isLoading ? 'Paying...' : 'Access API'}
95
+ </button>
96
+ {transactionUrl && (
97
+ <a href={transactionUrl} target="_blank">
98
+ View on {transactionNetworkLabel}
99
+ </a>
100
+ )}
101
+ </div>
102
+ );
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Supported Networks
109
+
110
+ | Network | Identifier | CAIP-2 | Signing Method | USDC Contract |
111
+ |---------|-----------|--------|----------------|---------------|
112
+ | **Solana** | `solana` | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | SPL transfer + fee payer | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |
113
+ | **Base** | `base` | `eip155:8453` | EIP-3009 transferWithAuthorization | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
114
+ | **Avalanche** | `avalanche` | `eip155:43114` | EIP-3009 transferWithAuthorization | `0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E` |
115
+ | **SKALE Base** | `skale-base` | `eip155:1187947933` | EIP-3009 transferWithAuthorization | `0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20` |
116
+
117
+ All networks use **USDC** with 6 decimals. Gas fees are sponsored by the RelAI facilitator.
118
+
119
+ ---
120
+
121
+ ## Package Exports
122
+
123
+ ```typescript
124
+ // Client — browser & Node.js fetch wrapper with automatic 402 handling
125
+ import { createX402Client } from '@relai-fi/x402/client';
126
+
127
+ // React hook — state management + wallet integration
128
+ import { useRelaiPayment } from '@relai-fi/x402/react';
129
+
130
+ // Server — Express middleware for protecting endpoints
131
+ import Relai from '@relai-fi/x402/server';
132
+
133
+ // Utilities — payload conversion, unit helpers
134
+ import {
135
+ convertV1ToV2,
136
+ convertV2ToV1,
137
+ networkV1ToV2,
138
+ networkV2ToV1,
139
+ toAtomicUnits,
140
+ fromAtomicUnits,
141
+ } from '@relai-fi/x402/utils';
142
+
143
+ // Types & constants
144
+ import {
145
+ RELAI_NETWORKS,
146
+ CHAIN_IDS,
147
+ USDC_ADDRESSES,
148
+ NETWORK_CAIP2,
149
+ EXPLORER_TX_URL,
150
+ type RelaiNetwork,
151
+ type SolanaWallet,
152
+ type EvmWallet,
153
+ type WalletSet,
154
+ } from '@relai-fi/x402';
155
+ ```
156
+
157
+ ---
158
+
159
+ ## API Reference
160
+
161
+ ### `createX402Client(config)`
162
+
163
+ Creates a fetch wrapper that automatically handles 402 Payment Required responses.
164
+
165
+ | Option | Type | Default | Description |
166
+ |--------|------|---------|-------------|
167
+ | `wallets` | `{ solana?, evm? }` | `{}` | Wallet adapters for each chain |
168
+ | `facilitatorUrl` | `string` | RelAI facilitator | Custom facilitator endpoint |
169
+ | `preferredNetwork` | `RelaiNetwork` | — | Prefer this network when multiple `accepts` |
170
+ | `solanaRpcUrl` | `string` | `https://api.mainnet-beta.solana.com` | Solana RPC (use Helius/Quicknode for production) |
171
+ | `evmRpcUrls` | `Record<string, string>` | Built-in defaults | RPC URLs per network name |
172
+ | `maxAmountAtomic` | `string` | — | Safety cap on payment amount |
173
+ | `verbose` | `boolean` | `false` | Log payment flow to console |
174
+
175
+ **Wallet interfaces:**
176
+
177
+ ```typescript
178
+ // Solana — compatible with @solana/wallet-adapter-react useWallet()
179
+ interface SolanaWallet {
180
+ publicKey: { toString(): string } | null;
181
+ signTransaction: ((tx: unknown) => Promise<unknown>) | null;
182
+ }
183
+
184
+ // EVM — pass address + signTypedData from wagmi
185
+ interface EvmWallet {
186
+ address: string;
187
+ signTypedData: (params: {
188
+ domain: Record<string, unknown>;
189
+ types: Record<string, unknown[]>;
190
+ message: Record<string, unknown>;
191
+ primaryType: string;
192
+ }) => Promise<string>;
193
+ }
194
+ ```
195
+
196
+ ---
197
+
198
+ ### `useRelaiPayment(config)`
199
+
200
+ React hook wrapping `createX402Client` with state management.
201
+
202
+ **Config** — same as `createX402Client` (see above).
203
+
204
+ **Returns:**
205
+
206
+ | Property | Type | Description |
207
+ |----------|------|-------------|
208
+ | `fetch` | `(input, init?) => Promise<Response>` | Payment-aware fetch |
209
+ | `isLoading` | `boolean` | Payment in progress |
210
+ | `status` | `'idle' \| 'pending' \| 'success' \| 'error'` | Current state |
211
+ | `error` | `Error \| null` | Error details on failure |
212
+ | `transactionId` | `string \| null` | Tx hash/signature on success |
213
+ | `transactionNetwork` | `RelaiNetwork \| null` | Network used for payment |
214
+ | `transactionNetworkLabel` | `string \| null` | Human-readable label (e.g. "Base") |
215
+ | `transactionUrl` | `string \| null` | Block explorer link |
216
+ | `connectedChains` | `{ solana: boolean, evm: boolean }` | Which wallets are connected |
217
+ | `isConnected` | `boolean` | Any wallet connected |
218
+ | `reset` | `() => void` | Reset state to idle |
219
+
220
+ ---
221
+
222
+ ### Server SDK (Express)
223
+
224
+ ```typescript
225
+ import Relai from '@relai-fi/x402/server';
226
+
227
+ const relai = new Relai({
228
+ apiKey: process.env.RELAI_API_KEY,
229
+ network: 'solana', // or 'base', 'avalanche', 'skale-base'
230
+ });
231
+
232
+ // Protect any Express route with micropayments
233
+ app.get('/api/data', relai.protect({
234
+ payTo: 'YourWalletAddress',
235
+ price: 0.01, // $0.01 USDC
236
+ description: 'Premium data access',
237
+ }), (req, res) => {
238
+ res.json({ data: 'Protected content', payment: req.payment });
239
+ });
240
+ ```
241
+
242
+ ---
243
+
244
+ ## Utilities
245
+
246
+ ```typescript
247
+ import { toAtomicUnits, fromAtomicUnits } from '@relai-fi/x402/utils';
248
+
249
+ toAtomicUnits(0.05, 6); // '50000' ($0.05 USDC)
250
+ toAtomicUnits(1.50, 6); // '1500000' ($1.50 USDC)
251
+
252
+ fromAtomicUnits('50000', 6); // 0.05
253
+ fromAtomicUnits('1500000', 6); // 1.5
254
+ ```
255
+
256
+ ### Payload Conversion (v1 ↔ v2)
257
+
258
+ ```typescript
259
+ import { convertV1ToV2, convertV2ToV1, networkV1ToV2 } from '@relai-fi/x402/utils';
260
+
261
+ networkV1ToV2('solana'); // 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'
262
+ networkV1ToV2('base'); // 'eip155:8453'
263
+ networkV1ToV2('avalanche'); // 'eip155:43114'
264
+ networkV1ToV2('skale-base'); // 'eip155:1187947933'
265
+
266
+ const v2Payload = convertV1ToV2(v1Payload);
267
+ const v1Payload = convertV2ToV1(v2Payload);
268
+ ```
269
+
270
+ ---
271
+
272
+ ## How It Works
273
+
274
+ ```
275
+ Client Server Facilitator
276
+ | | |
277
+ |── GET /api/data ──────────>| |
278
+ |<── 402 Payment Required ───| |
279
+ | (accepts: network, amount, asset) |
280
+ | | |
281
+ | SDK signs payment | |
282
+ | (EIP-3009/SPL) | |
283
+ | | |
284
+ |── GET /api/data ──────────>| |
285
+ | X-PAYMENT: <signed> |── settle ─────────────────>|
286
+ | |<── tx hash ────────────────|
287
+ |<── 200 OK + data ─────────| |
288
+ | PAYMENT-RESPONSE: <tx> | |
289
+ ```
290
+
291
+ ---
292
+
293
+ ## Development
294
+
295
+ ```bash
296
+ npm run build # Build ESM + CJS bundles
297
+ npm run dev # Watch mode
298
+ npm run type-check # TypeScript checks
299
+ npm test # Run tests
300
+ ```
301
+
302
+ ---
303
+
304
+ ## License
305
+
306
+ MIT
307
+
308
+ ---
309
+
310
+ <p align="center">
311
+ <a href="https://facilitator.x402.fi">RelAI Facilitator</a> ·
312
+ <a href="https://relai.fi">Marketplace</a> ·
313
+ <a href="https://github.com/web3luka/relai-sdk">GitHub</a>
314
+ </p>
@@ -0,0 +1,393 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/client.ts
21
+ var client_exports = {};
22
+ __export(client_exports, {
23
+ createX402Client: () => createX402Client,
24
+ default: () => client_default
25
+ });
26
+ module.exports = __toCommonJS(client_exports);
27
+ var import_web3 = require("@solana/web3.js");
28
+ var import_spl_token = require("@solana/spl-token");
29
+
30
+ // src/types.ts
31
+ var RELAI_FACILITATOR_URL = "https://facilitator.x402.fi";
32
+ var NETWORK_CAIP2 = {
33
+ "solana": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
34
+ "base": "eip155:8453",
35
+ "avalanche": "eip155:43114",
36
+ "skale-base": "eip155:1187947933"
37
+ };
38
+ var CAIP2_TO_NETWORK = Object.fromEntries(
39
+ Object.entries(NETWORK_CAIP2).map(([k, v]) => [v, k])
40
+ );
41
+ var CHAIN_IDS = {
42
+ "base": 8453,
43
+ "avalanche": 43114,
44
+ "skale-base": 1187947933
45
+ };
46
+ var USDC_ADDRESSES = {
47
+ "solana": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
48
+ "base": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
49
+ "avalanche": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
50
+ "skale-base": "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20"
51
+ };
52
+ var SOLANA_MAINNET_NETWORK = NETWORK_CAIP2["solana"];
53
+ var BASE_MAINNET_NETWORK = NETWORK_CAIP2["base"];
54
+ var USDC_SOLANA = USDC_ADDRESSES["solana"];
55
+ var USDC_BASE = USDC_ADDRESSES["base"];
56
+ var RELAI_NETWORKS = ["solana", "base", "avalanche", "skale-base"];
57
+ function isSolana(network) {
58
+ return network === "solana" || network.startsWith("solana:");
59
+ }
60
+ function isEvm(network) {
61
+ return ["base", "avalanche", "skale-base"].includes(network) || network.startsWith("eip155:");
62
+ }
63
+ function normalizeNetwork(network) {
64
+ if (RELAI_NETWORKS.includes(network)) return network;
65
+ const fromCaip2 = CAIP2_TO_NETWORK[network];
66
+ if (fromCaip2) return fromCaip2;
67
+ if (network.startsWith("solana:")) return "solana";
68
+ if (network.startsWith("eip155:")) {
69
+ const chainId = parseInt(network.split(":")[1]);
70
+ const entry = Object.entries(CHAIN_IDS).find(([, id]) => id === chainId);
71
+ if (entry) return entry[0];
72
+ }
73
+ return null;
74
+ }
75
+
76
+ // src/client.ts
77
+ var PERMIT_NETWORKS = /* @__PURE__ */ new Set([]);
78
+ var DEFAULT_EVM_RPC_URLS = {
79
+ "skale-base": "https://skale-base.skalenodes.com/v1/base",
80
+ "base": "https://mainnet.base.org",
81
+ "avalanche": "https://api.avax.network/ext/bc/C/rpc"
82
+ };
83
+ function createX402Client(config) {
84
+ const {
85
+ wallets = {},
86
+ wallet: legacyWallet,
87
+ facilitatorUrl = RELAI_FACILITATOR_URL,
88
+ preferredNetwork,
89
+ solanaRpcUrl = "https://api.mainnet-beta.solana.com",
90
+ evmRpcUrls = {},
91
+ maxAmountAtomic,
92
+ verbose = false
93
+ } = config;
94
+ const log = verbose ? console.log.bind(console, "[relai-x402]") : () => {
95
+ };
96
+ const effectiveWallets = { ...wallets };
97
+ if (legacyWallet && !effectiveWallets.solana) {
98
+ effectiveWallets.solana = legacyWallet;
99
+ }
100
+ const hasSolanaWallet = Boolean(
101
+ effectiveWallets.solana?.publicKey && effectiveWallets.solana?.signTransaction
102
+ );
103
+ if (hasSolanaWallet) log("Solana wallet ready");
104
+ function selectAccept(accepts) {
105
+ if (preferredNetwork) {
106
+ const caip2 = NETWORK_CAIP2[preferredNetwork];
107
+ for (const a of accepts) {
108
+ const net = a.network || "";
109
+ if (net === preferredNetwork || net === caip2) {
110
+ const chain = isSolana(net) ? "solana" : "evm";
111
+ if (chain === "solana" && hasSolanaWallet || chain === "evm" && effectiveWallets.evm) {
112
+ return { accept: a, chain };
113
+ }
114
+ }
115
+ }
116
+ }
117
+ for (const a of accepts) {
118
+ const net = a.network || "";
119
+ if (isSolana(net) && hasSolanaWallet) return { accept: a, chain: "solana" };
120
+ if (isEvm(net) && effectiveWallets.evm) return { accept: a, chain: "evm" };
121
+ }
122
+ return null;
123
+ }
124
+ async function evmRpcCall(rpcUrl, to, data) {
125
+ const res = await fetch(rpcUrl, {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify({
129
+ jsonrpc: "2.0",
130
+ method: "eth_call",
131
+ params: [{ to, data }, "latest"],
132
+ id: 1
133
+ })
134
+ });
135
+ const json = await res.json();
136
+ if (json.error) throw new Error(`RPC error: ${json.error.message}`);
137
+ return json.result;
138
+ }
139
+ function getEvmRpcUrl(network) {
140
+ return evmRpcUrls[network] || DEFAULT_EVM_RPC_URLS[network] || "";
141
+ }
142
+ async function buildEvmPermitPayment(accept, requirements, url) {
143
+ const evmWallet = effectiveWallets.evm;
144
+ const extra = accept.extra || {};
145
+ const rawNetwork = accept.network || "";
146
+ const network = normalizeNetwork(rawNetwork);
147
+ const chainId = network ? CHAIN_IDS[network] : parseInt(rawNetwork.split(":")[1] || "8453");
148
+ const paymentAmount = accept.amount || accept.maxAmountRequired;
149
+ const spender = extra.feePayer || accept.payTo;
150
+ const usdcAddress = accept.asset;
151
+ const rpcUrl = getEvmRpcUrl(network || rawNetwork);
152
+ if (!rpcUrl) throw new Error(`[relai-x402] No EVM RPC URL for network ${network || rawNetwork}`);
153
+ log("Building EIP-2612 permit on chain", chainId);
154
+ const paddedAddress = evmWallet.address.toLowerCase().replace("0x", "").padStart(64, "0");
155
+ const nonceHex = await evmRpcCall(rpcUrl, usdcAddress, "0x7ecebe00" + paddedAddress);
156
+ const nonce = nonceHex ? parseInt(nonceHex, 16) : 0;
157
+ if (isNaN(nonce)) throw new Error(`[relai-x402] Failed to read permit nonce from ${usdcAddress} on ${rpcUrl}`);
158
+ log(" Permit nonce:", nonce);
159
+ const nameHex = await evmRpcCall(rpcUrl, usdcAddress, "0x06fdde03");
160
+ let tokenName = "USD Coin";
161
+ try {
162
+ const offset = parseInt(nameHex.slice(2, 66), 16) * 2;
163
+ const length = parseInt(nameHex.slice(2 + offset, 2 + offset + 64), 16);
164
+ const hex = nameHex.slice(2 + offset + 64, 2 + offset + 64 + length * 2);
165
+ tokenName = decodeURIComponent(hex.replace(/[0-9a-f]{2}/g, "%$&"));
166
+ } catch {
167
+ tokenName = extra.name || "USD Coin";
168
+ }
169
+ log(" Token name:", tokenName);
170
+ const deadline = Math.floor(Date.now() / 1e3) + 600;
171
+ const domain = {
172
+ name: tokenName,
173
+ version: extra.version || "2",
174
+ chainId,
175
+ verifyingContract: usdcAddress
176
+ };
177
+ const types = {
178
+ Permit: [
179
+ { name: "owner", type: "address" },
180
+ { name: "spender", type: "address" },
181
+ { name: "value", type: "uint256" },
182
+ { name: "nonce", type: "uint256" },
183
+ { name: "deadline", type: "uint256" }
184
+ ]
185
+ };
186
+ const message = {
187
+ owner: evmWallet.address,
188
+ spender,
189
+ value: paymentAmount,
190
+ nonce: String(nonce),
191
+ deadline: String(deadline)
192
+ };
193
+ log("Signing EIP-2612 permit:", message);
194
+ const signature = await evmWallet.signTypedData({
195
+ domain,
196
+ types,
197
+ message,
198
+ primaryType: "Permit"
199
+ });
200
+ const sigHex = signature.replace("0x", "");
201
+ const r = "0x" + sigHex.slice(0, 64);
202
+ const s = "0x" + sigHex.slice(64, 128);
203
+ const v = parseInt(sigHex.slice(128, 130), 16);
204
+ log(" Permit signed: v=%d r=%s s=%s", v, r, s);
205
+ const paymentPayload = {
206
+ x402Version: 2,
207
+ scheme: "exact",
208
+ network: network || rawNetwork,
209
+ payload: {
210
+ userAddress: evmWallet.address,
211
+ permit: { deadline: String(deadline), v, r, s },
212
+ amount: paymentAmount
213
+ }
214
+ };
215
+ return btoa(JSON.stringify(paymentPayload));
216
+ }
217
+ async function buildEvmPayment(accept, requirements, url) {
218
+ const evmWallet = effectiveWallets.evm;
219
+ const extra = accept.extra || {};
220
+ const rawNetwork = accept.network || "";
221
+ const network = normalizeNetwork(rawNetwork);
222
+ const chainId = network ? CHAIN_IDS[network] : parseInt(rawNetwork.split(":")[1] || "8453");
223
+ const paymentAmount = accept.amount || accept.maxAmountRequired;
224
+ const domain = {
225
+ name: extra.name || "USD Coin",
226
+ version: extra.version || "2",
227
+ chainId,
228
+ verifyingContract: accept.asset
229
+ };
230
+ const validAfter = 0;
231
+ const validBefore = Math.floor(Date.now() / 1e3) + 3600;
232
+ const nonce = "0x" + [...crypto.getRandomValues(new Uint8Array(32))].map((b) => b.toString(16).padStart(2, "0")).join("");
233
+ const types = {
234
+ TransferWithAuthorization: [
235
+ { name: "from", type: "address" },
236
+ { name: "to", type: "address" },
237
+ { name: "value", type: "uint256" },
238
+ { name: "validAfter", type: "uint256" },
239
+ { name: "validBefore", type: "uint256" },
240
+ { name: "nonce", type: "bytes32" }
241
+ ]
242
+ };
243
+ const spender = extra.feePayer || accept.payTo;
244
+ const message = {
245
+ from: evmWallet.address,
246
+ to: spender,
247
+ value: paymentAmount,
248
+ validAfter: String(validAfter),
249
+ validBefore: String(validBefore),
250
+ nonce
251
+ };
252
+ log("Signing EIP-3009 transferWithAuthorization on chain", chainId);
253
+ const signature = await evmWallet.signTypedData({
254
+ domain,
255
+ types,
256
+ message,
257
+ primaryType: "TransferWithAuthorization"
258
+ });
259
+ const paymentPayload = {
260
+ x402Version: 2,
261
+ resource: requirements.resource || { url },
262
+ accepted: accept,
263
+ payload: {
264
+ authorization: message,
265
+ signature
266
+ },
267
+ facilitatorUrl
268
+ };
269
+ return btoa(JSON.stringify(paymentPayload));
270
+ }
271
+ async function buildSolanaPayment(accept, requirements, url) {
272
+ const solWallet = effectiveWallets.solana;
273
+ const extra = accept.extra || {};
274
+ if (!extra.feePayer) {
275
+ throw new Error("[relai-x402] Missing feePayer in Solana payment requirements");
276
+ }
277
+ const connection = new import_web3.Connection(solanaRpcUrl, "confirmed");
278
+ const userPubkey = new import_web3.PublicKey(solWallet.publicKey.toString());
279
+ const merchantPubkey = new import_web3.PublicKey(accept.payTo);
280
+ const feePayerPubkey = new import_web3.PublicKey(extra.feePayer);
281
+ const mintPubkey = new import_web3.PublicKey(accept.asset);
282
+ const paymentAmount = BigInt(accept.amount || accept.maxAmountRequired);
283
+ log("Building Solana SPL transfer");
284
+ log(" User:", userPubkey.toBase58());
285
+ log(" Merchant:", merchantPubkey.toBase58());
286
+ log(" FeePayer:", feePayerPubkey.toBase58());
287
+ log(" Mint:", mintPubkey.toBase58());
288
+ log(" Amount:", paymentAmount.toString());
289
+ const mintInfo = await (0, import_spl_token.getMint)(connection, mintPubkey);
290
+ const programId = mintInfo.address.equals(mintPubkey) ? mintInfo.owner?.toBase58?.() === import_spl_token.TOKEN_2022_PROGRAM_ID.toBase58() ? import_spl_token.TOKEN_2022_PROGRAM_ID : import_spl_token.TOKEN_PROGRAM_ID : import_spl_token.TOKEN_PROGRAM_ID;
291
+ const sourceAta = await (0, import_spl_token.getAssociatedTokenAddress)(
292
+ mintPubkey,
293
+ userPubkey,
294
+ false,
295
+ programId
296
+ );
297
+ const destinationAta = await (0, import_spl_token.getAssociatedTokenAddress)(
298
+ mintPubkey,
299
+ merchantPubkey,
300
+ true,
301
+ programId
302
+ );
303
+ log(" Source ATA:", sourceAta.toBase58());
304
+ log(" Dest ATA:", destinationAta.toBase58());
305
+ const transferIx = (0, import_spl_token.createTransferCheckedInstruction)(
306
+ sourceAta,
307
+ mintPubkey,
308
+ destinationAta,
309
+ userPubkey,
310
+ paymentAmount,
311
+ mintInfo.decimals,
312
+ [],
313
+ programId
314
+ );
315
+ const { blockhash } = await connection.getLatestBlockhash("confirmed");
316
+ const message = new import_web3.TransactionMessage({
317
+ payerKey: feePayerPubkey,
318
+ recentBlockhash: blockhash,
319
+ instructions: [transferIx]
320
+ }).compileToV0Message();
321
+ const transaction = new import_web3.VersionedTransaction(message);
322
+ const signedTx = await solWallet.signTransaction(transaction);
323
+ log("Transaction signed by user");
324
+ const serializedTx = Buffer.from(signedTx.serialize()).toString("base64");
325
+ const paymentPayload = {
326
+ x402Version: 2,
327
+ resource: requirements.resource || { url },
328
+ accepted: accept,
329
+ payload: {
330
+ transaction: serializedTx
331
+ }
332
+ };
333
+ return btoa(JSON.stringify(paymentPayload));
334
+ }
335
+ async function x402Fetch(input, init) {
336
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
337
+ log("Request:", url);
338
+ const response = await fetch(input, init);
339
+ if (response.status !== 402) return response;
340
+ log("Got 402 Payment Required");
341
+ let requirements;
342
+ try {
343
+ requirements = await response.clone().json();
344
+ } catch {
345
+ throw new Error("[relai-x402] Failed to parse 402 response body");
346
+ }
347
+ const accepts = requirements.accepts || [];
348
+ if (!accepts.length) throw new Error("[relai-x402] No payment options in 402 response");
349
+ const selected = selectAccept(accepts);
350
+ if (!selected) {
351
+ const networks = accepts.map((a) => a.network).join(", ");
352
+ throw new Error(`[relai-x402] No wallet available for networks: ${networks}`);
353
+ }
354
+ const { accept, chain } = selected;
355
+ const amount = accept.amount || accept.maxAmountRequired;
356
+ log(`Selected: ${chain} / ${accept.network} / amount=${amount}`);
357
+ if (maxAmountAtomic && BigInt(amount) > BigInt(maxAmountAtomic)) {
358
+ throw new Error(`[relai-x402] Amount ${amount} exceeds max ${maxAmountAtomic}`);
359
+ }
360
+ if (chain === "solana" && hasSolanaWallet) {
361
+ const paymentHeader = await buildSolanaPayment(accept, requirements, url);
362
+ log("Retrying with X-PAYMENT header (Solana)");
363
+ return fetch(input, {
364
+ ...init,
365
+ headers: {
366
+ ...init?.headers || {},
367
+ "X-PAYMENT": paymentHeader
368
+ }
369
+ });
370
+ }
371
+ if (chain === "evm") {
372
+ const evmNetwork = normalizeNetwork(accept.network || "");
373
+ const usePermit = evmNetwork && PERMIT_NETWORKS.has(evmNetwork);
374
+ const paymentHeader = usePermit ? await buildEvmPermitPayment(accept, requirements, url) : await buildEvmPayment(accept, requirements, url);
375
+ log("Retrying with X-PAYMENT header");
376
+ return fetch(input, {
377
+ ...init,
378
+ headers: {
379
+ ...init?.headers || {},
380
+ "X-PAYMENT": paymentHeader
381
+ }
382
+ });
383
+ }
384
+ throw new Error("[relai-x402] Unexpected state \u2014 no payment handler matched");
385
+ }
386
+ return { fetch: x402Fetch };
387
+ }
388
+ var client_default = createX402Client;
389
+ // Annotate the CommonJS export names for ESM import in node:
390
+ 0 && (module.exports = {
391
+ createX402Client
392
+ });
393
+ //# sourceMappingURL=client.cjs.map