blockchain0x-x402 0.0.1.alpha.0 → 0.0.1.alpha.1
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/lib/blockchain0x_x402/client.rb +59 -1
- data/lib/blockchain0x_x402/errors.rb +6 -0
- data/lib/blockchain0x_x402/version.rb +1 -1
- metadata +19 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee6cf734c4809fd53f64cb3a26a1f07710bd0d502008bb07e178f8c37b83bd5f
|
|
4
|
+
data.tar.gz: 9dfbf939eaa222810a74cc6cf502a5af8958077147843b270a15011e73ae7297
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59eb62699d8f3941e2c9edd2f40051a5e8015a4001a42ae10c8cd83419a867809b426c8baaef47100bfcee3aea15023c1e99373437461cca6b9fcb903f4ec728
|
|
7
|
+
data.tar.gz: fb89d30a00d8ac9fd262846d8ff3ca300408ce45bafa74927477f93e2a9688a2b7a1afa1060f1d0e178ab87a0f767ecdb6fb374ab2dd216cf03d8b85a0a49a95
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
# wrapper propagates that response unchanged so the caller can
|
|
22
22
|
# decide whether to loop or surface to the user.
|
|
23
23
|
#
|
|
24
|
+
# Spend policy (sub-plan 27.1 A6): the 402 challenge is
|
|
25
|
+
# ATTACKER-CONTROLLED input. Callers MUST set `max_amount_wei`
|
|
26
|
+
# (and should set `allowed_pay_to`) so a malicious or compromised
|
|
27
|
+
# seller cannot drain the session allowance by quoting an
|
|
28
|
+
# arbitrary amount/recipient. The wrapper also enforces the
|
|
29
|
+
# requirement's maxAgeSeconds and refuses 402s that match no
|
|
30
|
+
# known network instead of guessing.
|
|
31
|
+
#
|
|
24
32
|
# The `sdk` is duck-typed: any object responding to
|
|
25
33
|
# `network`, `payments_create(args)`, and `transactions_get(id)`
|
|
26
34
|
# works. Tests mock the surface; production passes a
|
|
@@ -39,24 +47,40 @@ module Blockchain0xX402
|
|
|
39
47
|
|
|
40
48
|
# @param sdk [#network, #payments_create, #transactions_get]
|
|
41
49
|
# @param agent_id [String] wallet that funds the on-chain payment
|
|
50
|
+
# @param max_amount_wei [String, nil] spend ceiling in 6-dp USDC base
|
|
51
|
+
# units (27.1 A6). SET THIS - a 402 quoting more is refused with
|
|
52
|
+
# ClientError 'amount_over_cap' before any payment is created.
|
|
53
|
+
# @param allowed_pay_to [Array<String>, nil] optional recipient
|
|
54
|
+
# allowlist (27.1 A6); compared case-insensitively.
|
|
42
55
|
# @param confirm_timeout_seconds [Integer]
|
|
43
56
|
# @param confirm_poll_seconds [Float]
|
|
44
57
|
# @param connection [Faraday::Connection, nil] test seam
|
|
45
58
|
# @param sleep_proc [#call, nil] test seam for the poll loop
|
|
59
|
+
# @param now_proc [#call, nil] test seam for the maxAgeSeconds clock
|
|
46
60
|
def initialize(
|
|
47
61
|
sdk:,
|
|
48
62
|
agent_id:,
|
|
63
|
+
max_amount_wei: nil,
|
|
64
|
+
allowed_pay_to: nil,
|
|
49
65
|
confirm_timeout_seconds: DEFAULT_CONFIRM_TIMEOUT_SECONDS,
|
|
50
66
|
confirm_poll_seconds: DEFAULT_CONFIRM_POLL_SECONDS,
|
|
51
67
|
connection: nil,
|
|
52
|
-
sleep_proc: nil
|
|
68
|
+
sleep_proc: nil,
|
|
69
|
+
now_proc: nil
|
|
53
70
|
)
|
|
71
|
+
if !max_amount_wei.nil? && !max_amount_wei.to_s.match?(/\A[0-9]+\z/)
|
|
72
|
+
raise ArgumentError, 'max_amount_wei must be an integer string of 6-dp USDC base units'
|
|
73
|
+
end
|
|
74
|
+
|
|
54
75
|
@sdk = sdk
|
|
55
76
|
@agent_id = agent_id
|
|
77
|
+
@max_amount_wei = max_amount_wei&.to_s
|
|
78
|
+
@allowed_pay_to = allowed_pay_to&.map(&:downcase)
|
|
56
79
|
@confirm_timeout = confirm_timeout_seconds
|
|
57
80
|
@confirm_poll = confirm_poll_seconds
|
|
58
81
|
@conn = connection || Faraday.new
|
|
59
82
|
@sleep = sleep_proc || Kernel.method(:sleep)
|
|
83
|
+
@now = now_proc || method(:monotonic_now)
|
|
60
84
|
end
|
|
61
85
|
|
|
62
86
|
# Perform an HTTP request that handles 402 automatically.
|
|
@@ -70,7 +94,11 @@ module Blockchain0xX402
|
|
|
70
94
|
return first unless first.status == 402
|
|
71
95
|
|
|
72
96
|
spec = Wire.parse_402_response(first)
|
|
97
|
+
challenge_at = @now.call
|
|
73
98
|
requirement = pick_requirement(spec.accepts)
|
|
99
|
+
# 27.1 A6: the 402 is attacker-controlled input - enforce the
|
|
100
|
+
# caller's spend policy BEFORE any payment is created.
|
|
101
|
+
enforce_spend_policy(requirement)
|
|
74
102
|
payment = @sdk.payments_create(
|
|
75
103
|
agent_id: @agent_id,
|
|
76
104
|
to: requirement.pay_to_address,
|
|
@@ -80,6 +108,18 @@ module Blockchain0xX402
|
|
|
80
108
|
raise ClientError.new('chain_failed', 'payments_create did not return an id.') if payment_id.nil?
|
|
81
109
|
|
|
82
110
|
confirmed = wait_for_confirmation(payment_id)
|
|
111
|
+
# 27.1 A6: a proof that confirmed after the requirement's
|
|
112
|
+
# maxAgeSeconds window is refused client-side, not presented.
|
|
113
|
+
if requirement.max_age_seconds
|
|
114
|
+
elapsed = @now.call - challenge_at
|
|
115
|
+
if elapsed > requirement.max_age_seconds
|
|
116
|
+
raise ClientError.new(
|
|
117
|
+
'stale_challenge',
|
|
118
|
+
"challenge expired: confirmation took #{elapsed.round}s, " \
|
|
119
|
+
"maxAgeSeconds is #{requirement.max_age_seconds}.",
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
83
123
|
|
|
84
124
|
header = Wire.build_payment_header(
|
|
85
125
|
Wire::ExactUsdcPayment.new(
|
|
@@ -122,6 +162,24 @@ module Blockchain0xX402
|
|
|
122
162
|
)
|
|
123
163
|
end
|
|
124
164
|
|
|
165
|
+
# 27.1 A6: spend-policy gate, evaluated before any payment exists.
|
|
166
|
+
def enforce_spend_policy(requirement)
|
|
167
|
+
if @max_amount_wei && Integer(requirement.amount_wei_usdc, 10) > Integer(@max_amount_wei, 10)
|
|
168
|
+
raise ClientError.new(
|
|
169
|
+
'amount_over_cap',
|
|
170
|
+
"402 quotes #{requirement.amount_wei_usdc} wei USDC, above the " \
|
|
171
|
+
"configured max_amount_wei #{@max_amount_wei}.",
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
return unless @allowed_pay_to && !@allowed_pay_to.include?(requirement.pay_to_address.downcase)
|
|
175
|
+
|
|
176
|
+
raise ClientError.new(
|
|
177
|
+
'recipient_not_allowed',
|
|
178
|
+
"402 pay-to address #{requirement.pay_to_address} is not in the " \
|
|
179
|
+
'configured allowed_pay_to list.',
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
125
183
|
def wait_for_confirmation(payment_id)
|
|
126
184
|
deadline = monotonic_now + @confirm_timeout
|
|
127
185
|
last = nil
|
|
@@ -35,8 +35,14 @@ module Blockchain0xX402
|
|
|
35
35
|
# challenge. Codes:
|
|
36
36
|
#
|
|
37
37
|
# no_matching_requirement - no accepts entry matched the SDK network
|
|
38
|
+
# (or the network is unknown; sub-plan 27.1
|
|
39
|
+
# A6 - a refusal, never an accepts[0] guess)
|
|
38
40
|
# settlement_timeout - on-chain payment did not confirm in budget
|
|
39
41
|
# chain_failed - payment status flipped to failed
|
|
42
|
+
# amount_over_cap - 402 quoted above max_amount_wei (27.1 A6)
|
|
43
|
+
# recipient_not_allowed - payToAddress outside allowed_pay_to (27.1 A6)
|
|
44
|
+
# stale_challenge - confirmation landed after the requirement's
|
|
45
|
+
# maxAgeSeconds window (27.1 A6)
|
|
40
46
|
class ClientError < Error
|
|
41
47
|
end
|
|
42
48
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: blockchain0x-x402
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.1.alpha.
|
|
4
|
+
version: 0.0.1.alpha.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Blockchain0x
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -30,6 +30,20 @@ dependencies:
|
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '3.0'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: base64
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.2'
|
|
40
|
+
type: :runtime
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.2'
|
|
33
47
|
- !ruby/object:Gem::Dependency
|
|
34
48
|
name: rspec
|
|
35
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,10 +79,10 @@ licenses:
|
|
|
65
79
|
- Apache-2.0
|
|
66
80
|
metadata:
|
|
67
81
|
homepage_uri: https://blockchain0x.com
|
|
68
|
-
source_code_uri: https://github.com/
|
|
69
|
-
bug_tracker_uri: https://github.com/
|
|
82
|
+
source_code_uri: https://github.com/tosh-labs/blockchain0x-app/tree/dev/packages/sdk-ruby-x402
|
|
83
|
+
bug_tracker_uri: https://github.com/tosh-labs/blockchain0x-app/issues
|
|
70
84
|
documentation_uri: https://docs.blockchain0x.com
|
|
71
|
-
changelog_uri: https://github.com/
|
|
85
|
+
changelog_uri: https://github.com/tosh-labs/blockchain0x-x402-ruby/blob/main/CHANGELOG.md
|
|
72
86
|
rubygems_mfa_required: 'true'
|
|
73
87
|
post_install_message:
|
|
74
88
|
rdoc_options: []
|