x402-rails 0.2.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +166 -29
- data/lib/x402/chains.rb +108 -16
- data/lib/x402/configuration.rb +62 -4
- data/lib/x402/payment_payload.rb +82 -10
- data/lib/x402/payment_requirement.rb +34 -13
- data/lib/x402/payment_validator.rb +18 -9
- data/lib/x402/rails/controller_extensions.rb +151 -133
- data/lib/x402/rails/generators/templates/x402_initializer.rb +2 -13
- data/lib/x402/rails/version.rb +1 -1
- data/lib/x402/rails.rb +1 -0
- data/lib/x402/requirement_generator.rb +74 -31
- data/lib/x402/versions/base.rb +39 -0
- data/lib/x402/versions/v1.rb +54 -0
- data/lib/x402/versions/v2.rb +61 -0
- data/lib/x402/versions.rb +17 -0
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b70874283a564f69295e16df7dcb11d4764480c62151f130335ee3d116d62f5
|
|
4
|
+
data.tar.gz: e4462c35ac33efc1993f166fa407b1e06f1b1fa35e3ba6547afce9031c07d3c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb7d7df8bda864f433e062f41cc1e956c6a979446e02bd3bb88ef5bccc2ac42e06a2933a5cafe7b7475b53665d360b5bc034b80e4c9c9ff10b668df1f61a106d
|
|
7
|
+
data.tar.gz: ea4523288ae8c5242fb03c90f1bc83f41779d46c170354cf2a3ccfdbaaf34da3f6656a03ab4b47d01f0d11e01e4adf1da4a52b405ddf25aa12f91270f11a6eaf
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.0.0] - 2026-01-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Protocol v2 support** - Full x402 protocol v2 compliance with CAIP-2 network identifiers and updated headers
|
|
9
|
+
- **v2 `PAYMENT-REQUIRED` header** - 402 responses include base64-encoded PaymentRequired in header per v2 HTTP transport spec
|
|
10
|
+
- **v2 PaymentPayload format** - `scheme` and `network` nested inside `accepted` object per v2 spec
|
|
11
|
+
- **v2 PaymentRequired format** - `resource` info at top level, `amount` field (not `maxAmountRequired`), `extensions` object
|
|
12
|
+
- **Multi-chain accept** - `config.accept(chain:, currency:)` allows accepting payments on multiple chains simultaneously
|
|
13
|
+
- **Per-endpoint version override** - `x402_paywall(amount:, version:)` to use v1 or v2 per endpoint
|
|
14
|
+
- **Custom chain registration** - `config.register_chain(name:, chain_id:, standard:)` for custom EVM chains
|
|
15
|
+
- **Custom token registration** - `config.register_token(chain:, symbol:, address:, decimals:, name:)` for custom tokens
|
|
16
|
+
- **CAIP-2 support** - `to_caip2()` and `from_caip2()` for network identifier conversion
|
|
17
|
+
- **Per-chain fee payer** - Configure via `X402_SOLANA_DEVNET_FEE_PAYER`, `X402_SOLANA_FEE_PAYER` env vars
|
|
18
|
+
- **Dynamic HTML paywall** - Detects decimals and asset symbol from chain configuration
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Default protocol version is now v2
|
|
22
|
+
- v2 responses use CAIP-2 network format (e.g., `eip155:84532` instead of `base-sepolia`)
|
|
23
|
+
- v2 402 responses include full PaymentRequired in both header and body for debugging
|
|
24
|
+
- v1 requirements include `resource`, `description`, `mimeType` in each accept; v2 places these at top level
|
|
25
|
+
|
|
26
|
+
## [0.2.1] - Previous Release
|
|
27
|
+
|
|
28
|
+
- Initial stable release with v1 protocol support
|
|
29
|
+
- Custom chain and token registration
|
|
30
|
+
- Optimistic and non-optimistic settlement modes
|
data/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# x402-rails
|
|
2
2
|
|
|
3
|
+
## Now supporting x402 v2!
|
|
4
|
+
|
|
5
|
+
> **⚠️ Note:** This gem now defaults to x402 protocol **v2**. If you need v1 compatibility, set `config.version = 1` in your initializer. See [Protocol Versions](#protocol-versions) for details on the differences.
|
|
6
|
+
|
|
3
7
|

|
|
4
8
|
|
|
5
9
|
Accept instant blockchain micropayments in your Rails applications using the [x402 payment protocol](https://www.x402.org/).
|
|
@@ -59,6 +63,7 @@ Use `x402_paywall` in any controller action:
|
|
|
59
63
|
class ApiController < ApplicationController
|
|
60
64
|
def weather
|
|
61
65
|
x402_paywall(amount: 0.001) # $0.001 in USD
|
|
66
|
+
return if performed?
|
|
62
67
|
|
|
63
68
|
render json: {
|
|
64
69
|
temperature: 72,
|
|
@@ -79,6 +84,7 @@ Call `x402_paywall` in any action:
|
|
|
79
84
|
```ruby
|
|
80
85
|
def show
|
|
81
86
|
x402_paywall(amount: 0.01)
|
|
87
|
+
return if performed?
|
|
82
88
|
# Action continues after payment verified
|
|
83
89
|
render json: @data
|
|
84
90
|
end
|
|
@@ -101,6 +107,7 @@ class PremiumController < ApplicationController
|
|
|
101
107
|
|
|
102
108
|
def require_payment
|
|
103
109
|
x402_paywall(amount: 0.001, chain: "base")
|
|
110
|
+
return if performed?
|
|
104
111
|
end
|
|
105
112
|
end
|
|
106
113
|
```
|
|
@@ -112,11 +119,13 @@ Different prices for different actions:
|
|
|
112
119
|
```ruby
|
|
113
120
|
def basic_data
|
|
114
121
|
x402_paywall(amount: 0.001)
|
|
122
|
+
return if performed?
|
|
115
123
|
render json: basic_info
|
|
116
124
|
end
|
|
117
125
|
|
|
118
126
|
def premium_data
|
|
119
127
|
x402_paywall(amount: 0.01)
|
|
128
|
+
return if performed?
|
|
120
129
|
render json: premium_info
|
|
121
130
|
end
|
|
122
131
|
```
|
|
@@ -159,48 +168,180 @@ end
|
|
|
159
168
|
| `chain` | No | `"base-sepolia"` | Blockchain network to use (`base-sepolia`, `base`, `avalanche-fuji`, `avalanche`) |
|
|
160
169
|
| `currency` | No | `"USDC"` | Payment token symbol (currently only USDC supported) |
|
|
161
170
|
| `optimistic` | No | `true` | Settlement mode (see Optimistic vs Non-Optimistic Mode below) |
|
|
162
|
-
| `
|
|
171
|
+
| `version` | No | `2` | Protocol version (1 or 2). See Protocol Versions section |
|
|
172
|
+
|
|
173
|
+
### Custom Chains and Tokens
|
|
174
|
+
|
|
175
|
+
You can register custom EVM chains and tokens beyond the built-in options.
|
|
163
176
|
|
|
164
|
-
|
|
177
|
+
#### Register a Custom Chain
|
|
165
178
|
|
|
166
|
-
|
|
179
|
+
Add support for any EVM-compatible chain:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
X402.configure do |config|
|
|
183
|
+
config.wallet_address = ENV['X402_WALLET_ADDRESS']
|
|
167
184
|
|
|
168
|
-
|
|
185
|
+
# Register Polygon mainnet
|
|
186
|
+
config.register_chain(
|
|
187
|
+
name: "polygon",
|
|
188
|
+
chain_id: 137,
|
|
189
|
+
standard: "eip155"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Register the token for that chain
|
|
193
|
+
config.register_token(
|
|
194
|
+
chain: "polygon",
|
|
195
|
+
symbol: "USDC",
|
|
196
|
+
address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
|
|
197
|
+
decimals: 6,
|
|
198
|
+
name: "USD Coin",
|
|
199
|
+
version: "2"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
config.chain = "polygon"
|
|
203
|
+
config.currency = "USDC"
|
|
204
|
+
end
|
|
205
|
+
```
|
|
169
206
|
|
|
170
|
-
|
|
171
|
-
2. Per-chain environment variables
|
|
172
|
-
3. Built-in default RPC URLs
|
|
207
|
+
#### Register a Custom Token on a Built-in Chain
|
|
173
208
|
|
|
174
|
-
|
|
209
|
+
> **⚠️ Note:** The Facilitator used **must support** the specified chain and token to ensure proper functionality.
|
|
175
210
|
|
|
176
|
-
|
|
211
|
+
Accept different tokens on existing chains:
|
|
177
212
|
|
|
178
213
|
```ruby
|
|
179
214
|
X402.configure do |config|
|
|
180
215
|
config.wallet_address = ENV['X402_WALLET_ADDRESS']
|
|
181
216
|
|
|
182
|
-
#
|
|
183
|
-
config.
|
|
184
|
-
|
|
185
|
-
|
|
217
|
+
# Accept WETH on Base instead of USDC
|
|
218
|
+
config.register_token(
|
|
219
|
+
chain: "base",
|
|
220
|
+
symbol: "WETH",
|
|
221
|
+
address: "0x4200000000000000000000000000000000000006",
|
|
222
|
+
decimals: 18,
|
|
223
|
+
name: "Wrapped Ether",
|
|
224
|
+
version: "1"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
config.chain = "base"
|
|
228
|
+
config.currency = "WETH"
|
|
186
229
|
end
|
|
187
230
|
```
|
|
188
231
|
|
|
189
|
-
####
|
|
232
|
+
#### Token Registration Parameters
|
|
190
233
|
|
|
191
|
-
|
|
234
|
+
| Parameter | Required | Description |
|
|
235
|
+
| ---------- | -------- | ---------------------------------------------- |
|
|
236
|
+
| `chain` | Yes | Chain name (built-in or custom registered) |
|
|
237
|
+
| `symbol` | Yes | Token symbol (e.g., "USDC", "WETH") |
|
|
238
|
+
| `address` | Yes | Token contract address |
|
|
239
|
+
| `decimals` | Yes | Token decimals (e.g., 6 for USDC, 18 for WETH) |
|
|
240
|
+
| `name` | Yes | Token name for EIP-712 domain |
|
|
241
|
+
| `version` | No | EIP-712 version (default: "1") |
|
|
192
242
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
243
|
+
**Note:** Custom chains and tokens are only supported for EVM (eip155) networks. Solana chains use a different implementation.
|
|
244
|
+
|
|
245
|
+
### Accept Multiple Payment Options
|
|
246
|
+
|
|
247
|
+
Allow clients to pay on any of several supported chains by using `config.accept()`:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
X402.configure do |config|
|
|
251
|
+
config.wallet_address = ENV['X402_WALLET_ADDRESS']
|
|
252
|
+
|
|
253
|
+
# Register a custom chain
|
|
254
|
+
config.register_chain(name: "polygon-amoy", chain_id: 80002, standard: "eip155")
|
|
255
|
+
config.register_token(
|
|
256
|
+
chain: "polygon-amoy",
|
|
257
|
+
symbol: "USDC",
|
|
258
|
+
address: "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582",
|
|
259
|
+
decimals: 6,
|
|
260
|
+
name: "USD Coin",
|
|
261
|
+
version: "2"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Accept payments on multiple chains
|
|
265
|
+
config.accept(chain: "base-sepolia", currency: "USDC")
|
|
266
|
+
config.accept(chain: "polygon-amoy", currency: "USDC")
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
When `config.accept()` is used, the 402 response will include all accepted payment options:
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{
|
|
274
|
+
"accepts": [
|
|
275
|
+
{ "network": "eip155:84532", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", ... },
|
|
276
|
+
{ "network": "eip155:80002", "asset": "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582", ... }
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Clients can then choose which chain to pay on based on their preferences or available funds.
|
|
282
|
+
|
|
283
|
+
**Per-accept wallet addresses:** You can specify different recipient addresses per chain:
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
config.accept(chain: "base-sepolia", currency: "USDC", wallet_address: "0xWallet1")
|
|
287
|
+
config.accept(chain: "polygon-amoy", currency: "USDC", wallet_address: "0xWallet2")
|
|
199
288
|
```
|
|
200
289
|
|
|
201
|
-
|
|
290
|
+
**Fallback behavior:** If no `config.accept()` calls are made, the default `config.chain` and `config.currency` are used.
|
|
202
291
|
|
|
203
|
-
|
|
292
|
+
## Protocol Versions
|
|
293
|
+
|
|
294
|
+
x402-rails supports both v1 and v2 of the x402 protocol. **v2 is the default**.
|
|
295
|
+
|
|
296
|
+
### Key Differences
|
|
297
|
+
|
|
298
|
+
| Feature | v1 (Legacy) | v2 (Default) |
|
|
299
|
+
| --------------- | ----------------------------- | -------------------------------- |
|
|
300
|
+
| Network format | Simple names (`base-sepolia`) | CAIP-2 (`eip155:84532`) |
|
|
301
|
+
| Payment header | `X-PAYMENT` | `PAYMENT-SIGNATURE` |
|
|
302
|
+
| Response header | `X-PAYMENT-RESPONSE` | `PAYMENT-RESPONSE` |
|
|
303
|
+
| Requirements | Body only | `PAYMENT-REQUIRED` header + body |
|
|
304
|
+
| Amount field | `maxAmountRequired` | `amount` |
|
|
305
|
+
|
|
306
|
+
### v2 (Default)
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
X402.configure do |config|
|
|
310
|
+
config.wallet_address = ENV['X402_WALLET_ADDRESS']
|
|
311
|
+
config.version = 2 # Default, can be omitted
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
v2 uses CAIP-2 network identifiers (`eip155:84532`) and the `PAYMENT-SIGNATURE` header. Payment requirements are sent in both the `PAYMENT-REQUIRED` header (base64-encoded) and the response body (JSON).
|
|
316
|
+
|
|
317
|
+
### v1 (Legacy)
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
X402.configure do |config|
|
|
321
|
+
config.wallet_address = ENV['X402_WALLET_ADDRESS']
|
|
322
|
+
config.version = 1
|
|
323
|
+
end
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
v1 uses simple network names (`base-sepolia`) and the `X-PAYMENT` header. Payment requirements are sent only in the response body.
|
|
327
|
+
|
|
328
|
+
### Per-Endpoint Version
|
|
329
|
+
|
|
330
|
+
Override the version for specific endpoints:
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
def premium_v2
|
|
334
|
+
x402_paywall(amount: 0.001, version: 2)
|
|
335
|
+
return if performed?
|
|
336
|
+
render json: { data: "v2 endpoint" }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def legacy_v1
|
|
340
|
+
x402_paywall(amount: 0.001, version: 1)
|
|
341
|
+
return if performed?
|
|
342
|
+
render json: { data: "v1 endpoint" }
|
|
343
|
+
end
|
|
344
|
+
```
|
|
204
345
|
|
|
205
346
|
## Environment Variables
|
|
206
347
|
|
|
@@ -215,12 +356,6 @@ X402_FACILITATOR_URL=https://x402.org/facilitator
|
|
|
215
356
|
X402_CHAIN=base-sepolia
|
|
216
357
|
X402_CURRENCY=USDC
|
|
217
358
|
X402_OPTIMISTIC=true # "true" or "false"
|
|
218
|
-
|
|
219
|
-
# Custom RPC URLs (optional, per-chain overrides)
|
|
220
|
-
X402_BASE_RPC_URL=https://your-base-rpc.quiknode.pro/your-key
|
|
221
|
-
X402_BASE_SEPOLIA_RPC_URL=https://your-base-speoliarpc.quiknode.pro/your-key
|
|
222
|
-
X402_AVALANCHE_RPC_URL=https://your-avalanche.quiknode.pro/your-key
|
|
223
|
-
X402_AVALANCHE_FUJI_RPC_URL=https://your-fuji-rpc.quiknode.pro/your-key
|
|
224
359
|
```
|
|
225
360
|
|
|
226
361
|
## Examples
|
|
@@ -231,11 +366,13 @@ X402_AVALANCHE_FUJI_RPC_URL=https://your-fuji-rpc.quiknode.pro/your-key
|
|
|
231
366
|
class WeatherController < ApplicationController
|
|
232
367
|
def current
|
|
233
368
|
x402_paywall(amount: 0.001)
|
|
369
|
+
return if performed?
|
|
234
370
|
render json: { temp: 72, condition: "sunny" }
|
|
235
371
|
end
|
|
236
372
|
|
|
237
373
|
def forecast
|
|
238
374
|
x402_paywall(amount: 0.01)
|
|
375
|
+
return if performed?
|
|
239
376
|
render json: { forecast: [...] }
|
|
240
377
|
end
|
|
241
378
|
end
|
data/lib/x402/chains.rb
CHANGED
|
@@ -5,27 +5,35 @@ module X402
|
|
|
5
5
|
CHAINS = {
|
|
6
6
|
"base-sepolia" => {
|
|
7
7
|
chain_id: 84532,
|
|
8
|
-
rpc_url: "https://clean-snowy-hexagon.base-sepolia.quiknode.pro",
|
|
9
8
|
usdc_address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
10
9
|
explorer_url: "https://sepolia.basescan.org"
|
|
11
10
|
},
|
|
12
11
|
"base" => {
|
|
13
12
|
chain_id: 8453,
|
|
14
|
-
rpc_url: "https://snowy-compatible-ensemble.base-mainnet.quiknode.pro",
|
|
15
13
|
usdc_address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
16
14
|
explorer_url: "https://basescan.org"
|
|
17
15
|
},
|
|
18
16
|
"avalanche-fuji" => {
|
|
19
17
|
chain_id: 43113,
|
|
20
|
-
rpc_url: "https://muddy-sly-field.avalanche-testnet.quiknode.pro/ext/bc/C/rpc",
|
|
21
18
|
usdc_address: "0x5425890298aed601595a70AB815c96711a31Bc65",
|
|
22
19
|
explorer_url: "https://testnet.snowtrace.io"
|
|
23
20
|
},
|
|
24
21
|
"avalanche" => {
|
|
25
22
|
chain_id: 43114,
|
|
26
|
-
rpc_url: "https://floral-patient-panorama.avalanche-mainnet.quiknode.pro/ext/bc/C/rpc",
|
|
27
23
|
usdc_address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
|
|
28
24
|
explorer_url: "https://snowtrace.io"
|
|
25
|
+
},
|
|
26
|
+
"solana-devnet" => {
|
|
27
|
+
chain_id: 103,
|
|
28
|
+
usdc_address: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
29
|
+
explorer_url: "https://explorer.solana.com/?cluster=devnet",
|
|
30
|
+
fee_payer: "CKPKJWNdJEqa81x7CkZ14BVPiY6y16Sxs7owznqtWYp5"
|
|
31
|
+
},
|
|
32
|
+
"solana" => {
|
|
33
|
+
chain_id: 101,
|
|
34
|
+
usdc_address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
35
|
+
explorer_url: "https://explorer.solana.com",
|
|
36
|
+
fee_payer: "CKPKJWNdJEqa81x7CkZ14BVPiY6y16Sxs7owznqtWYp5"
|
|
29
37
|
}
|
|
30
38
|
}.freeze
|
|
31
39
|
|
|
@@ -54,12 +62,38 @@ module X402
|
|
|
54
62
|
decimals: 6,
|
|
55
63
|
name: "USDC", # Mainnet uses "USDC"
|
|
56
64
|
version: "2"
|
|
65
|
+
},
|
|
66
|
+
"solana-devnet" => {
|
|
67
|
+
symbol: "USDC",
|
|
68
|
+
decimals: 6,
|
|
69
|
+
name: "USDC",
|
|
70
|
+
version: nil
|
|
71
|
+
},
|
|
72
|
+
"solana" => {
|
|
73
|
+
symbol: "USDC",
|
|
74
|
+
decimals: 6,
|
|
75
|
+
name: "USD Coin",
|
|
76
|
+
version: nil
|
|
57
77
|
}
|
|
58
78
|
}.freeze
|
|
59
79
|
|
|
80
|
+
CAIP2_MAPPING = {
|
|
81
|
+
"base-sepolia" => "eip155:84532",
|
|
82
|
+
"base" => "eip155:8453",
|
|
83
|
+
"avalanche-fuji" => "eip155:43113",
|
|
84
|
+
"avalanche" => "eip155:43114",
|
|
85
|
+
"solana-devnet" => "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
86
|
+
"solana" => "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
|
|
87
|
+
}.freeze
|
|
88
|
+
|
|
89
|
+
REVERSE_CAIP2_MAPPING = CAIP2_MAPPING.invert.freeze
|
|
90
|
+
|
|
60
91
|
class << self
|
|
61
92
|
def chain_config(chain_name)
|
|
62
|
-
|
|
93
|
+
custom = X402.configuration.chain_config(chain_name)
|
|
94
|
+
return custom if custom
|
|
95
|
+
|
|
96
|
+
CHAINS[chain_name] || raise(ConfigurationError, "Unsupported chain: #{chain_name}. Register with config.register_chain()")
|
|
63
97
|
end
|
|
64
98
|
|
|
65
99
|
def currency_config_for_chain(chain_name)
|
|
@@ -67,31 +101,89 @@ module X402
|
|
|
67
101
|
end
|
|
68
102
|
|
|
69
103
|
def supported_chains
|
|
70
|
-
CHAINS.keys
|
|
104
|
+
CHAINS.keys + X402.configuration.custom_chains.keys
|
|
71
105
|
end
|
|
72
106
|
|
|
73
107
|
def usdc_address_for(chain_name)
|
|
74
|
-
|
|
108
|
+
builtin = CHAINS[chain_name]
|
|
109
|
+
builtin ? builtin[:usdc_address] : nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def asset_address_for(chain_name, symbol = nil)
|
|
113
|
+
symbol ||= X402.configuration.currency
|
|
114
|
+
|
|
115
|
+
custom = X402.configuration.token_config(chain_name, symbol)
|
|
116
|
+
return custom[:address] if custom
|
|
117
|
+
|
|
118
|
+
if symbol.upcase == "USDC"
|
|
119
|
+
builtin = CHAINS[chain_name]
|
|
120
|
+
return builtin[:usdc_address] if builtin && builtin[:usdc_address]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
raise ConfigurationError, "Unknown token #{symbol} for chain #{chain_name}. Register with config.register_token()"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def token_config_for(chain_name, symbol = nil)
|
|
127
|
+
symbol ||= X402.configuration.currency
|
|
128
|
+
|
|
129
|
+
custom = X402.configuration.token_config(chain_name, symbol)
|
|
130
|
+
return custom if custom
|
|
131
|
+
|
|
132
|
+
if symbol.upcase == "USDC" && CURRENCY_BY_CHAIN[chain_name]
|
|
133
|
+
currency_config_for_chain(chain_name)
|
|
134
|
+
else
|
|
135
|
+
raise ConfigurationError, "Unknown token #{symbol} for chain #{chain_name}. Register with config.register_token()"
|
|
136
|
+
end
|
|
75
137
|
end
|
|
76
138
|
|
|
77
139
|
def currency_decimals_for_chain(chain_name)
|
|
78
140
|
currency_config_for_chain(chain_name)[:decimals]
|
|
79
141
|
end
|
|
80
142
|
|
|
81
|
-
def
|
|
82
|
-
# Priority: 1) Programmatic config, 2) ENV variable, 3) Default from CHAINS
|
|
143
|
+
def fee_payer_for(chain_name)
|
|
144
|
+
# Priority: 1) Programmatic config, 2) Per-chain ENV variable, 3) Generic ENV variable, 4) Default from CHAINS
|
|
83
145
|
config = X402.configuration
|
|
84
146
|
|
|
85
147
|
# Check programmatic configuration
|
|
86
|
-
return config.
|
|
148
|
+
return config.fee_payer if config.fee_payer && !config.fee_payer.empty?
|
|
149
|
+
|
|
150
|
+
# Check per-chain environment variable (e.g., X402_SOLANA_DEVNET_FEE_PAYER, X402_SOLANA_FEE_PAYER)
|
|
151
|
+
env_var_name = "X402_#{chain_name.upcase.gsub('-', '_')}_FEE_PAYER"
|
|
152
|
+
env_fee_payer = ENV[env_var_name]
|
|
153
|
+
return env_fee_payer if env_fee_payer && !env_fee_payer.empty?
|
|
154
|
+
|
|
155
|
+
# Check generic environment variable
|
|
156
|
+
env_fee_payer = ENV["X402_FEE_PAYER"]
|
|
157
|
+
return env_fee_payer if env_fee_payer && !env_fee_payer.empty?
|
|
158
|
+
|
|
159
|
+
# Fall back to default from chain config
|
|
160
|
+
chain_config(chain_name)[:fee_payer]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def to_caip2(network_name)
|
|
164
|
+
custom = X402.configuration.chain_config(network_name)
|
|
165
|
+
if custom
|
|
166
|
+
return "#{custom[:standard]}:#{custom[:chain_id]}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
CAIP2_MAPPING[network_name] || raise(ConfigurationError, "No CAIP-2 mapping for: #{network_name}")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def from_caip2(caip2_string)
|
|
173
|
+
return REVERSE_CAIP2_MAPPING[caip2_string] if REVERSE_CAIP2_MAPPING[caip2_string]
|
|
174
|
+
|
|
175
|
+
X402.configuration.custom_chains.each do |name, config|
|
|
176
|
+
caip2 = "#{config[:standard]}:#{config[:chain_id]}"
|
|
177
|
+
return name if caip2 == caip2_string
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
raise(ConfigurationError, "Unknown CAIP-2 network: #{caip2_string}")
|
|
181
|
+
end
|
|
87
182
|
|
|
88
|
-
|
|
89
|
-
env_var_name = "X402_#{chain_name.upcase.gsub('-', '_')}_RPC_URL"
|
|
90
|
-
env_rpc = ENV[env_var_name]
|
|
91
|
-
return env_rpc if env_rpc && !env_rpc.empty?
|
|
183
|
+
SOLANA_CHAINS = %w[solana solana-devnet].freeze
|
|
92
184
|
|
|
93
|
-
|
|
94
|
-
|
|
185
|
+
def solana_chain?(chain_name)
|
|
186
|
+
SOLANA_CHAINS.include?(chain_name)
|
|
95
187
|
end
|
|
96
188
|
end
|
|
97
189
|
end
|
data/lib/x402/configuration.rb
CHANGED
|
@@ -2,15 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
module X402
|
|
4
4
|
class Configuration
|
|
5
|
-
attr_accessor :wallet_address, :facilitator, :chain, :currency, :optimistic,
|
|
5
|
+
attr_accessor :wallet_address, :facilitator, :chain, :currency, :optimistic,
|
|
6
|
+
:version, :fee_payer, :custom_chains, :custom_tokens,
|
|
7
|
+
:accepted_payments
|
|
6
8
|
|
|
7
9
|
def initialize
|
|
8
10
|
@wallet_address = ENV.fetch("X402_WALLET_ADDRESS", nil)
|
|
9
|
-
@facilitator = ENV.fetch("X402_FACILITATOR_URL", "https://x402.org/facilitator")
|
|
11
|
+
@facilitator = ENV.fetch("X402_FACILITATOR_URL", "https://www.x402.org/facilitator")
|
|
10
12
|
@chain = ENV.fetch("X402_CHAIN", "base-sepolia")
|
|
11
13
|
@currency = ENV.fetch("X402_CURRENCY", "USDC")
|
|
12
|
-
@optimistic = ENV.fetch("X402_OPTIMISTIC", "false") == "true"
|
|
13
|
-
@
|
|
14
|
+
@optimistic = ENV.fetch("X402_OPTIMISTIC", "false") == "true"
|
|
15
|
+
@version = ENV.fetch("X402_VERSION", "2").to_i
|
|
16
|
+
@fee_payer = ENV.fetch("X402_FEE_PAYER", nil)
|
|
17
|
+
@custom_chains = {}
|
|
18
|
+
@custom_tokens = {}
|
|
19
|
+
@accepted_payments = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def accept(chain:, currency: "USDC", wallet_address: nil)
|
|
23
|
+
@accepted_payments << {
|
|
24
|
+
chain: chain,
|
|
25
|
+
currency: currency,
|
|
26
|
+
wallet_address: wallet_address
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def effective_accepted_payments
|
|
31
|
+
if @accepted_payments.empty?
|
|
32
|
+
[{ chain: @chain, currency: @currency, wallet_address: nil }]
|
|
33
|
+
else
|
|
34
|
+
@accepted_payments
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def register_chain(name:, chain_id:, standard: "eip155")
|
|
39
|
+
unless standard == "eip155"
|
|
40
|
+
raise ConfigurationError, "Only eip155 (EVM) chains are supported for custom registration"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@custom_chains[name] = {
|
|
44
|
+
chain_id: chain_id,
|
|
45
|
+
standard: standard
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def register_token(chain:, symbol:, address:, decimals:, name:, version: "1")
|
|
50
|
+
if chain.to_s.start_with?("solana")
|
|
51
|
+
raise ConfigurationError, "Custom tokens are only supported on EVM chains"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
key = "#{chain}:#{symbol}"
|
|
55
|
+
@custom_tokens[key] = {
|
|
56
|
+
symbol: symbol,
|
|
57
|
+
address: address,
|
|
58
|
+
decimals: decimals,
|
|
59
|
+
name: name,
|
|
60
|
+
version: version
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def chain_config(name)
|
|
65
|
+
@custom_chains[name]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def token_config(chain, symbol)
|
|
69
|
+
key = "#{chain}:#{symbol}"
|
|
70
|
+
@custom_tokens[key]
|
|
14
71
|
end
|
|
15
72
|
|
|
16
73
|
def validate!
|
|
@@ -18,6 +75,7 @@ module X402
|
|
|
18
75
|
raise ConfigurationError, "facilitator URL is required" if facilitator.nil? || facilitator.empty?
|
|
19
76
|
raise ConfigurationError, "chain is required" if chain.nil? || chain.empty?
|
|
20
77
|
raise ConfigurationError, "currency is required" if currency.nil? || currency.empty?
|
|
78
|
+
raise ConfigurationError, "version must be 1 or 2" unless [1, 2].include?(version)
|
|
21
79
|
end
|
|
22
80
|
end
|
|
23
81
|
|