x402-rails 0.2.0 → 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 +11 -4
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
|
-
|
|
8
|
+
protocol_version = version || config.version
|
|
9
|
+
version_strategy = X402::Versions.for(protocol_version)
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
accepted_payments = resolve_accepted_payments(
|
|
12
|
+
accepts: accepts,
|
|
13
|
+
chain: chain,
|
|
14
|
+
currency: currency
|
|
15
|
+
)
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
requirement = PaymentRequirement.new(
|
|
48
|
+
internal_requirement = {
|
|
29
49
|
scheme: "exact",
|
|
30
|
-
network:
|
|
31
|
-
|
|
50
|
+
network: chain,
|
|
51
|
+
amount: atomic_amount,
|
|
32
52
|
asset: asset_address,
|
|
33
|
-
pay_to:
|
|
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:
|
|
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.
|
|
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:
|
|
11
|
+
date: 2026-01-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -128,9 +128,11 @@ email:
|
|
|
128
128
|
- zach+x402@quiknode.io
|
|
129
129
|
executables: []
|
|
130
130
|
extensions: []
|
|
131
|
-
extra_rdoc_files:
|
|
131
|
+
extra_rdoc_files:
|
|
132
|
+
- README.md
|
|
132
133
|
files:
|
|
133
134
|
- ".rspec"
|
|
135
|
+
- CHANGELOG.md
|
|
134
136
|
- LICENSE.txt
|
|
135
137
|
- README.md
|
|
136
138
|
- Rakefile
|
|
@@ -149,12 +151,17 @@ files:
|
|
|
149
151
|
- lib/x402/rails/version.rb
|
|
150
152
|
- lib/x402/requirement_generator.rb
|
|
151
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
|
|
152
158
|
homepage: https://github.com/quiknode-labs/x402-rails
|
|
153
159
|
licenses:
|
|
154
160
|
- MIT
|
|
155
161
|
metadata:
|
|
156
162
|
homepage_uri: https://github.com/quiknode-labs/x402-rails
|
|
157
163
|
source_code_uri: https://github.com/quiknode-labs/x402-rails
|
|
164
|
+
documentation_uri: https://github.com/quiknode-labs/x402-rails#readme
|
|
158
165
|
post_install_message:
|
|
159
166
|
rdoc_options: []
|
|
160
167
|
require_paths:
|
|
@@ -173,5 +180,5 @@ requirements: []
|
|
|
173
180
|
rubygems_version: 3.5.16
|
|
174
181
|
signing_key:
|
|
175
182
|
specification_version: 4
|
|
176
|
-
summary: Rails integration for x402 payment protocol
|
|
183
|
+
summary: Rails integration for x402 payment protocol (supporting x402 v2).
|
|
177
184
|
test_files: []
|