mpp-rb 0.1.2 → 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.
@@ -14,12 +14,19 @@ module Mpp
14
14
 
15
15
  def initialize(secret_key:, network_id:, payment_methods: nil,
16
16
  metadata: nil, currency: Defaults::DEFAULT_CURRENCY,
17
- decimals: Defaults::DEFAULT_DECIMALS)
17
+ decimals: Defaults::DEFAULT_DECIMALS, external_id: nil)
18
+ unless payment_methods.is_a?(Array) &&
19
+ payment_methods.any? &&
20
+ payment_methods.all? { |type| type.is_a?(String) && !type.strip.empty? }
21
+ raise ArgumentError, "payment_methods must be a non-empty array of Stripe payment method type strings"
22
+ end
23
+
18
24
  @name = "stripe"
19
25
  @secret_key = secret_key
20
26
  @network_id = network_id
21
27
  @payment_methods = payment_methods
22
28
  @metadata = metadata
29
+ @external_id = external_id
23
30
  @currency = currency
24
31
  @recipient = network_id
25
32
  @decimals = decimals
@@ -32,10 +39,12 @@ module Mpp
32
39
  method_details = {} unless method_details.is_a?(Hash)
33
40
 
34
41
  method_details["networkId"] = @network_id
35
- method_details["paymentMethods"] = @payment_methods if @payment_methods
42
+ method_details["paymentMethodTypes"] = @payment_methods
36
43
  method_details["metadata"] = @metadata if @metadata
37
44
 
38
- request.merge("methodDetails" => method_details)
45
+ transformed = request.merge("methodDetails" => method_details)
46
+ transformed["externalId"] = @external_id if !@external_id.nil? && !transformed.key?("externalId")
47
+ transformed
39
48
  end
40
49
  end
41
50
 
@@ -43,6 +52,7 @@ module Mpp
43
52
  def self.stripe(secret_key:, network_id:, payment_methods: nil,
44
53
  metadata: nil, currency: Defaults::DEFAULT_CURRENCY,
45
54
  decimals: Defaults::DEFAULT_DECIMALS,
55
+ external_id: nil,
46
56
  api_base: Defaults::STRIPE_API_BASE)
47
57
  charge_intent = ChargeIntent.new(secret_key: secret_key, api_base: api_base)
48
58
 
@@ -52,7 +62,8 @@ module Mpp
52
62
  payment_methods: payment_methods,
53
63
  metadata: metadata,
54
64
  currency: currency,
55
- decimals: decimals
65
+ decimals: decimals,
66
+ external_id: external_id
56
67
  )
57
68
 
58
69
  method.intents = {"charge" => charge_intent}
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative "defaults"
5
+ require_relative "fee_payer_policy"
5
6
  require_relative "transaction"
6
7
 
7
8
  module Mpp
@@ -16,18 +17,20 @@ module Mpp
16
17
  # Tempo payment method implementation.
17
18
  # Handles client-side credential creation for Tempo payments.
18
19
  class TempoMethod
19
- attr_reader :name, :account, :fee_payer, :root_account, :rpc_url,
20
- :chain_id, :currency, :recipient, :decimals, :client_id,
20
+ attr_reader :name, :account, :fee_payer, :fee_payer_allowed_fee_tokens,
21
+ :root_account, :rpc_url, :chain_id, :currency, :recipient, :decimals, :client_id,
21
22
  :expected_recipients
22
23
  attr_accessor :intents
23
24
 
24
25
  def initialize(account: nil, fee_payer: nil, root_account: nil,
25
26
  rpc_url: Defaults::RPC_URL, chain_id: nil, currency: nil,
26
27
  recipient: nil, decimals: 6, client_id: nil,
27
- expected_recipients: nil)
28
+ expected_recipients: nil, fee_payer_allowed_fee_tokens: nil)
28
29
  @name = "tempo"
29
30
  @account = account
30
31
  @fee_payer = fee_payer
32
+ @fee_payer_allowed_fee_tokens =
33
+ fee_payer_allowed_fee_tokens&.map { |token| token.to_s.downcase }
31
34
  @root_account = root_account
32
35
  @rpc_url = rpc_url
33
36
  @chain_id = chain_id
@@ -65,13 +68,21 @@ module Mpp
65
68
  memo = method_details["memo"]
66
69
  memo ||= Attribution.encode(server_id: challenge.realm, client_id: @client_id, challenge_id: challenge.id)
67
70
 
68
- # Resolve RPC URL from challenge's chainId
71
+ # Resolve RPC URL from challenge's chainId. Normalize the configured pin
72
+ # once (it may be a String from ENV/config) so it compares equal to
73
+ # integer chain ids everywhere, including the downstream RPC check.
69
74
  resolved_rpc_url = @rpc_url
70
75
  expected_chain_id = nil
76
+ configured_chain_id = @chain_id.nil? ? nil : Integer(@chain_id)
71
77
  challenge_chain_id = method_details["chainId"]
72
78
  if challenge_chain_id
73
79
  begin
74
80
  parsed_chain_id = Integer(challenge_chain_id)
81
+ # Chain pinning: reject a challenge whose chainId conflicts with the
82
+ # configured chain, before any RPC call or signing.
83
+ if configured_chain_id && parsed_chain_id != configured_chain_id
84
+ raise TransactionError, "Chain ID mismatch: expected #{configured_chain_id}, got #{parsed_chain_id}"
85
+ end
75
86
  resolved = Defaults::CHAIN_RPC_URLS[parsed_chain_id]
76
87
  if resolved
77
88
  resolved_rpc_url = resolved
@@ -82,7 +93,7 @@ module Mpp
82
93
  end
83
94
  end
84
95
 
85
- expected_chain_id ||= @chain_id
96
+ expected_chain_id ||= configured_chain_id
86
97
 
87
98
  # Proof mode: sign EIP-712 typed data (no transaction needed)
88
99
  if mode == :proof
@@ -92,7 +103,8 @@ module Mpp
92
103
  signature = Proof.sign(
93
104
  account: @account,
94
105
  chain_id: chain_id,
95
- challenge_id: challenge.id
106
+ challenge_id: challenge.id,
107
+ realm: challenge.realm
96
108
  )
97
109
 
98
110
  return Mpp::Credential.new(
@@ -172,6 +184,11 @@ module Mpp
172
184
  raise TransactionError,
173
185
  "Chain ID mismatch: RPC returned #{chain_id}, expected #{expected_chain_id} from challenge"
174
186
  end
187
+ if awaiting_fee_payer
188
+ policy = FeePayerPolicy.for_chain_id(chain_id)
189
+ max_fee_per_gas = [gas_price, policy.max_fee_per_gas].min
190
+ max_priority_fee_per_gas = [gas_price, policy.max_priority_fee_per_gas].min
191
+ end
175
192
 
176
193
  if awaiting_fee_payer
177
194
  resolved_nonce_key = EXPIRING_NONCE_KEY
@@ -195,6 +212,8 @@ module Mpp
195
212
  chain_id: chain_id,
196
213
  gas_limit: gas_limit,
197
214
  gas_price: gas_price,
215
+ max_priority_fee_per_gas: max_priority_fee_per_gas,
216
+ max_fee_per_gas: max_fee_per_gas,
198
217
  nonce: resolved_nonce,
199
218
  nonce_key: resolved_nonce_key,
200
219
  currency: currency,
@@ -230,7 +249,7 @@ module Mpp
230
249
  # Factory function to create a configured TempoMethod.
231
250
  def self.tempo(intents:, account: nil, fee_payer: nil, chain_id: nil, rpc_url: nil,
232
251
  root_account: nil, currency: nil, recipient: nil, decimals: 6, client_id: nil,
233
- expected_recipients: nil)
252
+ expected_recipients: nil, fee_payer_allowed_fee_tokens: nil)
234
253
  rpc_url ||= chain_id ? Defaults.rpc_url_for_chain(chain_id) : Defaults::RPC_URL
235
254
  currency ||= Defaults.default_currency_for_chain(chain_id)
236
255
 
@@ -244,7 +263,8 @@ module Mpp
244
263
  recipient: recipient,
245
264
  decimals: decimals,
246
265
  client_id: client_id,
247
- expected_recipients: expected_recipients
266
+ expected_recipients: expected_recipients,
267
+ fee_payer_allowed_fee_tokens: fee_payer_allowed_fee_tokens
248
268
  )
249
269
 
250
270
  intents.each_value do |intent|
@@ -0,0 +1,45 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ module Methods
6
+ module Tempo
7
+ module FeePayerPolicy
8
+ Policy = Data.define(
9
+ :max_gas,
10
+ :max_fee_per_gas,
11
+ :max_priority_fee_per_gas,
12
+ :max_total_fee,
13
+ :max_validity_window_seconds
14
+ )
15
+
16
+ DEFAULT = Policy.new(
17
+ max_gas: 2_000_000,
18
+ max_fee_per_gas: 100_000_000_000,
19
+ max_priority_fee_per_gas: 10_000_000_000,
20
+ max_total_fee: 50_000_000_000_000_000,
21
+ max_validity_window_seconds: 15 * 60
22
+ )
23
+
24
+ TESTNET = Policy.new(
25
+ max_gas: DEFAULT.max_gas,
26
+ max_fee_per_gas: DEFAULT.max_fee_per_gas,
27
+ max_priority_fee_per_gas: 50_000_000_000,
28
+ max_total_fee: DEFAULT.max_total_fee,
29
+ max_validity_window_seconds: DEFAULT.max_validity_window_seconds
30
+ )
31
+
32
+ module_function
33
+
34
+ def for_chain_id(chain_id)
35
+ case chain_id
36
+ when Defaults::CHAIN_ID, Defaults::TESTNET_CHAIN_ID
37
+ TESTNET
38
+ else
39
+ DEFAULT
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end