mpp-rb 0.1.4 → 0.1.6
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/README.md +128 -1
- data/lib/mpp/challenge.rb +31 -8
- data/lib/mpp/challenge_echo.rb +14 -3
- data/lib/mpp/challenge_id.rb +30 -6
- data/lib/mpp/client/transport.rb +1 -1
- data/lib/mpp/errors.rb +22 -0
- data/lib/mpp/http/headers.rb +61 -0
- data/lib/mpp/http.rb +8 -0
- data/lib/mpp/methods/evm/assets.rb +103 -0
- data/lib/mpp/methods/evm/authorization.rb +140 -0
- data/lib/mpp/methods/evm/charge_intent.rb +129 -0
- data/lib/mpp/methods/evm/evm_method.rb +133 -0
- data/lib/mpp/methods/evm.rb +67 -0
- data/lib/mpp/methods/stripe/charge_intent.rb +10 -8
- data/lib/mpp/methods/stripe/crypto_payment_recorder.rb +55 -0
- data/lib/mpp/methods/stripe/defaults.rb +1 -0
- data/lib/mpp/methods/stripe/machine_payments.rb +130 -0
- data/lib/mpp/methods/stripe/stripe_method.rb +22 -3
- data/lib/mpp/methods/stripe.rb +23 -0
- data/lib/mpp/methods/tempo/attribution.rb +3 -9
- data/lib/mpp/methods/tempo/client_method.rb +35 -6
- data/lib/mpp/methods/tempo/fee_payer_client.rb +120 -0
- data/lib/mpp/methods/tempo/intents.rb +80 -48
- data/lib/mpp/methods/tempo/proof.rb +39 -12
- data/lib/mpp/methods/tempo/relay.rb +257 -0
- data/lib/mpp/methods/tempo.rb +2 -0
- data/lib/mpp/parsing.rb +22 -2
- data/lib/mpp/receipt.rb +4 -3
- data/lib/mpp/server/accept_payment.rb +194 -0
- data/lib/mpp/server/compose.rb +399 -0
- data/lib/mpp/server/decorator.rb +30 -7
- data/lib/mpp/server/method.rb +15 -0
- data/lib/mpp/server/middleware.rb +160 -18
- data/lib/mpp/server/mpp_handler.rb +322 -34
- data/lib/mpp/server/result.rb +100 -0
- data/lib/mpp/server/verify.rb +24 -14
- data/lib/mpp/server.rb +4 -0
- data/lib/mpp/version.rb +1 -1
- data/lib/mpp/x402/facilitator.rb +127 -0
- data/lib/mpp/x402/header.rb +131 -0
- data/lib/mpp/x402/server.rb +247 -0
- data/lib/mpp/x402/types.rb +33 -0
- data/lib/mpp/x402.rb +12 -0
- data/lib/mpp.rb +16 -3
- metadata +48 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mpp
|
|
5
|
+
module Server
|
|
6
|
+
# Result of a composed (or multi-method) payment verification.
|
|
7
|
+
#
|
|
8
|
+
# Either a 402 with one or more Challenges, or a verified payment.
|
|
9
|
+
class ComposedResult
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(Integer) }
|
|
13
|
+
attr_reader :status
|
|
14
|
+
|
|
15
|
+
sig { returns(T::Array[Mpp::Challenge]) }
|
|
16
|
+
attr_reader :challenges
|
|
17
|
+
|
|
18
|
+
sig { returns(T.untyped) }
|
|
19
|
+
attr_reader :credential
|
|
20
|
+
|
|
21
|
+
sig { returns(T.nilable(Mpp::Receipt)) }
|
|
22
|
+
attr_reader :receipt
|
|
23
|
+
|
|
24
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
25
|
+
attr_reader :extra_headers
|
|
26
|
+
|
|
27
|
+
sig { returns(String) }
|
|
28
|
+
attr_reader :realm
|
|
29
|
+
|
|
30
|
+
sig do
|
|
31
|
+
params(
|
|
32
|
+
status: Integer,
|
|
33
|
+
realm: String,
|
|
34
|
+
challenges: T::Array[Mpp::Challenge],
|
|
35
|
+
credential: T.untyped,
|
|
36
|
+
receipt: T.nilable(Mpp::Receipt),
|
|
37
|
+
extra_headers: T::Hash[String, T.untyped]
|
|
38
|
+
).void
|
|
39
|
+
end
|
|
40
|
+
def initialize(status:, realm:, challenges: [], credential: nil, receipt: nil, extra_headers: {})
|
|
41
|
+
@status = T.let(status, Integer)
|
|
42
|
+
@realm = T.let(realm, String)
|
|
43
|
+
@challenges = T.let(challenges, T::Array[Mpp::Challenge])
|
|
44
|
+
@credential = T.let(credential, T.untyped)
|
|
45
|
+
@receipt = T.let(receipt, T.nilable(Mpp::Receipt))
|
|
46
|
+
@extra_headers = T.let(extra_headers, T::Hash[String, T.untyped])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig do
|
|
50
|
+
params(
|
|
51
|
+
challenges: T::Array[Mpp::Challenge],
|
|
52
|
+
realm: String,
|
|
53
|
+
extra_headers: T::Hash[String, T.untyped]
|
|
54
|
+
).returns(ComposedResult)
|
|
55
|
+
end
|
|
56
|
+
def self.payment_required(challenges, realm:, extra_headers: {})
|
|
57
|
+
new(status: 402, realm: realm, challenges: challenges, extra_headers: extra_headers)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig do
|
|
61
|
+
params(
|
|
62
|
+
credential: T.untyped,
|
|
63
|
+
receipt: Mpp::Receipt,
|
|
64
|
+
realm: String,
|
|
65
|
+
extra_headers: T::Hash[String, T.untyped]
|
|
66
|
+
).returns(ComposedResult)
|
|
67
|
+
end
|
|
68
|
+
def self.paid(credential, receipt, realm:, extra_headers: {})
|
|
69
|
+
new(status: 200, realm: realm, credential: credential, receipt: receipt, extra_headers: extra_headers)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { returns(T::Boolean) }
|
|
73
|
+
def payment_required?
|
|
74
|
+
@status == 402
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns [credential, receipt] for a successful payment.
|
|
78
|
+
sig { returns(T::Array[T.untyped]) }
|
|
79
|
+
def payment
|
|
80
|
+
Kernel.raise ArgumentError, "payment is not available for a 402 response" if payment_required?
|
|
81
|
+
|
|
82
|
+
[@credential, @receipt]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
sig { returns(T.nilable(String)) }
|
|
86
|
+
def payment_response
|
|
87
|
+
value = @extra_headers["PAYMENT-RESPONSE"]
|
|
88
|
+
value.is_a?(String) ? value : nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Render a 402 challenge response hash, including extra x402 headers.
|
|
92
|
+
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
|
93
|
+
def to_response
|
|
94
|
+
Kernel.raise ArgumentError, "to_response is only valid for 402 results" unless payment_required?
|
|
95
|
+
|
|
96
|
+
Decorator.make_challenge_response(@challenges, @realm, extra_headers: @extra_headers)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/mpp/server/verify.rb
CHANGED
|
@@ -15,17 +15,17 @@ module Mpp
|
|
|
15
15
|
# Verify a payment credential or generate a new challenge.
|
|
16
16
|
#
|
|
17
17
|
# Returns Challenge (payment required) or [Credential, Receipt] (verified).
|
|
18
|
-
sig { params(authorization: T.nilable(String), intent: T.untyped, request: T::Hash[String, T.untyped], realm: String, secret_key: String, method: T.nilable(String), description: T.nilable(String), meta: T.nilable(T::Hash[String, T.untyped]), expires: T.nilable(String), events: T.nilable(Mpp::Events::Dispatcher), body: T.untyped).returns(T.untyped) }
|
|
18
|
+
sig { params(authorization: T.nilable(String), intent: T.untyped, request: T::Hash[String, T.untyped], realm: String, secret_key: String, method: T.nilable(String), description: T.nilable(String), meta: T.nilable(T::Hash[String, T.untyped]), expires: T.nilable(String), events: T.nilable(Mpp::Events::Dispatcher), method_hook_events: T.nilable(Mpp::Events::Dispatcher), body: T.untyped, header: T.nilable(String)).returns(T.untyped) }
|
|
19
19
|
def verify_or_challenge(authorization:, intent:, request:, realm:, secret_key:,
|
|
20
|
-
method: nil, description: nil, meta: nil, expires: nil, events: nil, body: nil)
|
|
20
|
+
method: nil, description: nil, meta: nil, expires: nil, events: nil, method_hook_events: nil, body: nil, header: nil)
|
|
21
21
|
method_name = method || "tempo"
|
|
22
22
|
request = Mpp::Units.transform_units(request)
|
|
23
23
|
dispatcher = events
|
|
24
|
-
events_enabled = dispatcher&.has_handlers?
|
|
24
|
+
events_enabled = dispatcher&.has_handlers? || method_hook_events&.has_handlers?
|
|
25
25
|
method_context = events_enabled ? {name: method_name, intent: intent.name} : nil
|
|
26
26
|
|
|
27
27
|
new_challenge = Kernel.lambda { |credential = nil, error = nil, submitted_challenge = nil|
|
|
28
|
-
challenge = create_challenge(method_name, intent.name, request, realm, secret_key, description, meta, expires, body)
|
|
28
|
+
challenge = create_challenge(method_name, intent.name, request, realm, secret_key, description, meta, expires, body, header)
|
|
29
29
|
if error && dispatcher&.has_handlers?(Mpp::Events::PAYMENT_FAILED)
|
|
30
30
|
emit_payment_failed(
|
|
31
31
|
dispatcher: dispatcher,
|
|
@@ -79,7 +79,8 @@ module Mpp
|
|
|
79
79
|
request: echo_request,
|
|
80
80
|
expires: echo.expires,
|
|
81
81
|
digest: echo.digest,
|
|
82
|
-
opaque: echo_opaque
|
|
82
|
+
opaque: echo_opaque,
|
|
83
|
+
header: echo.header
|
|
83
84
|
)
|
|
84
85
|
unless Mpp.secure_compare(echo.id, expected_id)
|
|
85
86
|
return new_challenge.call(
|
|
@@ -172,9 +173,10 @@ module Mpp
|
|
|
172
173
|
end
|
|
173
174
|
Kernel.raise
|
|
174
175
|
end
|
|
175
|
-
if dispatcher&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
|
|
176
|
+
if dispatcher&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS) || method_hook_events&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
|
|
176
177
|
emit_payment_success(
|
|
177
178
|
dispatcher: dispatcher,
|
|
179
|
+
method_hook_dispatcher: method_hook_events,
|
|
178
180
|
challenge: echo,
|
|
179
181
|
credential: credential,
|
|
180
182
|
method: T.must(method_context),
|
|
@@ -185,9 +187,9 @@ module Mpp
|
|
|
185
187
|
[credential, receipt]
|
|
186
188
|
end
|
|
187
189
|
|
|
188
|
-
sig { params(method: String, intent_name: String, request: T::Hash[String, T.untyped], realm: String, secret_key: String, description: T.nilable(String), meta: T.nilable(T::Hash[String, T.untyped]), expires: T.nilable(String), body: T.untyped).returns(Mpp::Challenge) }
|
|
190
|
+
sig { params(method: String, intent_name: String, request: T::Hash[String, T.untyped], realm: String, secret_key: String, description: T.nilable(String), meta: T.nilable(T::Hash[String, T.untyped]), expires: T.nilable(String), body: T.untyped, header: T.nilable(String)).returns(Mpp::Challenge) }
|
|
189
191
|
def create_challenge(method, intent_name, request, realm, secret_key,
|
|
190
|
-
description = nil, meta = nil, expires = nil, body = nil)
|
|
192
|
+
description = nil, meta = nil, expires = nil, body = nil, header = nil)
|
|
191
193
|
expires = nil if expires && !expires.is_a?(String)
|
|
192
194
|
|
|
193
195
|
if expires.nil?
|
|
@@ -205,7 +207,8 @@ module Mpp
|
|
|
205
207
|
expires: expires,
|
|
206
208
|
digest: digest,
|
|
207
209
|
description: description,
|
|
208
|
-
meta: meta
|
|
210
|
+
meta: meta,
|
|
211
|
+
header: header
|
|
209
212
|
)
|
|
210
213
|
end
|
|
211
214
|
|
|
@@ -258,17 +261,24 @@ module Mpp
|
|
|
258
261
|
dispatcher.emit(Mpp::Events::PAYMENT_FAILED, payload)
|
|
259
262
|
end
|
|
260
263
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
+
# Method hooks are owned by a handler, while `dispatcher` may be shared
|
|
265
|
+
# by many handlers. Emit the same payload to each without exposing that
|
|
266
|
+
# handler ownership in the public event payload.
|
|
267
|
+
sig { params(dispatcher: T.nilable(Mpp::Events::Dispatcher), challenge: T.untyped, credential: T.untyped, method: T::Hash[Symbol, T.untyped], receipt: T.untyped, request: T::Hash[String, T.untyped], method_hook_dispatcher: T.nilable(Mpp::Events::Dispatcher)).void }
|
|
268
|
+
def emit_payment_success(dispatcher:, challenge:, credential:, method:, receipt:, request:, method_hook_dispatcher: nil)
|
|
269
|
+
public_handlers = dispatcher&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
|
|
270
|
+
hook_handlers = method_hook_dispatcher&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
|
|
271
|
+
return unless public_handlers || hook_handlers
|
|
264
272
|
|
|
265
|
-
|
|
273
|
+
payload = {
|
|
266
274
|
challenge: challenge,
|
|
267
275
|
credential: credential,
|
|
268
276
|
method: method,
|
|
269
277
|
receipt: receipt,
|
|
270
278
|
request: request
|
|
271
|
-
}
|
|
279
|
+
}
|
|
280
|
+
dispatcher.emit(Mpp::Events::PAYMENT_SUCCESS, payload) if public_handlers
|
|
281
|
+
method_hook_dispatcher.emit(Mpp::Events::PAYMENT_SUCCESS, payload) if hook_handlers && method_hook_dispatcher != dispatcher
|
|
272
282
|
end
|
|
273
283
|
end
|
|
274
284
|
end
|
data/lib/mpp/server.rb
CHANGED
|
@@ -11,5 +11,9 @@ module Mpp
|
|
|
11
11
|
autoload :MppHandler, "mpp/server/mpp_handler"
|
|
12
12
|
autoload :Decorator, "mpp/server/decorator"
|
|
13
13
|
autoload :Middleware, "mpp/server/middleware"
|
|
14
|
+
autoload :AcceptPayment, "mpp/server/accept_payment"
|
|
15
|
+
autoload :ComposedResult, "mpp/server/result"
|
|
16
|
+
autoload :ComposedHandler, "mpp/server/compose"
|
|
17
|
+
autoload :ComposeOffer, "mpp/server/compose"
|
|
14
18
|
end
|
|
15
19
|
end
|
data/lib/mpp/version.rb
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "types"
|
|
5
|
+
require_relative "../http/headers"
|
|
6
|
+
require "json"
|
|
7
|
+
require "net/http"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module Mpp
|
|
11
|
+
module X402
|
|
12
|
+
# HTTP client for an x402 facilitator `/verify` and `/settle` API.
|
|
13
|
+
#
|
|
14
|
+
# `resolve` accepts:
|
|
15
|
+
# * a URL string (public / unauthenticated facilitator)
|
|
16
|
+
# * a config hash (`url:` plus optional `headers:` as a Hash or per-request proc)
|
|
17
|
+
# * any object that responds to `#verify` and `#settle` (e.g. a CDP HTTPFacilitatorClient)
|
|
18
|
+
class Facilitator
|
|
19
|
+
extend T::Sig
|
|
20
|
+
|
|
21
|
+
sig { returns(String) }
|
|
22
|
+
attr_reader :base_url
|
|
23
|
+
|
|
24
|
+
sig {
|
|
25
|
+
params(
|
|
26
|
+
url: String,
|
|
27
|
+
headers: T.untyped
|
|
28
|
+
).void
|
|
29
|
+
}
|
|
30
|
+
def initialize(url, headers: nil)
|
|
31
|
+
@base_url = T.let(Mpp::Http::Headers.normalize_base_url(url), String)
|
|
32
|
+
Kernel.raise ArgumentError, "x402 exact requires `facilitator`." if @base_url.empty?
|
|
33
|
+
|
|
34
|
+
@headers = T.let(headers, T.untyped)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { params(facilitator: T.untyped).returns(T.untyped) }
|
|
38
|
+
def self.resolve(facilitator)
|
|
39
|
+
return facilitator if facilitator.is_a?(Facilitator)
|
|
40
|
+
return new(facilitator) if facilitator.is_a?(String) && !facilitator.empty?
|
|
41
|
+
return from_config(facilitator) if facilitator.is_a?(Hash)
|
|
42
|
+
return facilitator if duck_type?(facilitator)
|
|
43
|
+
|
|
44
|
+
Kernel.raise ArgumentError, "x402 exact requires `facilitator`."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { params(config: T::Hash[T.untyped, T.untyped]).returns(Facilitator) }
|
|
48
|
+
def self.from_config(config)
|
|
49
|
+
cfg = Mpp::Http::Headers.symbolize(config)
|
|
50
|
+
url = cfg[:url] || cfg[:base_url] || cfg[:baseUrl]
|
|
51
|
+
|
|
52
|
+
new(url.to_s, headers: cfg[:headers])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(facilitator: T.untyped).returns(T::Boolean) }
|
|
56
|
+
def self.duck_type?(facilitator)
|
|
57
|
+
!facilitator.nil? && facilitator.respond_to?(:verify) && facilitator.respond_to?(:settle)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig do
|
|
61
|
+
params(
|
|
62
|
+
payment_payload: T::Hash[T.untyped, T.untyped],
|
|
63
|
+
payment_requirements: T::Hash[T.untyped, T.untyped]
|
|
64
|
+
).returns(T::Hash[T.untyped, T.untyped])
|
|
65
|
+
end
|
|
66
|
+
def verify(payment_payload, payment_requirements)
|
|
67
|
+
post("/verify", payment_payload, payment_requirements)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
sig do
|
|
71
|
+
params(
|
|
72
|
+
payment_payload: T::Hash[T.untyped, T.untyped],
|
|
73
|
+
payment_requirements: T::Hash[T.untyped, T.untyped]
|
|
74
|
+
).returns(T::Hash[T.untyped, T.untyped])
|
|
75
|
+
end
|
|
76
|
+
def settle(payment_payload, payment_requirements)
|
|
77
|
+
post("/settle", payment_payload, payment_requirements)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
sig { params(path: String).returns(T::Hash[String, String]) }
|
|
83
|
+
def request_headers(path)
|
|
84
|
+
Mpp::Http::Headers.resolve(@headers, path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
sig do
|
|
88
|
+
params(
|
|
89
|
+
path: String,
|
|
90
|
+
payment_payload: T::Hash[T.untyped, T.untyped],
|
|
91
|
+
payment_requirements: T::Hash[T.untyped, T.untyped]
|
|
92
|
+
).returns(T::Hash[T.untyped, T.untyped])
|
|
93
|
+
end
|
|
94
|
+
def post(path, payment_payload, payment_requirements)
|
|
95
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
96
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
97
|
+
http.use_ssl = uri.scheme == "https"
|
|
98
|
+
request = Net::HTTP::Post.new(uri)
|
|
99
|
+
request["Content-Type"] = "application/json"
|
|
100
|
+
request_headers(path).each { |key, value| request[key] = value }
|
|
101
|
+
request.body = JSON.generate({
|
|
102
|
+
"paymentPayload" => payment_payload,
|
|
103
|
+
"paymentRequirements" => payment_requirements,
|
|
104
|
+
"x402Version" => VERSION
|
|
105
|
+
})
|
|
106
|
+
response = http.request(request)
|
|
107
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
108
|
+
Kernel.raise Mpp::VerificationFailedError.new(reason: "facilitator #{path} returned HTTP #{response.code}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
body = response.body.to_s
|
|
112
|
+
if body.empty?
|
|
113
|
+
Kernel.raise Mpp::VerificationFailedError.new(reason: "facilitator #{path} returned an empty body")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
parsed = JSON.parse(body)
|
|
117
|
+
unless parsed.is_a?(Hash)
|
|
118
|
+
Kernel.raise Mpp::VerificationFailedError.new(reason: "facilitator #{path} returned JSON that is not an object")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
parsed
|
|
122
|
+
rescue JSON::ParserError
|
|
123
|
+
Kernel.raise Mpp::VerificationFailedError.new(reason: "facilitator #{path} returned invalid JSON")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "types"
|
|
5
|
+
require "base64"
|
|
6
|
+
require "json"
|
|
7
|
+
|
|
8
|
+
module Mpp
|
|
9
|
+
module X402
|
|
10
|
+
# Base64 JSON codecs for x402 v2 headers.
|
|
11
|
+
#
|
|
12
|
+
# x402 uses standard (not url-safe) base64 of JSON, unlike Payment-auth
|
|
13
|
+
# which uses base64url.
|
|
14
|
+
module Header
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
sig { params(payment_required: T::Hash[T.untyped, T.untyped]).returns(String) }
|
|
20
|
+
def encode_payment_required(payment_required)
|
|
21
|
+
encode_json(payment_required)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { params(value: String).returns(T::Hash[T.untyped, T.untyped]) }
|
|
25
|
+
def decode_payment_required(value)
|
|
26
|
+
parsed = decode_json(value)
|
|
27
|
+
validate_payment_required!(parsed)
|
|
28
|
+
parsed
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { params(payment_payload: T::Hash[T.untyped, T.untyped]).returns(String) }
|
|
32
|
+
def encode_payment_signature(payment_payload)
|
|
33
|
+
encode_json(payment_payload)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { params(value: String).returns(T::Hash[T.untyped, T.untyped]) }
|
|
37
|
+
def decode_payment_signature(value)
|
|
38
|
+
parsed = decode_json(value)
|
|
39
|
+
validate_payment_payload!(parsed)
|
|
40
|
+
parsed
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { params(settle_response: T::Hash[T.untyped, T.untyped]).returns(String) }
|
|
44
|
+
def encode_payment_response(settle_response)
|
|
45
|
+
encode_json(settle_response)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig { params(value: String).returns(T::Hash[T.untyped, T.untyped]) }
|
|
49
|
+
def decode_payment_response(value)
|
|
50
|
+
parsed = decode_json(value)
|
|
51
|
+
validate_settle_response!(parsed)
|
|
52
|
+
parsed
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(obj: T.untyped).returns(String) }
|
|
56
|
+
def encode_json(obj)
|
|
57
|
+
json = Mpp::Json.compact_encode(obj)
|
|
58
|
+
Base64.strict_encode64(json)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { params(value: String).returns(T::Hash[T.untyped, T.untyped]) }
|
|
62
|
+
def decode_json(value)
|
|
63
|
+
json = decode_base64(value)
|
|
64
|
+
parsed = JSON.parse(json)
|
|
65
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless parsed.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
parsed
|
|
68
|
+
rescue JSON::ParserError
|
|
69
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
sig { params(value: String).returns(String) }
|
|
73
|
+
def decode_base64(value)
|
|
74
|
+
Base64.strict_decode64(value)
|
|
75
|
+
rescue ArgumentError
|
|
76
|
+
padded = value + ("=" * ((-value.length) % 4))
|
|
77
|
+
begin
|
|
78
|
+
Base64.urlsafe_decode64(padded)
|
|
79
|
+
rescue ArgumentError
|
|
80
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header."
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
private_class_method :decode_base64
|
|
84
|
+
|
|
85
|
+
sig { params(parsed: T::Hash[T.untyped, T.untyped]).void }
|
|
86
|
+
def validate_payment_required!(parsed)
|
|
87
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless parsed["x402Version"] == VERSION
|
|
88
|
+
accepts = parsed["accepts"]
|
|
89
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless accepts.is_a?(Array) && !accepts.empty?
|
|
90
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless parsed["resource"].is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
accepts.each { |item| validate_payment_requirements!(item) }
|
|
93
|
+
end
|
|
94
|
+
private_class_method :validate_payment_required!
|
|
95
|
+
|
|
96
|
+
sig { params(parsed: T::Hash[T.untyped, T.untyped]).void }
|
|
97
|
+
def validate_payment_payload!(parsed)
|
|
98
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless parsed["x402Version"] == VERSION
|
|
99
|
+
validate_payment_requirements!(parsed["accepted"])
|
|
100
|
+
payload = parsed["payload"]
|
|
101
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless payload.is_a?(Hash)
|
|
102
|
+
Kernel.raise Mpp::ParseError, "EVM charge only supports x402 EIP-3009 authorization payloads" unless payload.key?("authorization")
|
|
103
|
+
end
|
|
104
|
+
private_class_method :validate_payment_payload!
|
|
105
|
+
|
|
106
|
+
sig { params(parsed: T::Hash[T.untyped, T.untyped]).void }
|
|
107
|
+
def validate_settle_response!(parsed)
|
|
108
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless parsed.key?("success")
|
|
109
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless parsed["transaction"].is_a?(String)
|
|
110
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless parsed["network"].is_a?(String)
|
|
111
|
+
end
|
|
112
|
+
private_class_method :validate_settle_response!
|
|
113
|
+
|
|
114
|
+
sig { params(requirements: T.untyped).void }
|
|
115
|
+
def validate_payment_requirements!(requirements)
|
|
116
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless requirements.is_a?(Hash)
|
|
117
|
+
Kernel.raise Mpp::ParseError, "x402 exact requires scheme \"exact\"" unless requirements["scheme"] == SCHEME_EXACT
|
|
118
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless requirements["amount"].is_a?(String) && requirements["amount"].match?(/\A\d+\z/)
|
|
119
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless requirements["asset"].is_a?(String) && !requirements["asset"].empty?
|
|
120
|
+
Kernel.raise Mpp::ParseError, "Invalid EVM CAIP-2 network" unless requirements["network"].is_a?(String) && requirements["network"].start_with?(EVM_NETWORK_PREFIX)
|
|
121
|
+
Kernel.raise Mpp::ParseError, "Invalid base64 JSON header." unless requirements["payTo"].is_a?(String) && !requirements["payTo"].empty?
|
|
122
|
+
extra = requirements["extra"]
|
|
123
|
+
return if extra.nil?
|
|
124
|
+
|
|
125
|
+
transfer = extra["assetTransferMethod"]
|
|
126
|
+
Kernel.raise Mpp::ParseError, "x402 exact permit2 signing is not implemented" if transfer == "permit2"
|
|
127
|
+
end
|
|
128
|
+
private_class_method :validate_payment_requirements!
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|