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
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
|
|
@@ -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: []
|