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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1bfc5a5a404b98b156b9c8ad7d833787e5f082eff1b5d5b95e18663a1cb64fc8
4
- data.tar.gz: 0bda82b7d122d5b6fbbe6278db1dcd19efda67a6418ee2d8d6a23fb74ecd55fd
3
+ metadata.gz: 9b70874283a564f69295e16df7dcb11d4764480c62151f130335ee3d116d62f5
4
+ data.tar.gz: e4462c35ac33efc1993f166fa407b1e06f1b1fa35e3ba6547afce9031c07d3c4
5
5
  SHA512:
6
- metadata.gz: 88f0a27e97ee07b7ae746d7e0ecef9c8676bd8b40544fadead68f6eae5d57b5e318c73edf03645d97670ce197334b596e086d948e92c711ad7fe488f2a1666df
7
- data.tar.gz: c5647b5c8ce65f3bbdd2dde221afbd2a27240d77e12ba2f6e0c1387a1b046e170efbdbf5288898cc5b670ea9ee615d92f579e28d4f78b86558787ad5d90ebe77
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
  ![Coverage](./coverage/coverage.svg)
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
- | `rpc_urls` | No | `{}` | Custom RPC endpoint URLs per chain (see Custom RPC URLs below) |
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
- ### Custom RPC URLs
177
+ #### Register a Custom Chain
165
178
 
166
- By default, x402-rails uses public QuickNode RPC endpoints for each supported chain. For production use or higher reliability, you can configure custom RPC URLs from providers like [QuickNode](https://www.quicknode.com/).
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
- **Configuration Priority** (highest to lowest):
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
- 1. Programmatic configuration via `config.rpc_urls`
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
- #### Method 1: Programmatic Configuration
209
+ > **⚠️ Note:** The Facilitator used **must support** the specified chain and token to ensure proper functionality.
175
210
 
176
- Configure RPC URLs in your initializer:
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
- # Custom RPC URLs per chain
183
- config.rpc_urls["base"] = "https://your-base-rpc.quiknode.pro/your-key"
184
- config.rpc_urls["base-sepolia"] = "https://your-sepolia-rpc.quiknode.pro/your-key"
185
- config.rpc_urls["avalanche"] = "https://your-avalanche-rpc.quiknode.pro/your-key"
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
- #### Method 2: Environment Variables
232
+ #### Token Registration Parameters
190
233
 
191
- Set per-chain environment variables:
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
- ```bash
194
- # Per-chain RPC URL overrides
195
- X402_BASE_RPC_URL=https://your-base-rpc.quiknode.pro/your-key
196
- X402_BASE_SEPOLIA_RPC_URL=https://your-sepolia-rpc.quiknode.pro/your-key
197
- X402_AVALANCHE_RPC_URL=https://your-avalanche-rpc.quiknode.pro/your-key
198
- X402_AVALANCHE_FUJI_RPC_URL=https://your-fuji-rpc.quiknode.pro/your-key
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
- #### Method 3: Default RPC URLs
290
+ **Fallback behavior:** If no `config.accept()` calls are made, the default `config.chain` and `config.currency` are used.
202
291
 
203
- If no custom RPC URL is configured, it will default to the public QuickNode RPC urls.
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
- CHAINS[chain_name] || raise(ConfigurationError, "Unsupported chain: #{chain_name}")
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
- chain_config(chain_name)[:usdc_address]
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 rpc_url_for(chain_name)
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.rpc_urls[chain_name] if config.rpc_urls[chain_name]
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
- # Check environment variable
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
- # Fall back to default
94
- chain_config(chain_name)[:rpc_url]
185
+ def solana_chain?(chain_name)
186
+ SOLANA_CHAINS.include?(chain_name)
95
187
  end
96
188
  end
97
189
  end
@@ -2,15 +2,72 @@
2
2
 
3
3
  module X402
4
4
  class Configuration
5
- attr_accessor :wallet_address, :facilitator, :chain, :currency, :optimistic, :rpc_urls
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" # Default to optimistic mode (fast response, settle after)
13
- @rpc_urls = {}
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