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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +128 -1
  3. data/lib/mpp/challenge.rb +31 -8
  4. data/lib/mpp/challenge_echo.rb +14 -3
  5. data/lib/mpp/challenge_id.rb +30 -6
  6. data/lib/mpp/client/transport.rb +1 -1
  7. data/lib/mpp/errors.rb +22 -0
  8. data/lib/mpp/http/headers.rb +61 -0
  9. data/lib/mpp/http.rb +8 -0
  10. data/lib/mpp/methods/evm/assets.rb +103 -0
  11. data/lib/mpp/methods/evm/authorization.rb +140 -0
  12. data/lib/mpp/methods/evm/charge_intent.rb +129 -0
  13. data/lib/mpp/methods/evm/evm_method.rb +133 -0
  14. data/lib/mpp/methods/evm.rb +67 -0
  15. data/lib/mpp/methods/stripe/charge_intent.rb +10 -8
  16. data/lib/mpp/methods/stripe/crypto_payment_recorder.rb +55 -0
  17. data/lib/mpp/methods/stripe/defaults.rb +1 -0
  18. data/lib/mpp/methods/stripe/machine_payments.rb +130 -0
  19. data/lib/mpp/methods/stripe/stripe_method.rb +22 -3
  20. data/lib/mpp/methods/stripe.rb +23 -0
  21. data/lib/mpp/methods/tempo/attribution.rb +3 -9
  22. data/lib/mpp/methods/tempo/client_method.rb +35 -6
  23. data/lib/mpp/methods/tempo/fee_payer_client.rb +120 -0
  24. data/lib/mpp/methods/tempo/intents.rb +80 -48
  25. data/lib/mpp/methods/tempo/proof.rb +39 -12
  26. data/lib/mpp/methods/tempo/relay.rb +257 -0
  27. data/lib/mpp/methods/tempo.rb +2 -0
  28. data/lib/mpp/parsing.rb +22 -2
  29. data/lib/mpp/receipt.rb +4 -3
  30. data/lib/mpp/server/accept_payment.rb +194 -0
  31. data/lib/mpp/server/compose.rb +399 -0
  32. data/lib/mpp/server/decorator.rb +30 -7
  33. data/lib/mpp/server/method.rb +15 -0
  34. data/lib/mpp/server/middleware.rb +160 -18
  35. data/lib/mpp/server/mpp_handler.rb +322 -34
  36. data/lib/mpp/server/result.rb +100 -0
  37. data/lib/mpp/server/verify.rb +24 -14
  38. data/lib/mpp/server.rb +4 -0
  39. data/lib/mpp/version.rb +1 -1
  40. data/lib/mpp/x402/facilitator.rb +127 -0
  41. data/lib/mpp/x402/header.rb +131 -0
  42. data/lib/mpp/x402/server.rb +247 -0
  43. data/lib/mpp/x402/types.rb +33 -0
  44. data/lib/mpp/x402.rb +12 -0
  45. data/lib/mpp.rb +16 -3
  46. 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
@@ -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
- sig { params(dispatcher: Mpp::Events::Dispatcher, challenge: T.untyped, credential: T.untyped, method: T::Hash[Symbol, T.untyped], receipt: T.untyped, request: T::Hash[String, T.untyped]).void }
262
- def emit_payment_success(dispatcher:, challenge:, credential:, method:, receipt:, request:)
263
- return unless dispatcher.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
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
- dispatcher.emit(Mpp::Events::PAYMENT_SUCCESS, {
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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Mpp
5
- VERSION = "0.1.4"
5
+ VERSION = "0.1.6"
6
6
  end
@@ -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