@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 +314 -0
- package/dist/client.cjs +393 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +27 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.js +379 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +720 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +664 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +519 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +80 -0
- package/dist/react/index.d.ts +80 -0
- package/dist/react/index.js +503 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server.cjs +171 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +18 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +136 -0
- package/dist/server.js.map +1 -0
- package/dist/types-DGRfrYd3.d.cts +124 -0
- package/dist/types-DGRfrYd3.d.ts +124 -0
- package/dist/utils/index.cjs +192 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +136 -0
- package/dist/utils/index.d.ts +136 -0
- package/dist/utils/index.js +152 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +92 -0
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>
|
package/dist/client.cjs
ADDED
|
@@ -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
|