mpp-rb 0.1.5 → 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 +65 -0
- 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/methods/evm/evm_method.rb +18 -2
- data/lib/mpp/methods/evm.rb +5 -2
- data/lib/mpp/methods/stripe/charge_intent.rb +6 -7
- 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/client_method.rb +22 -4
- data/lib/mpp/methods/tempo/intents.rb +53 -41
- data/lib/mpp/parsing.rb +20 -2
- data/lib/mpp/server/compose.rb +22 -10
- data/lib/mpp/server/decorator.rb +3 -1
- data/lib/mpp/server/method.rb +15 -0
- data/lib/mpp/server/middleware.rb +16 -2
- data/lib/mpp/server/mpp_handler.rb +66 -12
- data/lib/mpp/server/verify.rb +24 -14
- data/lib/mpp/version.rb +1 -1
- data/lib/mpp.rb +13 -3
- metadata +17 -1
|
@@ -19,14 +19,21 @@ module Mpp
|
|
|
19
19
|
class TempoMethod
|
|
20
20
|
attr_reader :name, :account, :fee_payer, :fee_payer_allowed_fee_tokens,
|
|
21
21
|
:root_account, :rpc_url, :chain_id, :currency, :recipient, :decimals, :client_id,
|
|
22
|
-
:expected_recipients, :relay
|
|
22
|
+
:expected_recipients, :relay, :on_payment_success
|
|
23
23
|
attr_accessor :intents
|
|
24
24
|
|
|
25
25
|
def initialize(account: nil, fee_payer: nil, root_account: nil,
|
|
26
26
|
rpc_url: Defaults::RPC_URL, chain_id: nil, currency: nil,
|
|
27
27
|
recipient: nil, decimals: 6, client_id: nil,
|
|
28
28
|
expected_recipients: nil, fee_payer_allowed_fee_tokens: nil,
|
|
29
|
-
relay: nil)
|
|
29
|
+
relay: nil, on_payment_success: nil, can_offer: nil)
|
|
30
|
+
unless on_payment_success.nil? || on_payment_success.respond_to?(:call)
|
|
31
|
+
raise ArgumentError, "on_payment_success must be callable"
|
|
32
|
+
end
|
|
33
|
+
if !can_offer.nil? && !can_offer.respond_to?(:call)
|
|
34
|
+
raise ArgumentError, "can_offer must be callable"
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
@name = "tempo"
|
|
31
38
|
@account = account
|
|
32
39
|
@fee_payer = fee_payer
|
|
@@ -41,9 +48,17 @@ module Mpp
|
|
|
41
48
|
@decimals = decimals
|
|
42
49
|
@client_id = client_id
|
|
43
50
|
@expected_recipients = expected_recipients&.map(&:downcase)&.to_set
|
|
51
|
+
@on_payment_success = on_payment_success
|
|
52
|
+
@can_offer = can_offer
|
|
44
53
|
@intents = {}
|
|
45
54
|
end
|
|
46
55
|
|
|
56
|
+
def can_offer?(request)
|
|
57
|
+
return true unless @can_offer
|
|
58
|
+
|
|
59
|
+
@can_offer.call(request)
|
|
60
|
+
end
|
|
61
|
+
|
|
47
62
|
# Create a credential to satisfy the given challenge.
|
|
48
63
|
#
|
|
49
64
|
# mode: :pull (default) — return signed transaction for server to broadcast
|
|
@@ -251,7 +266,8 @@ module Mpp
|
|
|
251
266
|
# Factory function to create a configured TempoMethod.
|
|
252
267
|
def self.tempo(intents:, account: nil, fee_payer: nil, chain_id: nil, rpc_url: nil,
|
|
253
268
|
root_account: nil, currency: nil, recipient: nil, decimals: 6, client_id: nil,
|
|
254
|
-
expected_recipients: nil, fee_payer_allowed_fee_tokens: nil, relay: nil
|
|
269
|
+
expected_recipients: nil, fee_payer_allowed_fee_tokens: nil, relay: nil,
|
|
270
|
+
on_payment_success: nil, can_offer: nil)
|
|
255
271
|
rpc_url ||= chain_id ? Defaults.rpc_url_for_chain(chain_id) : Defaults::RPC_URL
|
|
256
272
|
currency ||= Defaults.default_currency_for_chain(chain_id)
|
|
257
273
|
|
|
@@ -273,7 +289,9 @@ module Mpp
|
|
|
273
289
|
client_id: client_id,
|
|
274
290
|
expected_recipients: expected_recipients,
|
|
275
291
|
fee_payer_allowed_fee_tokens: fee_payer_allowed_fee_tokens,
|
|
276
|
-
relay: Relay.resolve_optional(relay)
|
|
292
|
+
relay: Relay.resolve_optional(relay),
|
|
293
|
+
on_payment_success: on_payment_success,
|
|
294
|
+
can_offer: can_offer
|
|
277
295
|
)
|
|
278
296
|
|
|
279
297
|
intents.each_value do |intent|
|
|
@@ -22,7 +22,9 @@ module Mpp
|
|
|
22
22
|
attr_reader :name
|
|
23
23
|
attr_accessor :rpc_url
|
|
24
24
|
|
|
25
|
-
def initialize(chain_id: nil, rpc_url: nil, timeout: 30, store:
|
|
25
|
+
def initialize(chain_id: nil, rpc_url: nil, timeout: 30, store: Mpp::MemoryStore.new, validate_sender: nil)
|
|
26
|
+
raise ArgumentError, "store is required" if store.nil?
|
|
27
|
+
|
|
26
28
|
@name = "charge"
|
|
27
29
|
@rpc_url = rpc_url || (chain_id ? Defaults.rpc_url_for_chain(chain_id) : nil)
|
|
28
30
|
@_method = nil
|
|
@@ -111,11 +113,9 @@ module Mpp
|
|
|
111
113
|
# Validate the source before reserving the hash.
|
|
112
114
|
source_address = parse_hash_credential_source(credential.source, request.method_details.chain_id)
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
payload.hash)
|
|
118
|
-
end
|
|
116
|
+
store_key = "mpp:charge:#{payload.hash.downcase}"
|
|
117
|
+
raise Mpp::VerificationError, "Transaction hash already used" unless @store.put_if_absent(store_key,
|
|
118
|
+
payload.hash)
|
|
119
119
|
|
|
120
120
|
rpc_url = get_rpc_url
|
|
121
121
|
result = Rpc.call(rpc_url, "eth_getTransactionReceipt", [payload.hash])
|
|
@@ -158,19 +158,15 @@ module Mpp
|
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
rpc_url = get_rpc_url
|
|
161
|
-
reserved_tx_hash =
|
|
162
|
-
store_key =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
store_key = "mpp:charge:#{reserved_tx_hash.downcase}"
|
|
166
|
-
unless @store.put_if_absent(store_key, TRANSACTION_PENDING)
|
|
167
|
-
raise Mpp::VerificationError, "Transaction hash already used" unless @store.get(store_key) == TRANSACTION_PENDING
|
|
161
|
+
reserved_tx_hash = raw_transaction_hash(raw_tx)
|
|
162
|
+
store_key = "mpp:charge:#{reserved_tx_hash.downcase}"
|
|
163
|
+
unless @store.put_if_absent(store_key, TRANSACTION_PENDING)
|
|
164
|
+
raise Mpp::VerificationError, "Transaction hash already used" unless @store.get(store_key) == TRANSACTION_PENDING
|
|
168
165
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
end
|
|
166
|
+
receipt_data = fetch_transaction_receipt(rpc_url, reserved_tx_hash)
|
|
167
|
+
verify_transaction_receipt!(receipt_data, request, credential: credential)
|
|
168
|
+
@store.put(store_key, TRANSACTION_VERIFIED)
|
|
169
|
+
return Mpp::Receipt.success(reserved_tx_hash)
|
|
174
170
|
end
|
|
175
171
|
|
|
176
172
|
# We pay the gas, so simulate the co-signed tx first and bail if it
|
|
@@ -178,7 +174,7 @@ module Mpp
|
|
|
178
174
|
begin
|
|
179
175
|
simulate_before_broadcast(simulate_payload, rpc_url) if simulate_payload
|
|
180
176
|
rescue
|
|
181
|
-
@store.delete(store_key)
|
|
177
|
+
@store.delete(store_key)
|
|
182
178
|
raise
|
|
183
179
|
end
|
|
184
180
|
|
|
@@ -186,33 +182,36 @@ module Mpp
|
|
|
186
182
|
begin
|
|
187
183
|
tx_hash = Rpc.call(rpc_url, "eth_sendRawTransaction", [raw_tx])
|
|
188
184
|
rescue => e
|
|
189
|
-
if
|
|
185
|
+
if transaction_submission_may_have_succeeded?(e)
|
|
190
186
|
receipt_data = fetch_transaction_receipt(rpc_url, reserved_tx_hash)
|
|
191
187
|
verify_transaction_receipt!(receipt_data, request, credential: credential)
|
|
192
|
-
@store
|
|
188
|
+
@store.put(store_key, TRANSACTION_VERIFIED)
|
|
193
189
|
return Mpp::Receipt.success(reserved_tx_hash)
|
|
194
190
|
end
|
|
195
191
|
|
|
196
|
-
@store.delete(store_key)
|
|
192
|
+
@store.delete(store_key)
|
|
197
193
|
raise Mpp::VerificationError, "Transaction submission failed: #{e.message}"
|
|
198
194
|
end
|
|
199
195
|
|
|
200
196
|
unless tx_hash
|
|
201
|
-
@store.delete(store_key)
|
|
197
|
+
@store.delete(store_key)
|
|
202
198
|
raise Mpp::VerificationError, "No transaction hash returned"
|
|
203
199
|
end
|
|
204
200
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
201
|
+
# Tempo nodes hash canonical RLP, which can differ from keccak(raw
|
|
202
|
+
# bytes). Replay keys must follow the chain hash so a later hash
|
|
203
|
+
# credential for the same payment is rejected. Keep the raw-hash
|
|
204
|
+
# claim until verification finishes so a concurrent retry cannot
|
|
205
|
+
# rebroadcast the same payload.
|
|
206
|
+
raw_store_key = store_key
|
|
207
|
+
store_key, reserved_tx_hash = rebase_reservation_to_chain_hash(store_key, reserved_tx_hash, tx_hash)
|
|
209
208
|
|
|
210
|
-
|
|
211
|
-
receipt_data = fetch_transaction_receipt(rpc_url, tx_hash)
|
|
209
|
+
receipt_data = fetch_transaction_receipt(rpc_url, reserved_tx_hash)
|
|
212
210
|
verify_transaction_receipt!(receipt_data, request, credential: credential)
|
|
213
|
-
@store.put(store_key, TRANSACTION_VERIFIED)
|
|
211
|
+
@store.put(store_key, TRANSACTION_VERIFIED)
|
|
212
|
+
@store.put(raw_store_key, TRANSACTION_VERIFIED) if raw_store_key != store_key
|
|
214
213
|
|
|
215
|
-
Mpp::Receipt.success(
|
|
214
|
+
Mpp::Receipt.success(reserved_tx_hash)
|
|
216
215
|
end
|
|
217
216
|
|
|
218
217
|
def transaction_submission_may_have_succeeded?(error)
|
|
@@ -389,17 +388,32 @@ module Mpp
|
|
|
389
388
|
end
|
|
390
389
|
|
|
391
390
|
def raw_transaction_hash(raw_tx)
|
|
392
|
-
Kernel.require "eth"
|
|
393
|
-
|
|
394
391
|
hex = raw_tx.delete_prefix("0x")
|
|
395
392
|
unless hex.match?(/\A[0-9a-fA-F]+\z/) && hex.length.even?
|
|
396
393
|
raise Mpp::VerificationError, "Invalid transaction signature"
|
|
397
394
|
end
|
|
398
395
|
|
|
399
|
-
"0x#{
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
396
|
+
"0x#{Attribution.keccak256([hex].pack("H*")).unpack1("H*")}"
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def normalize_tx_hash(tx_hash)
|
|
400
|
+
hex = tx_hash.to_s.downcase
|
|
401
|
+
hex.start_with?("0x") ? hex : "0x#{hex}"
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def rebase_reservation_to_chain_hash(store_key, reserved_tx_hash, returned_tx_hash)
|
|
405
|
+
canonical = normalize_tx_hash(returned_tx_hash)
|
|
406
|
+
return [store_key, reserved_tx_hash] if canonical == reserved_tx_hash.downcase
|
|
407
|
+
|
|
408
|
+
canonical_key = "mpp:charge:#{canonical}"
|
|
409
|
+
unless @store.put_if_absent(canonical_key, TRANSACTION_PENDING)
|
|
410
|
+
unless @store.get(canonical_key) == TRANSACTION_PENDING
|
|
411
|
+
@store.put(store_key, TRANSACTION_VERIFIED)
|
|
412
|
+
raise Mpp::VerificationError, "Transaction hash already used"
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
[canonical_key, canonical]
|
|
403
417
|
end
|
|
404
418
|
|
|
405
419
|
def match_transfer_calldata(call_data_hex, request, challenge: nil)
|
|
@@ -455,10 +469,8 @@ module Mpp
|
|
|
455
469
|
)
|
|
456
470
|
raise Mpp::VerificationError, "Proof signature does not match source" unless valid
|
|
457
471
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
raise Mpp::VerificationError, "Proof credential has already been used" unless @store.put_if_absent(store_key, true)
|
|
461
|
-
end
|
|
472
|
+
store_key = "mpp:proof:#{credential.challenge.id}"
|
|
473
|
+
raise Mpp::VerificationError, "Proof credential has already been used" unless @store.put_if_absent(store_key, true)
|
|
462
474
|
|
|
463
475
|
Mpp::Receipt.success(credential.challenge.id)
|
|
464
476
|
end
|
data/lib/mpp/parsing.rb
CHANGED
|
@@ -101,6 +101,12 @@ module Mpp
|
|
|
101
101
|
opaque_b64 = params["opaque"]
|
|
102
102
|
opaque = (opaque_b64 && !opaque_b64.empty?) ? b64_decode(opaque_b64) : nil
|
|
103
103
|
|
|
104
|
+
header = begin
|
|
105
|
+
Mpp.advertised_credential_header(params["header"])
|
|
106
|
+
rescue ArgumentError => e
|
|
107
|
+
Kernel.raise Mpp::ParseError, e.message
|
|
108
|
+
end
|
|
109
|
+
|
|
104
110
|
Mpp::Challenge.new(
|
|
105
111
|
id: id,
|
|
106
112
|
method: method,
|
|
@@ -111,7 +117,8 @@ module Mpp
|
|
|
111
117
|
digest: params["digest"],
|
|
112
118
|
expires: params["expires"],
|
|
113
119
|
description: params["description"],
|
|
114
|
-
opaque: opaque
|
|
120
|
+
opaque: opaque,
|
|
121
|
+
header: header
|
|
115
122
|
)
|
|
116
123
|
end
|
|
117
124
|
|
|
@@ -131,6 +138,9 @@ module Mpp
|
|
|
131
138
|
parts << "digest=\"#{escape_quoted(challenge.digest)}\"" if challenge.digest
|
|
132
139
|
parts << "expires=\"#{escape_quoted(challenge.expires)}\"" if challenge.expires
|
|
133
140
|
parts << "description=\"#{escape_quoted(challenge.description)}\"" if challenge.description
|
|
141
|
+
if challenge.header
|
|
142
|
+
parts << "header=\"#{escape_quoted(challenge.header)}\""
|
|
143
|
+
end
|
|
134
144
|
if challenge.opaque
|
|
135
145
|
opaque_b64 = b64_encode(challenge.opaque)
|
|
136
146
|
parts << "opaque=\"#{opaque_b64}\""
|
|
@@ -158,6 +168,12 @@ module Mpp
|
|
|
158
168
|
method = challenge_data["method"]
|
|
159
169
|
validate_payment_method_id(method)
|
|
160
170
|
|
|
171
|
+
header = begin
|
|
172
|
+
Mpp.advertised_credential_header(challenge_data["header"]&.to_s)
|
|
173
|
+
rescue ArgumentError => e
|
|
174
|
+
Kernel.raise Mpp::ParseError, e.message
|
|
175
|
+
end
|
|
176
|
+
|
|
161
177
|
echo = Mpp::ChallengeEcho.new(
|
|
162
178
|
id: challenge_data["id"].to_s,
|
|
163
179
|
realm: (challenge_data["realm"] || "").to_s,
|
|
@@ -166,7 +182,8 @@ module Mpp
|
|
|
166
182
|
request: (challenge_data["request"] || "").to_s,
|
|
167
183
|
expires: challenge_data["expires"]&.to_s,
|
|
168
184
|
digest: challenge_data["digest"]&.to_s,
|
|
169
|
-
opaque: challenge_data["opaque"]&.to_s
|
|
185
|
+
opaque: challenge_data["opaque"]&.to_s,
|
|
186
|
+
header: header
|
|
170
187
|
)
|
|
171
188
|
|
|
172
189
|
Mpp::Credential.new(
|
|
@@ -188,6 +205,7 @@ module Mpp
|
|
|
188
205
|
}
|
|
189
206
|
challenge_dict["expires"] = credential.challenge.expires if credential.challenge.expires
|
|
190
207
|
challenge_dict["digest"] = credential.challenge.digest if credential.challenge.digest
|
|
208
|
+
challenge_dict["header"] = credential.challenge.header if credential.challenge.header
|
|
191
209
|
challenge_dict["opaque"] = credential.challenge.opaque if credential.challenge.opaque
|
|
192
210
|
|
|
193
211
|
payload = {
|
data/lib/mpp/server/compose.rb
CHANGED
|
@@ -42,8 +42,8 @@ module Mpp
|
|
|
42
42
|
@canonical_request ||= @handler.build_charge_request(@method, amount, **charge_kwargs)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
sig { params(authorization: T.nilable(String), payment_signature: T.nilable(String), body: T.untyped, url: T.nilable(String), http_method: T.nilable(String)).returns(T.untyped) }
|
|
46
|
-
def verify(authorization: nil, payment_signature: nil, body: nil, url: nil, http_method: nil)
|
|
45
|
+
sig { params(authorization: T.nilable(String), payment_signature: T.nilable(String), body: T.untyped, url: T.nilable(String), http_method: T.nilable(String), request_guard: T.nilable(T.proc.params(request: T::Hash[String, T.untyped]).returns(T::Boolean))).returns(T.untyped) }
|
|
46
|
+
def verify(authorization: nil, payment_signature: nil, body: nil, url: nil, http_method: nil, &request_guard)
|
|
47
47
|
@handler.charge_one(
|
|
48
48
|
@method,
|
|
49
49
|
authorization,
|
|
@@ -52,13 +52,17 @@ module Mpp
|
|
|
52
52
|
url: url,
|
|
53
53
|
body: body,
|
|
54
54
|
http_method: http_method,
|
|
55
|
-
**charge_kwargs
|
|
55
|
+
**charge_kwargs,
|
|
56
|
+
&request_guard
|
|
56
57
|
)
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
sig { params(body: T.untyped, url: T.nilable(String), http_method: T.nilable(String)).returns(Mpp::Challenge) }
|
|
60
|
+
sig { params(body: T.untyped, url: T.nilable(String), http_method: T.nilable(String)).returns(T.nilable(Mpp::Challenge)) }
|
|
60
61
|
def challenge(body: nil, url: nil, http_method: nil)
|
|
61
|
-
result = verify(body: body, url: url, http_method: http_method)
|
|
62
|
+
result = verify(body: body, url: url, http_method: http_method) do |request|
|
|
63
|
+
Mpp::Server::MethodHelper.can_offer?(@method, request)
|
|
64
|
+
end
|
|
65
|
+
return nil if result.nil?
|
|
62
66
|
return result if result.is_a?(Mpp::Challenge)
|
|
63
67
|
|
|
64
68
|
Kernel.raise "expected a challenge from unpaid offer #{key}"
|
|
@@ -68,7 +72,7 @@ module Mpp
|
|
|
68
72
|
def decorate_challenge(headers, challenge, url: nil, http_method: nil)
|
|
69
73
|
return headers unless @method.respond_to?(:decorate_challenge)
|
|
70
74
|
|
|
71
|
-
@method.decorate_challenge(headers, challenge, url: url, http_method: http_method, request:
|
|
75
|
+
@method.decorate_challenge(headers, challenge, url: url, http_method: http_method, request: challenge.request)
|
|
72
76
|
headers
|
|
73
77
|
end
|
|
74
78
|
|
|
@@ -100,7 +104,7 @@ module Mpp
|
|
|
100
104
|
@options[:amount].to_s
|
|
101
105
|
end
|
|
102
106
|
|
|
103
|
-
REQUEST_OPTION_KEYS = [:body, :url, :payment_signature, :accept_payment, :authorization, :http_method].freeze
|
|
107
|
+
REQUEST_OPTION_KEYS = [:body, :url, :payment_signature, :accept_payment, :authorization, :http_method, :payment_authorization].freeze
|
|
104
108
|
|
|
105
109
|
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
106
110
|
def charge_kwargs
|
|
@@ -159,6 +163,7 @@ module Mpp
|
|
|
159
163
|
sig do
|
|
160
164
|
params(
|
|
161
165
|
authorization: T.nilable(String),
|
|
166
|
+
payment_authorization: T.nilable(String),
|
|
162
167
|
payment_signature: T.nilable(String),
|
|
163
168
|
body: T.untyped,
|
|
164
169
|
url: T.nilable(String),
|
|
@@ -167,10 +172,11 @@ module Mpp
|
|
|
167
172
|
scope: T.nilable(T::Hash[String, String])
|
|
168
173
|
).returns(ComposedResult)
|
|
169
174
|
end
|
|
170
|
-
def call(authorization: nil, payment_signature: nil, body: nil, url: nil, accept_payment: nil, http_method: nil, scope: nil)
|
|
175
|
+
def call(authorization: nil, payment_authorization: nil, payment_signature: nil, body: nil, url: nil, accept_payment: nil, http_method: nil, scope: nil)
|
|
171
176
|
if scope && !scope.empty?
|
|
172
177
|
return self.class.new(handler: @handler, offers: @offers.map { |offer| offer.with_scope(scope) }).call(
|
|
173
178
|
authorization: authorization,
|
|
179
|
+
payment_authorization: payment_authorization,
|
|
174
180
|
payment_signature: payment_signature,
|
|
175
181
|
body: body,
|
|
176
182
|
url: url,
|
|
@@ -179,8 +185,9 @@ module Mpp
|
|
|
179
185
|
)
|
|
180
186
|
end
|
|
181
187
|
|
|
188
|
+
credential = @handler.payment_credential_value(authorization, payment_authorization)
|
|
182
189
|
dispatched = dispatch_credential(
|
|
183
|
-
authorization:
|
|
190
|
+
authorization: credential,
|
|
184
191
|
payment_signature: payment_signature,
|
|
185
192
|
body: body,
|
|
186
193
|
url: url,
|
|
@@ -330,10 +337,15 @@ module Mpp
|
|
|
330
337
|
).returns(ComposedResult)
|
|
331
338
|
end
|
|
332
339
|
def merge_challenges(body:, url:, accept_payment:, http_method:)
|
|
333
|
-
pairs = @offers.
|
|
340
|
+
pairs = @offers.filter_map do |offer|
|
|
334
341
|
challenge = offer.challenge(body: body, url: url, http_method: http_method)
|
|
342
|
+
next if challenge.nil?
|
|
343
|
+
|
|
335
344
|
[offer, challenge]
|
|
336
345
|
end
|
|
346
|
+
if pairs.empty?
|
|
347
|
+
Kernel.raise ArgumentError, "No payment offers are available for this request"
|
|
348
|
+
end
|
|
337
349
|
|
|
338
350
|
ranked = AcceptPayment.apply(pairs.map { |_offer, challenge| challenge }, accept_payment)
|
|
339
351
|
by_id = pairs.to_h { |offer, challenge| [challenge.id, [offer, challenge]] }
|
data/lib/mpp/server/decorator.rb
CHANGED
|
@@ -38,9 +38,11 @@ module Mpp
|
|
|
38
38
|
|
|
39
39
|
headers[key] = value
|
|
40
40
|
end
|
|
41
|
+
credential_headers = challenges.map(&:credential_header).uniq
|
|
42
|
+
vary = extra_headers.key?("PAYMENT-REQUIRED") ? credential_headers + ["PAYMENT-SIGNATURE"] : credential_headers
|
|
41
43
|
Mpp::Server::Middleware.mark_authorization_bound_response(
|
|
42
44
|
headers,
|
|
43
|
-
vary:
|
|
45
|
+
vary: vary
|
|
44
46
|
)
|
|
45
47
|
{
|
|
46
48
|
"_mpp_challenge" => true,
|
data/lib/mpp/server/method.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Mpp
|
|
|
7
7
|
# name -> String
|
|
8
8
|
# intents -> Hash[String, Intent]
|
|
9
9
|
# create_credential(challenge) -> Credential
|
|
10
|
+
# on_payment_success -> optional callable receiving a payment.success payload
|
|
10
11
|
|
|
11
12
|
module MethodHelper
|
|
12
13
|
extend T::Sig
|
|
@@ -22,6 +23,20 @@ module Mpp
|
|
|
22
23
|
request
|
|
23
24
|
end
|
|
24
25
|
end
|
|
26
|
+
|
|
27
|
+
# Check whether a method should be advertised for a canonical request.
|
|
28
|
+
# This only governs composing new 402 offers, never credential redemption.
|
|
29
|
+
sig { params(method: T.untyped, request: T::Hash[String, T.untyped]).returns(T::Boolean) }
|
|
30
|
+
def can_offer?(method, request)
|
|
31
|
+
return true unless method.respond_to?(:can_offer?)
|
|
32
|
+
|
|
33
|
+
available = method.can_offer?(request)
|
|
34
|
+
unless available == true || available == false
|
|
35
|
+
Kernel.raise ArgumentError, "can_offer? must return true or false"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
available
|
|
39
|
+
end
|
|
25
40
|
end
|
|
26
41
|
end
|
|
27
42
|
end
|
|
@@ -37,6 +37,7 @@ module Mpp
|
|
|
37
37
|
return @app.call(env) unless charge_opts
|
|
38
38
|
|
|
39
39
|
authorization = env["HTTP_AUTHORIZATION"]
|
|
40
|
+
payment_authorization = env["HTTP_PAYMENT_AUTHORIZATION"]
|
|
40
41
|
payment_signature = env["HTTP_PAYMENT_SIGNATURE"]
|
|
41
42
|
accept_payment = env["HTTP_ACCEPT_PAYMENT"]
|
|
42
43
|
http_method = env["REQUEST_METHOD"]
|
|
@@ -50,6 +51,7 @@ module Mpp
|
|
|
50
51
|
env,
|
|
51
52
|
charge_opts,
|
|
52
53
|
authorization: authorization,
|
|
54
|
+
payment_authorization: payment_authorization,
|
|
53
55
|
payment_signature: payment_signature,
|
|
54
56
|
accept_payment: accept_payment,
|
|
55
57
|
http_method: http_method,
|
|
@@ -67,7 +69,7 @@ module Mpp
|
|
|
67
69
|
extra_headers.each { |key, value| headers[key] = value unless value.nil? }
|
|
68
70
|
decorate_single_method_receipt(headers, credential, receipt, payment_signature)
|
|
69
71
|
end
|
|
70
|
-
vary = [
|
|
72
|
+
vary = [credential_vary_field]
|
|
71
73
|
vary << "PAYMENT-SIGNATURE" if x402_bound?(headers, payment_signature)
|
|
72
74
|
self.class.mark_authorization_bound_response(headers, vary: vary)
|
|
73
75
|
|
|
@@ -99,6 +101,7 @@ module Mpp
|
|
|
99
101
|
env: T.untyped,
|
|
100
102
|
charge_opts: T.untyped,
|
|
101
103
|
authorization: T.untyped,
|
|
104
|
+
payment_authorization: T.untyped,
|
|
102
105
|
payment_signature: T.untyped,
|
|
103
106
|
accept_payment: T.untyped,
|
|
104
107
|
http_method: T.untyped,
|
|
@@ -106,10 +109,11 @@ module Mpp
|
|
|
106
109
|
request_body: T.untyped
|
|
107
110
|
).returns(T.untyped)
|
|
108
111
|
end
|
|
109
|
-
def verify_payment(env, charge_opts, authorization:, payment_signature:, accept_payment:, http_method:, url:, request_body:)
|
|
112
|
+
def verify_payment(env, charge_opts, authorization:, payment_authorization:, payment_signature:, accept_payment:, http_method:, url:, request_body:)
|
|
110
113
|
if @handler.is_a?(Mpp::Server::ComposedHandler)
|
|
111
114
|
return @handler.call(
|
|
112
115
|
authorization: authorization,
|
|
116
|
+
payment_authorization: payment_authorization,
|
|
113
117
|
payment_signature: payment_signature,
|
|
114
118
|
body: request_body,
|
|
115
119
|
url: url,
|
|
@@ -127,6 +131,7 @@ module Mpp
|
|
|
127
131
|
amount,
|
|
128
132
|
**opts,
|
|
129
133
|
body: request_body,
|
|
134
|
+
payment_authorization: payment_authorization,
|
|
130
135
|
payment_signature: payment_signature,
|
|
131
136
|
url: url,
|
|
132
137
|
accept_payment: accept_payment,
|
|
@@ -188,6 +193,15 @@ module Mpp
|
|
|
188
193
|
headers.key?("PAYMENT-REQUIRED")
|
|
189
194
|
end
|
|
190
195
|
|
|
196
|
+
sig { returns(String) }
|
|
197
|
+
def credential_vary_field
|
|
198
|
+
if @handler.respond_to?(:requires_auth) && @handler.requires_auth
|
|
199
|
+
Mpp::PAYMENT_AUTHORIZATION_HEADER
|
|
200
|
+
else
|
|
201
|
+
Mpp::AUTHORIZATION_HEADER
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
191
205
|
sig { params(env: T.untyped).returns(String) }
|
|
192
206
|
def request_url(env)
|
|
193
207
|
scheme = env["HTTP_X_FORWARDED_PROTO"] || env["rack.url_scheme"] || "http"
|