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.
data/lib/x402/rails.rb CHANGED
@@ -9,6 +9,7 @@ require "base64"
9
9
  require_relative "rails/version"
10
10
  require_relative "../x402/configuration"
11
11
  require_relative "../x402/chains"
12
+ require_relative "../x402/versions"
12
13
  require_relative "../x402/payment_requirement"
13
14
  require_relative "../x402/payment_payload"
14
15
  require_relative "../x402/settlement_response"
@@ -2,54 +2,97 @@
2
2
 
3
3
  module X402
4
4
  class RequirementGenerator
5
- def self.generate(amount:, resource:, description: nil, chain: nil, currency: nil)
5
+ def self.generate(amount:, resource:, description: nil, chain: nil, currency: nil,
6
+ wallet_address: nil, fee_payer: nil, version: nil, accepts: nil)
6
7
  config = X402.configuration
7
- chain_name = chain || config.chain
8
+ protocol_version = version || config.version
9
+ version_strategy = X402::Versions.for(protocol_version)
8
10
 
9
- # Get chain and currency configuration for this specific chain
10
- chain_config = X402.chain_config(chain_name)
11
- currency_config = X402.currency_config_for_chain(chain_name)
11
+ accepted_payments = resolve_accepted_payments(
12
+ accepts: accepts,
13
+ chain: chain,
14
+ currency: currency
15
+ )
12
16
 
13
- # Convert amount to atomic units
14
- atomic_amount = convert_to_atomic(amount, currency_config[:decimals])
17
+ formatted_accepts = accepted_payments.map do |payment|
18
+ build_formatted_requirement(
19
+ amount: amount,
20
+ resource: resource,
21
+ description: description,
22
+ chain: payment[:chain],
23
+ currency: payment[:currency],
24
+ wallet_address: wallet_address || payment[:wallet_address] || config.wallet_address,
25
+ fee_payer: fee_payer,
26
+ version_strategy: version_strategy
27
+ )
28
+ end
15
29
 
16
- # Get asset address (USDC contract address for the chain)
17
- asset_address = X402.usdc_address_for(chain_name)
30
+ version_strategy.format_requirement_response(
31
+ accepts: formatted_accepts,
32
+ resource: {
33
+ url: resource,
34
+ description: description || "Payment required for #{resource}",
35
+ mime_type: "application/json"
36
+ },
37
+ error: "Payment required to access this resource"
38
+ )
39
+ end
18
40
 
19
- # Build EIP-712 domain info (required for signature verification)
20
- # IMPORTANT: The name must match what the USDC contract returns for name()
21
- # Testnets use "USDC", mainnets use "USD Coin" or "USDC" depending on chain
22
- eip712_domain = {
23
- name: currency_config[:name],
24
- version: currency_config[:version]
25
- }
41
+ def self.build_formatted_requirement(amount:, resource:, description:, chain:, currency:,
42
+ wallet_address:, fee_payer:, version_strategy:)
43
+ token_config = X402.token_config_for(chain, currency)
44
+ asset_address = X402.asset_address_for(chain, currency)
45
+ atomic_amount = convert_to_atomic(amount, token_config[:decimals])
46
+ extra_data = build_extra_data(chain, token_config, fee_payer)
26
47
 
27
- # Create payment requirement
28
- requirement = PaymentRequirement.new(
48
+ internal_requirement = {
29
49
  scheme: "exact",
30
- network: chain_name,
31
- max_amount_required: atomic_amount,
50
+ network: chain,
51
+ amount: atomic_amount,
32
52
  asset: asset_address,
33
- pay_to: config.wallet_address,
53
+ pay_to: wallet_address,
34
54
  resource: resource,
35
55
  description: description || "Payment required for #{resource}",
36
56
  max_timeout_seconds: 600,
37
57
  mime_type: "application/json",
38
- extra: eip712_domain
39
- )
40
-
41
- # Build full response
42
- {
43
- x402Version: 1,
44
- error: "Payment required to access this resource",
45
- accepts: [requirement.to_h]
58
+ extra: extra_data
46
59
  }
60
+
61
+ version_strategy.format_requirement(internal_requirement)
62
+ end
63
+
64
+ def self.resolve_accepted_payments(accepts:, chain:, currency:)
65
+ config = X402.configuration
66
+
67
+ if accepts && !accepts.empty?
68
+ accepts.map do |acc|
69
+ {
70
+ chain: acc[:chain],
71
+ currency: acc[:currency] || "USDC",
72
+ wallet_address: acc[:wallet_address]
73
+ }
74
+ end
75
+ elsif chain
76
+ [{ chain: chain, currency: currency || config.currency, wallet_address: nil }]
77
+ else
78
+ config.effective_accepted_payments
79
+ end
47
80
  end
48
81
 
49
82
  def self.convert_to_atomic(amount, decimals)
50
- # Convert USD amount to atomic units
51
- # For USDC with 6 decimals: $0.001 = 1000 atomic units
52
83
  (amount.to_f * (10**decimals)).to_i
53
84
  end
85
+
86
+ def self.build_extra_data(chain_name, token_config, fee_payer_override)
87
+ if X402.solana_chain?(chain_name)
88
+ fee_payer = fee_payer_override || X402.fee_payer_for(chain_name)
89
+ { feePayer: fee_payer }
90
+ else
91
+ {
92
+ name: token_config[:name],
93
+ version: token_config[:version]
94
+ }
95
+ end
96
+ end
54
97
  end
55
98
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Versions
5
+ class Base
6
+ def version_number
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def payment_header_name
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def response_header_name
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def requirement_header_name
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def format_network(internal_network)
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def parse_network(network)
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def format_requirement(requirement)
31
+ raise NotImplementedError
32
+ end
33
+
34
+ def format_requirement_response(accepts:, resource:, error:)
35
+ raise NotImplementedError
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Versions
5
+ class V1 < Base
6
+ def version_number
7
+ 1
8
+ end
9
+
10
+ def payment_header_name
11
+ "X-PAYMENT"
12
+ end
13
+
14
+ def response_header_name
15
+ "X-PAYMENT-RESPONSE"
16
+ end
17
+
18
+ def requirement_header_name
19
+ nil
20
+ end
21
+
22
+ def format_network(internal_network)
23
+ internal_network
24
+ end
25
+
26
+ def parse_network(network)
27
+ network
28
+ end
29
+
30
+ def format_requirement(requirement)
31
+ {
32
+ scheme: requirement[:scheme],
33
+ network: format_network(requirement[:network]),
34
+ maxAmountRequired: requirement[:amount].to_s,
35
+ asset: requirement[:asset],
36
+ payTo: requirement[:pay_to],
37
+ resource: requirement[:resource],
38
+ description: requirement[:description],
39
+ maxTimeoutSeconds: requirement[:max_timeout_seconds],
40
+ mimeType: requirement[:mime_type],
41
+ extra: requirement[:extra]
42
+ }.compact
43
+ end
44
+
45
+ def format_requirement_response(accepts:, resource:, error:)
46
+ {
47
+ x402Version: 1,
48
+ error: error,
49
+ accepts: accepts
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X402
4
+ module Versions
5
+ class V2 < Base
6
+ def version_number
7
+ 2
8
+ end
9
+
10
+ def payment_header_name
11
+ "PAYMENT-SIGNATURE"
12
+ end
13
+
14
+ def response_header_name
15
+ "PAYMENT-RESPONSE"
16
+ end
17
+
18
+ def requirement_header_name
19
+ "PAYMENT-REQUIRED"
20
+ end
21
+
22
+ def format_network(internal_network)
23
+ X402.to_caip2(internal_network)
24
+ end
25
+
26
+ def parse_network(network)
27
+ if network.include?(":")
28
+ X402.from_caip2(network)
29
+ else
30
+ network
31
+ end
32
+ end
33
+
34
+ def format_requirement(requirement)
35
+ {
36
+ scheme: requirement[:scheme],
37
+ network: format_network(requirement[:network]),
38
+ amount: requirement[:amount].to_s,
39
+ asset: requirement[:asset],
40
+ payTo: requirement[:pay_to],
41
+ maxTimeoutSeconds: requirement[:max_timeout_seconds],
42
+ extra: requirement[:extra]
43
+ }.compact
44
+ end
45
+
46
+ def format_requirement_response(accepts:, resource:, error:)
47
+ {
48
+ x402Version: 2,
49
+ error: error,
50
+ resource: {
51
+ url: resource[:url],
52
+ description: resource[:description],
53
+ mimeType: resource[:mime_type]
54
+ }.compact,
55
+ accepts: accepts,
56
+ extensions: {}
57
+ }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "versions/base"
4
+ require_relative "versions/v1"
5
+ require_relative "versions/v2"
6
+
7
+ module X402
8
+ module Versions
9
+ def self.for(version)
10
+ case version.to_i
11
+ when 1 then V1.new
12
+ when 2 then V2.new
13
+ else raise ConfigurationError, "Unsupported x402 version: #{version}"
14
+ end
15
+ end
16
+ end
17
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: x402-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - QuickNode
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-30 00:00:00.000000000 Z
11
+ date: 2026-01-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -132,6 +132,7 @@ extra_rdoc_files:
132
132
  - README.md
133
133
  files:
134
134
  - ".rspec"
135
+ - CHANGELOG.md
135
136
  - LICENSE.txt
136
137
  - README.md
137
138
  - Rakefile
@@ -150,6 +151,10 @@ files:
150
151
  - lib/x402/rails/version.rb
151
152
  - lib/x402/requirement_generator.rb
152
153
  - lib/x402/settlement_response.rb
154
+ - lib/x402/versions.rb
155
+ - lib/x402/versions/base.rb
156
+ - lib/x402/versions/v1.rb
157
+ - lib/x402/versions/v2.rb
153
158
  homepage: https://github.com/quiknode-labs/x402-rails
154
159
  licenses:
155
160
  - MIT
@@ -175,5 +180,5 @@ requirements: []
175
180
  rubygems_version: 3.5.16
176
181
  signing_key:
177
182
  specification_version: 4
178
- summary: Rails integration for x402 payment protocol
183
+ summary: Rails integration for x402 payment protocol (supporting x402 v2).
179
184
  test_files: []