mpp-rb 0.1.1 → 0.1.3
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 +37 -5
- data/lib/mpp/body_digest.rb +1 -1
- data/lib/mpp/challenge.rb +71 -6
- data/lib/mpp/client/transport.rb +153 -18
- data/lib/mpp/errors.rb +3 -0
- data/lib/mpp/events.rb +124 -0
- data/lib/mpp/extensions/mcp/types.rb +2 -1
- data/lib/mpp/methods/stripe/charge_intent.rb +41 -16
- data/lib/mpp/methods/stripe/client_method.rb +5 -1
- data/lib/mpp/methods/stripe/stripe_method.rb +15 -4
- data/lib/mpp/methods/tempo/attribution.rb +13 -0
- data/lib/mpp/methods/tempo/client_method.rb +28 -8
- data/lib/mpp/methods/tempo/fee_payer_envelope.rb +21 -6
- data/lib/mpp/methods/tempo/fee_payer_policy.rb +45 -0
- data/lib/mpp/methods/tempo/intents.rb +427 -69
- data/lib/mpp/methods/tempo/proof.rb +19 -15
- data/lib/mpp/methods/tempo/transaction.rb +68 -10
- data/lib/mpp/methods/tempo.rb +1 -0
- data/lib/mpp/parsing.rb +14 -2
- data/lib/mpp/receipt.rb +3 -2
- data/lib/mpp/server/decorator.rb +1 -0
- data/lib/mpp/server/middleware.rb +101 -2
- data/lib/mpp/server/mpp_handler.rb +40 -8
- data/lib/mpp/server/verify.rb +173 -27
- data/lib/mpp/version.rb +1 -1
- data/lib/mpp.rb +5 -3
- metadata +5 -7
|
@@ -6,16 +6,16 @@ module Mpp
|
|
|
6
6
|
module Tempo
|
|
7
7
|
# EIP-712 proof credentials for zero-amount challenges.
|
|
8
8
|
#
|
|
9
|
-
# Domain: { name: "MPP", version: "
|
|
10
|
-
# Types: { Proof: [{ name: "challengeId", type: "string" }] }
|
|
11
|
-
# Message: { challengeId: <challenge.id> }
|
|
9
|
+
# Domain: { name: "MPP", version: "2", chainId }
|
|
10
|
+
# Types: { Proof: [{ name: "challengeId", type: "string" }, { name: "realm", type: "string" }] }
|
|
11
|
+
# Message: { challengeId: <challenge.id>, realm: <challenge.realm> }
|
|
12
12
|
module Proof
|
|
13
13
|
DOMAIN_NAME = "MPP"
|
|
14
|
-
DOMAIN_VERSION = "
|
|
14
|
+
DOMAIN_VERSION = "2"
|
|
15
15
|
|
|
16
16
|
# EIP-712 domain separator type hash
|
|
17
17
|
DOMAIN_TYPE_HASH = "EIP712Domain(string name,string version,uint256 chainId)"
|
|
18
|
-
PROOF_TYPE_HASH = "Proof(string challengeId)"
|
|
18
|
+
PROOF_TYPE_HASH = "Proof(string challengeId,string realm)"
|
|
19
19
|
|
|
20
20
|
module_function
|
|
21
21
|
|
|
@@ -36,35 +36,36 @@ module Mpp
|
|
|
36
36
|
)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# Compute the EIP-712 struct hash for Proof(challengeId).
|
|
40
|
-
def struct_hash(challenge_id)
|
|
39
|
+
# Compute the EIP-712 struct hash for Proof(challengeId, realm).
|
|
40
|
+
def struct_hash(challenge_id, realm)
|
|
41
41
|
keccak256(
|
|
42
42
|
abi_encode(
|
|
43
43
|
keccak256(PROOF_TYPE_HASH),
|
|
44
|
-
keccak256(challenge_id)
|
|
44
|
+
keccak256(challenge_id),
|
|
45
|
+
keccak256(realm)
|
|
45
46
|
)
|
|
46
47
|
)
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# Compute the full EIP-712 signing hash.
|
|
50
|
-
def signing_hash(chain_id:, challenge_id:)
|
|
51
|
+
def signing_hash(chain_id:, challenge_id:, realm:)
|
|
51
52
|
keccak256(
|
|
52
|
-
"\x19\x01".b + domain_separator(chain_id) + struct_hash(challenge_id)
|
|
53
|
+
"\x19\x01".b + domain_separator(chain_id) + struct_hash(challenge_id, realm)
|
|
53
54
|
)
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
# Sign a proof credential (client-side).
|
|
57
|
-
def sign(account:, chain_id:, challenge_id:)
|
|
58
|
-
hash = signing_hash(chain_id: chain_id, challenge_id: challenge_id)
|
|
58
|
+
def sign(account:, chain_id:, challenge_id:, realm:)
|
|
59
|
+
hash = signing_hash(chain_id: chain_id, challenge_id: challenge_id, realm: realm)
|
|
59
60
|
sig = account.sign_hash(hash)
|
|
60
61
|
"0x#{sig.unpack1("H*")}"
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
# Verify a proof credential signature (server-side).
|
|
64
|
-
def verify(address:, chain_id:, challenge_id:, signature:)
|
|
65
|
+
def verify(address:, chain_id:, challenge_id:, realm:, signature:)
|
|
65
66
|
Kernel.require "eth"
|
|
66
67
|
|
|
67
|
-
hash = signing_hash(chain_id: chain_id, challenge_id: challenge_id)
|
|
68
|
+
hash = signing_hash(chain_id: chain_id, challenge_id: challenge_id, realm: realm)
|
|
68
69
|
sig_bytes = [signature.delete_prefix("0x")].pack("H*")
|
|
69
70
|
|
|
70
71
|
# Recover the signer address from the signature
|
|
@@ -106,7 +107,10 @@ module Mpp
|
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
def uint256(value)
|
|
109
|
-
|
|
110
|
+
value = Integer(value)
|
|
111
|
+
raise ArgumentError, "uint256 out of range" if value.negative? || value >= (1 << 256)
|
|
112
|
+
|
|
113
|
+
[value.to_s(16).rjust(64, "0")].pack("H*")
|
|
110
114
|
end
|
|
111
115
|
|
|
112
116
|
def recover_address(hash, sig_bytes)
|
|
@@ -45,29 +45,50 @@ module Mpp
|
|
|
45
45
|
require_eth!
|
|
46
46
|
require_rlp!
|
|
47
47
|
|
|
48
|
-
Eth::Util.keccak256([TYPE_ID].pack("C") + RLP.encode(
|
|
48
|
+
Eth::Util.keccak256([TYPE_ID].pack("C") + RLP.encode(signing_rlp_fields))
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# Hash for fee payer to sign
|
|
51
|
+
# Hash for fee payer to sign.
|
|
52
52
|
def fee_payer_signature_hash
|
|
53
53
|
require_eth!
|
|
54
54
|
require_rlp!
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
fields.insert(11, sender_signature)
|
|
58
|
-
Eth::Util.keccak256([TYPE_ID].pack("C") + RLP.encode(fields))
|
|
56
|
+
Eth::Util.keccak256([FeePayer::TYPE_ID].pack("C") + RLP.encode(fee_payer_signing_rlp_fields))
|
|
59
57
|
end
|
|
60
58
|
|
|
61
59
|
private
|
|
62
60
|
|
|
63
61
|
def rlp_fields
|
|
62
|
+
fields = signing_rlp_fields
|
|
63
|
+
fields << signature_envelope(sender_signature) if sender_signature
|
|
64
|
+
fields
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def signing_rlp_fields
|
|
64
68
|
fields = unsigned_rlp_fields
|
|
65
|
-
fields
|
|
66
|
-
fields.insert(12, fee_payer_signature || EMPTY_SIGNATURE)
|
|
69
|
+
fields << key_authorization if key_authorization
|
|
67
70
|
fields
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
def unsigned_rlp_fields
|
|
74
|
+
[
|
|
75
|
+
chain_id,
|
|
76
|
+
max_priority_fee_per_gas,
|
|
77
|
+
max_fee_per_gas,
|
|
78
|
+
gas_limit,
|
|
79
|
+
calls.map(&:as_rlp_list),
|
|
80
|
+
access_list || EMPTY_LIST,
|
|
81
|
+
nonce_key,
|
|
82
|
+
nonce,
|
|
83
|
+
encode_optional_uint(valid_before),
|
|
84
|
+
encode_optional_uint(valid_after),
|
|
85
|
+
fee_token ? pack_hex(fee_token) : "".b,
|
|
86
|
+
fee_payer_field,
|
|
87
|
+
tempo_authorization_list || EMPTY_LIST
|
|
88
|
+
]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def fee_payer_signing_rlp_fields
|
|
71
92
|
fields = [
|
|
72
93
|
chain_id,
|
|
73
94
|
max_priority_fee_per_gas,
|
|
@@ -80,12 +101,48 @@ module Mpp
|
|
|
80
101
|
encode_optional_uint(valid_before),
|
|
81
102
|
encode_optional_uint(valid_after),
|
|
82
103
|
fee_token ? pack_hex(fee_token) : "".b,
|
|
104
|
+
pack_hex(sender_address),
|
|
83
105
|
tempo_authorization_list || EMPTY_LIST
|
|
84
106
|
]
|
|
85
107
|
fields << key_authorization if key_authorization
|
|
86
108
|
fields
|
|
87
109
|
end
|
|
88
110
|
|
|
111
|
+
def fee_payer_field
|
|
112
|
+
return signature_tuple(fee_payer_signature) if fee_payer_signature && fee_payer_signature != EMPTY_SIGNATURE
|
|
113
|
+
return EMPTY_SIGNATURE if fee_token.nil?
|
|
114
|
+
|
|
115
|
+
"".b
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def signature_tuple(signature)
|
|
119
|
+
normalized = normalized_signature(signature)
|
|
120
|
+
[
|
|
121
|
+
normalized.getbyte(64).zero? ? "".b : normalized[64],
|
|
122
|
+
trim_leading_zeroes(normalized[0, 32]),
|
|
123
|
+
trim_leading_zeroes(normalized[32, 32])
|
|
124
|
+
]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def signature_envelope(signature)
|
|
128
|
+
normalized_signature(signature)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def normalized_signature(signature)
|
|
132
|
+
bytes = signature.b
|
|
133
|
+
raise ArgumentError, "signature must be 65 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 65
|
|
134
|
+
|
|
135
|
+
v = bytes.getbyte(64)
|
|
136
|
+
parity = (v >= 27) ? v - 27 : v
|
|
137
|
+
raise ArgumentError, "signature parity must be 0 or 1, got #{v}" unless [0, 1].include?(parity)
|
|
138
|
+
|
|
139
|
+
bytes[0, 64] + [parity].pack("C")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def trim_leading_zeroes(value)
|
|
143
|
+
value.sub(/\A\x00+/n, "")
|
|
144
|
+
end
|
|
145
|
+
|
|
89
146
|
def pack_hex(value)
|
|
90
147
|
[value.delete_prefix("0x")].pack("H*")
|
|
91
148
|
end
|
|
@@ -112,11 +169,12 @@ module Mpp
|
|
|
112
169
|
module_function
|
|
113
170
|
|
|
114
171
|
def build_signed_transfer(account:, chain_id:, gas_limit:, gas_price:, nonce:, nonce_key:,
|
|
115
|
-
currency:, transfer_data:,
|
|
172
|
+
currency:, transfer_data:, max_priority_fee_per_gas: nil, max_fee_per_gas: nil,
|
|
173
|
+
valid_before: nil, awaiting_fee_payer: false)
|
|
116
174
|
tx = SignedTransaction.new(
|
|
117
175
|
chain_id: chain_id,
|
|
118
|
-
max_priority_fee_per_gas: gas_price,
|
|
119
|
-
max_fee_per_gas: gas_price,
|
|
176
|
+
max_priority_fee_per_gas: max_priority_fee_per_gas || gas_price,
|
|
177
|
+
max_fee_per_gas: max_fee_per_gas || gas_price,
|
|
120
178
|
gas_limit: gas_limit,
|
|
121
179
|
calls: [Call.new(to: currency, value: 0, data: transfer_data)],
|
|
122
180
|
access_list: EMPTY_LIST,
|
data/lib/mpp/methods/tempo.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Mpp
|
|
|
10
10
|
autoload :Attribution, "mpp/methods/tempo/attribution"
|
|
11
11
|
autoload :Rpc, "mpp/methods/tempo/rpc"
|
|
12
12
|
autoload :Transaction, "mpp/methods/tempo/transaction"
|
|
13
|
+
autoload :FeePayerPolicy, "mpp/methods/tempo/fee_payer_policy"
|
|
13
14
|
autoload :Schemas, "mpp/methods/tempo/schemas"
|
|
14
15
|
# Eagerly require client_method so the Tempo.tempo factory method is available
|
|
15
16
|
require_relative "tempo/client_method"
|
data/lib/mpp/parsing.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Mpp
|
|
|
13
13
|
|
|
14
14
|
# RFC 9110 auth-param regex: key="value" or key=token
|
|
15
15
|
AUTH_PARAM_RE = /([a-zA-Z_][\w-]*+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]++))/
|
|
16
|
+
PAYMENT_METHOD_ID_RE = /\A[a-z]+\z/
|
|
16
17
|
|
|
17
18
|
module_function
|
|
18
19
|
|
|
@@ -65,6 +66,11 @@ module Mpp
|
|
|
65
66
|
params
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
sig { params(method: T.untyped).void }
|
|
70
|
+
def validate_payment_method_id(method)
|
|
71
|
+
Kernel.raise Mpp::ParseError, "Invalid payment method ID" unless method.is_a?(String) && PAYMENT_METHOD_ID_RE.match?(method)
|
|
72
|
+
end
|
|
73
|
+
|
|
68
74
|
# Parse a WWW-Authenticate header into a Challenge.
|
|
69
75
|
sig { params(header: T.untyped).returns(Mpp::Challenge) }
|
|
70
76
|
def parse_www_authenticate(header)
|
|
@@ -82,6 +88,7 @@ module Mpp
|
|
|
82
88
|
|
|
83
89
|
method = params["method"]
|
|
84
90
|
Kernel.raise Mpp::ParseError, "Missing 'method' field" unless method && !method.empty?
|
|
91
|
+
validate_payment_method_id(method)
|
|
85
92
|
|
|
86
93
|
intent = params["intent"]
|
|
87
94
|
Kernel.raise Mpp::ParseError, "Missing 'intent' field" unless intent && !intent.empty?
|
|
@@ -148,10 +155,13 @@ module Mpp
|
|
|
148
155
|
Kernel.raise Mpp::ParseError, "Credential challenge must be an object" unless challenge_data.is_a?(Hash)
|
|
149
156
|
Kernel.raise Mpp::ParseError, "Credential challenge missing required field: id" unless challenge_data.key?("id")
|
|
150
157
|
|
|
158
|
+
method = challenge_data["method"]
|
|
159
|
+
validate_payment_method_id(method)
|
|
160
|
+
|
|
151
161
|
echo = Mpp::ChallengeEcho.new(
|
|
152
162
|
id: challenge_data["id"].to_s,
|
|
153
163
|
realm: (challenge_data["realm"] || "").to_s,
|
|
154
|
-
method:
|
|
164
|
+
method: method,
|
|
155
165
|
intent: (challenge_data["intent"] || "").to_s,
|
|
156
166
|
request: (challenge_data["request"] || "").to_s,
|
|
157
167
|
expires: challenge_data["expires"]&.to_s,
|
|
@@ -213,6 +223,8 @@ module Mpp
|
|
|
213
223
|
Kernel.raise Mpp::ParseError, "Invalid receipt status" unless status == "success"
|
|
214
224
|
|
|
215
225
|
timestamp = parse_timestamp(data["timestamp"].to_s)
|
|
226
|
+
method = data["method"]
|
|
227
|
+
validate_payment_method_id(method)
|
|
216
228
|
|
|
217
229
|
extra = data["extra"]
|
|
218
230
|
extra = nil unless extra.is_a?(Hash)
|
|
@@ -221,7 +233,7 @@ module Mpp
|
|
|
221
233
|
status: status,
|
|
222
234
|
timestamp: timestamp,
|
|
223
235
|
reference: data["reference"].to_s,
|
|
224
|
-
method:
|
|
236
|
+
method: method,
|
|
225
237
|
external_id: data["externalId"]&.to_s,
|
|
226
238
|
extra: extra
|
|
227
239
|
)
|
data/lib/mpp/receipt.rb
CHANGED
|
@@ -18,13 +18,14 @@ module Mpp
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# Create a success receipt with current timestamp.
|
|
21
|
-
def self.success(reference, timestamp: nil, method: "tempo", external_id: nil)
|
|
21
|
+
def self.success(reference, timestamp: nil, method: "tempo", external_id: nil, extra: nil)
|
|
22
22
|
new(
|
|
23
23
|
status: "success",
|
|
24
24
|
timestamp: timestamp || Time.now.utc,
|
|
25
25
|
reference: reference,
|
|
26
26
|
method: method,
|
|
27
|
-
external_id: external_id
|
|
27
|
+
external_id: external_id,
|
|
28
|
+
extra: extra
|
|
28
29
|
)
|
|
29
30
|
end
|
|
30
31
|
end
|
data/lib/mpp/server/decorator.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# typed: strict
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
4
6
|
module Mpp
|
|
5
7
|
module Server
|
|
6
8
|
# Rack middleware that intercepts requests requiring payment.
|
|
@@ -26,15 +28,20 @@ module Mpp
|
|
|
26
28
|
sig { params(env: T.untyped).returns(T::Array[T.untyped]) }
|
|
27
29
|
def call(env)
|
|
28
30
|
authorization = env["HTTP_AUTHORIZATION"]
|
|
31
|
+
body_capture = capture_request_body(env)
|
|
29
32
|
status, headers, body = @app.call(env)
|
|
30
33
|
|
|
31
34
|
charge_opts = env["mpp.charge"]
|
|
32
35
|
return [status, headers, body] unless charge_opts
|
|
33
36
|
|
|
37
|
+
request_body = body_capture&.materialize
|
|
38
|
+
env["rack.input"] = StringIO.new(request_body || "") if body_capture
|
|
39
|
+
|
|
34
40
|
amount = charge_opts[:amount]
|
|
35
|
-
opts = charge_opts.except(:amount)
|
|
41
|
+
opts = charge_opts.except(:amount, :body)
|
|
42
|
+
opts[:mppx_scope] ||= mppx_scope(env)
|
|
36
43
|
|
|
37
|
-
result = @handler.charge(authorization, amount, **opts)
|
|
44
|
+
result = @handler.charge(authorization, amount, **opts, body: request_body)
|
|
38
45
|
|
|
39
46
|
if result.is_a?(Mpp::Challenge)
|
|
40
47
|
resp = Mpp::Server::Decorator.make_challenge_response(result, @handler.realm)
|
|
@@ -43,9 +50,101 @@ module Mpp
|
|
|
43
50
|
|
|
44
51
|
_credential, receipt = result
|
|
45
52
|
headers["Payment-Receipt"] = receipt.to_payment_receipt
|
|
53
|
+
self.class.mark_authorization_bound_response(headers)
|
|
46
54
|
|
|
47
55
|
[status, headers, body]
|
|
48
56
|
end
|
|
57
|
+
|
|
58
|
+
sig { params(headers: T::Hash[T.untyped, T.untyped]).void }
|
|
59
|
+
def self.mark_authorization_bound_response(headers)
|
|
60
|
+
headers["Cache-Control"] = "no-store"
|
|
61
|
+
|
|
62
|
+
vary_values = headers["Vary"].to_s.split(",").map do |value|
|
|
63
|
+
value.strip.downcase
|
|
64
|
+
end
|
|
65
|
+
return if vary_values.include?("*") || vary_values.include?("authorization")
|
|
66
|
+
|
|
67
|
+
headers["Vary"] = [headers["Vary"], "Authorization"]
|
|
68
|
+
.compact
|
|
69
|
+
.reject(&:empty?)
|
|
70
|
+
.join(", ")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
sig { params(env: T.untyped).returns(T.nilable(RackInputCapture)) }
|
|
76
|
+
def capture_request_body(env)
|
|
77
|
+
input = env["rack.input"]
|
|
78
|
+
return nil unless input&.respond_to?(:read)
|
|
79
|
+
|
|
80
|
+
capture = RackInputCapture.new(input)
|
|
81
|
+
env["rack.input"] = capture
|
|
82
|
+
capture
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
sig { params(env: T.untyped).returns(T::Hash[String, String]) }
|
|
86
|
+
def mppx_scope(env)
|
|
87
|
+
scope = T.let({}, T::Hash[String, String])
|
|
88
|
+
route = env["action_dispatch.route_uri_pattern"] || env["sinatra.route"] || env["roda.route"]
|
|
89
|
+
route = route.split(" ", 2).last if route.is_a?(String) && route.match?(/\A[A-Z]+\s+/)
|
|
90
|
+
scope["route"] = route if route.is_a?(String) && !route.empty?
|
|
91
|
+
path = env["PATH_INFO"]
|
|
92
|
+
scope["resource"] = path if path.is_a?(String) && !path.empty?
|
|
93
|
+
query = env["QUERY_STRING"]
|
|
94
|
+
scope["query"] = query if query.is_a?(String) && !query.empty?
|
|
95
|
+
scope
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class RackInputCapture
|
|
99
|
+
extend T::Sig
|
|
100
|
+
|
|
101
|
+
sig { params(input: T.untyped).void }
|
|
102
|
+
def initialize(input)
|
|
103
|
+
@input = T.let(input, T.untyped)
|
|
104
|
+
@buffer = T.let(+"".b, String)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { params(args: T.untyped).returns(T.untyped) }
|
|
108
|
+
def read(*args)
|
|
109
|
+
chunk = @input.read(*args)
|
|
110
|
+
@buffer << chunk.b if chunk && !chunk.empty?
|
|
111
|
+
chunk
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
sig { params(args: T.untyped).returns(T.untyped) }
|
|
115
|
+
def gets(*args)
|
|
116
|
+
chunk = @input.gets(*args)
|
|
117
|
+
@buffer << chunk.b if chunk && !chunk.empty?
|
|
118
|
+
chunk
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
sig { params(block: T.nilable(T.proc.params(chunk: T.untyped).void)).returns(T.untyped) }
|
|
122
|
+
def each(&block)
|
|
123
|
+
return enum_for(:each) unless block
|
|
124
|
+
|
|
125
|
+
@input.each do |chunk|
|
|
126
|
+
@buffer << chunk.b if chunk && !chunk.empty?
|
|
127
|
+
block.call(chunk)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
sig { returns(T.untyped) }
|
|
132
|
+
def rewind
|
|
133
|
+
@input.rewind if @input.respond_to?(:rewind)
|
|
134
|
+
@buffer = +"".b
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
sig { returns(T.untyped) }
|
|
138
|
+
def close
|
|
139
|
+
@input.close if @input.respond_to?(:close)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
sig { returns(T.nilable(String)) }
|
|
143
|
+
def materialize
|
|
144
|
+
read
|
|
145
|
+
@buffer.empty? ? nil : @buffer.dup
|
|
146
|
+
end
|
|
147
|
+
end
|
|
49
148
|
end
|
|
50
149
|
end
|
|
51
150
|
end
|
|
@@ -22,28 +22,51 @@ module Mpp
|
|
|
22
22
|
sig { returns(T::Hash[String, T.untyped]) }
|
|
23
23
|
attr_reader :defaults
|
|
24
24
|
|
|
25
|
-
sig { params(method: T.untyped, realm: String, secret_key: String, defaults: T.nilable(T::Hash[String, T.untyped])).void }
|
|
26
|
-
def initialize(method:, realm:, secret_key:, defaults: nil)
|
|
25
|
+
sig { params(method: T.untyped, realm: String, secret_key: String, defaults: T.nilable(T::Hash[String, T.untyped]), events: T.nilable(Mpp::Events::Dispatcher)).void }
|
|
26
|
+
def initialize(method:, realm:, secret_key:, defaults: nil, events: nil)
|
|
27
27
|
@method = T.let(method, T.untyped)
|
|
28
28
|
@realm = T.let(realm, String)
|
|
29
29
|
@secret_key = T.let(secret_key, String)
|
|
30
30
|
@defaults = T.let(defaults || {}, T::Hash[String, T.untyped])
|
|
31
|
+
@events = T.let(events || Mpp::Events.server_dispatcher, Mpp::Events::Dispatcher)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
# Create with auto-detected realm and secret_key.
|
|
34
|
-
sig { params(method: T.untyped, realm: T.untyped, secret_key: T.untyped).returns(T.attached_class) }
|
|
35
|
-
def self.create(method:, realm: nil, secret_key: nil)
|
|
35
|
+
sig { params(method: T.untyped, realm: T.untyped, secret_key: T.untyped, events: T.nilable(Mpp::Events::Dispatcher)).returns(T.attached_class) }
|
|
36
|
+
def self.create(method:, realm: nil, secret_key: nil, events: nil)
|
|
36
37
|
new(
|
|
37
38
|
method: method,
|
|
38
39
|
realm: realm || Defaults.detect_realm,
|
|
39
|
-
secret_key: secret_key || Defaults.detect_secret_key
|
|
40
|
+
secret_key: secret_key || Defaults.detect_secret_key,
|
|
41
|
+
events: events
|
|
40
42
|
)
|
|
41
43
|
end
|
|
42
44
|
|
|
45
|
+
sig { params(name: String, handler: T.untyped, block: T.nilable(T.proc.params(payload: T.untyped).returns(T.untyped))).returns(T.proc.void) }
|
|
46
|
+
def on(name, handler = nil, &block)
|
|
47
|
+
@events.on(name, handler, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sig { params(handler: T.untyped, block: T.nilable(T.proc.params(payload: T.untyped).returns(T.untyped))).returns(T.proc.void) }
|
|
51
|
+
def on_challenge_created(handler = nil, &block)
|
|
52
|
+
on(Mpp::Events::CHALLENGE_CREATED, handler, &block)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sig { params(handler: T.untyped, block: T.nilable(T.proc.params(payload: T.untyped).returns(T.untyped))).returns(T.proc.void) }
|
|
56
|
+
def on_payment_failed(handler = nil, &block)
|
|
57
|
+
on(Mpp::Events::PAYMENT_FAILED, handler, &block)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { params(handler: T.untyped, block: T.nilable(T.proc.params(payload: T.untyped).returns(T.untyped))).returns(T.proc.void) }
|
|
61
|
+
def on_payment_success(handler = nil, &block)
|
|
62
|
+
on(Mpp::Events::PAYMENT_SUCCESS, handler, &block)
|
|
63
|
+
end
|
|
64
|
+
|
|
43
65
|
# Handle a charge intent.
|
|
44
|
-
sig { params(authorization: T.nilable(String), amount: String, currency: T.nilable(String), recipient: T.nilable(String), expires: T.nilable(String), description: T.nilable(String), memo: T.nilable(String), fee_payer: T::Boolean, chain_id: T.nilable(Integer), extra: T.nilable(T::Hash[String, String])).returns(T.untyped) }
|
|
66
|
+
sig { params(authorization: T.nilable(String), amount: String, currency: T.nilable(String), recipient: T.nilable(String), expires: T.nilable(String), description: T.nilable(String), external_id: T.nilable(String), memo: T.nilable(String), fee_payer: T::Boolean, chain_id: T.nilable(Integer), extra: T.nilable(T::Hash[String, String]), mppx_scope: T.nilable(T::Hash[String, String]), body: T.untyped).returns(T.untyped) }
|
|
45
67
|
def charge(authorization, amount, currency: nil, recipient: nil, expires: nil,
|
|
46
|
-
description: nil, memo: nil, fee_payer: false, chain_id: nil,
|
|
68
|
+
description: nil, external_id: nil, memo: nil, fee_payer: false, chain_id: nil,
|
|
69
|
+
extra: nil, mppx_scope: nil, body: nil)
|
|
47
70
|
intent = @method.intents["charge"]
|
|
48
71
|
raise ArgumentError, "Method #{@method.name} does not support charge intent" unless intent
|
|
49
72
|
|
|
@@ -60,6 +83,7 @@ module Mpp
|
|
|
60
83
|
"currency" => resolved_currency,
|
|
61
84
|
"recipient" => resolved_recipient
|
|
62
85
|
}
|
|
86
|
+
request["externalId"] = external_id unless external_id.nil?
|
|
63
87
|
|
|
64
88
|
if extra
|
|
65
89
|
extra.each do |k, v|
|
|
@@ -67,6 +91,12 @@ module Mpp
|
|
|
67
91
|
end
|
|
68
92
|
request["extra"] = extra
|
|
69
93
|
end
|
|
94
|
+
if mppx_scope
|
|
95
|
+
mppx_scope.each do |k, v|
|
|
96
|
+
raise ArgumentError, "mppx_scope must be a dict[str, str]" unless k.is_a?(String) && v.is_a?(String)
|
|
97
|
+
end
|
|
98
|
+
request["_mppx_scope"] = mppx_scope
|
|
99
|
+
end
|
|
70
100
|
|
|
71
101
|
resolved_chain_id = chain_id
|
|
72
102
|
resolved_chain_id ||= @method.chain_id if @method.respond_to?(:chain_id)
|
|
@@ -89,7 +119,9 @@ module Mpp
|
|
|
89
119
|
secret_key: @secret_key,
|
|
90
120
|
method: @method.name,
|
|
91
121
|
description: description,
|
|
92
|
-
expires: expires
|
|
122
|
+
expires: expires,
|
|
123
|
+
events: @events,
|
|
124
|
+
body: body
|
|
93
125
|
)
|
|
94
126
|
end
|
|
95
127
|
end
|