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,130 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mpp
|
|
5
|
+
module Methods
|
|
6
|
+
module Stripe
|
|
7
|
+
# Configures Stripe-backed MPP methods with static deposit addresses.
|
|
8
|
+
class MachinePayments
|
|
9
|
+
SPT_MINIMUM_MINOR_UNITS = 50
|
|
10
|
+
|
|
11
|
+
attr_reader :spt, :tempo, :base
|
|
12
|
+
|
|
13
|
+
def initialize(network_id:, livemode:, client:, deposit_addresses: nil, metadata: nil)
|
|
14
|
+
raise ArgumentError, "client must respond to #v1" unless client.respond_to?(:v1)
|
|
15
|
+
|
|
16
|
+
addresses = normalize_deposit_addresses(deposit_addresses)
|
|
17
|
+
@spt = SptFactory.new(network_id: network_id, client: client, metadata: metadata)
|
|
18
|
+
@tempo = TempoFactory.new(livemode: livemode, client: client, recipient: addresses[:tempo], metadata: metadata)
|
|
19
|
+
@base = BaseFactory.new(livemode: livemode, client: client, recipient: addresses[:base], metadata: metadata)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Tempo is preferred when configured; Base requires an explicit facilitator.
|
|
23
|
+
def default_methods
|
|
24
|
+
methods = [spt.charge]
|
|
25
|
+
methods.unshift(tempo.charge) if tempo.configured?
|
|
26
|
+
methods
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.minimum_amount(minimum)
|
|
30
|
+
lambda do |request|
|
|
31
|
+
Integer(request.fetch("amount")) >= minimum
|
|
32
|
+
rescue ArgumentError, TypeError, KeyError
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def normalize_deposit_addresses(deposit_addresses)
|
|
40
|
+
return {} if deposit_addresses.nil?
|
|
41
|
+
raise ArgumentError, "deposit_addresses must be a Hash" unless deposit_addresses.is_a?(Hash)
|
|
42
|
+
|
|
43
|
+
deposit_addresses.each_with_object({}) do |(network, address), result|
|
|
44
|
+
unless [:tempo, :base].include?(network)
|
|
45
|
+
raise ArgumentError, "unsupported deposit address network: #{network}"
|
|
46
|
+
end
|
|
47
|
+
unless address.is_a?(String) && address.match?(/\A0x[0-9a-fA-F]{40}\z/)
|
|
48
|
+
raise ArgumentError, "deposit_addresses[:#{network}] must be a 0x-prefixed 40-hex-character address"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
result[network] = address.dup.freeze
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class SptFactory
|
|
57
|
+
def initialize(network_id:, client:, metadata:)
|
|
58
|
+
@network_id = network_id
|
|
59
|
+
@client = client
|
|
60
|
+
@metadata = metadata
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def charge
|
|
64
|
+
method = Mpp::Methods::Stripe::StripeMethod.new(
|
|
65
|
+
secret_key: nil,
|
|
66
|
+
network_id: @network_id,
|
|
67
|
+
payment_methods: ["card", "link"],
|
|
68
|
+
metadata: @metadata,
|
|
69
|
+
can_offer: MachinePayments.minimum_amount(MachinePayments::SPT_MINIMUM_MINOR_UNITS)
|
|
70
|
+
)
|
|
71
|
+
method.intents = {"charge" => Mpp::Methods::Stripe::ChargeIntent.new(secret_key: nil, client: @client)}
|
|
72
|
+
method
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class TempoFactory
|
|
77
|
+
def initialize(livemode:, client:, recipient:, metadata:)
|
|
78
|
+
@livemode = livemode
|
|
79
|
+
@client = client
|
|
80
|
+
@recipient = recipient
|
|
81
|
+
@metadata = metadata
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def configured?
|
|
85
|
+
!@recipient.nil?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def charge
|
|
89
|
+
raise ArgumentError, "deposit_addresses[:tempo] is required for Tempo payments" unless configured?
|
|
90
|
+
|
|
91
|
+
chain_id = @livemode ? Mpp::Methods::Tempo::Defaults::CHAIN_ID : Mpp::Methods::Tempo::Defaults::TESTNET_CHAIN_ID
|
|
92
|
+
currency = @livemode ? Mpp::Methods::Tempo::Defaults::USDC : Mpp::Methods::Tempo::Defaults::PATH_USD
|
|
93
|
+
recorder = CryptoPaymentRecorder.new(client: @client, network: "tempo", metadata: @metadata)
|
|
94
|
+
Mpp::Methods::Tempo.tempo(
|
|
95
|
+
intents: {"charge" => Mpp::Methods::Tempo::ChargeIntent.new(chain_id: chain_id)},
|
|
96
|
+
chain_id: chain_id,
|
|
97
|
+
currency: currency,
|
|
98
|
+
recipient: @recipient,
|
|
99
|
+
decimals: Mpp::Methods::Tempo::Defaults::PATH_USD_DECIMALS,
|
|
100
|
+
can_offer: MachinePayments.minimum_amount(CryptoPaymentRecorder::RAW_UNITS_PER_CENT),
|
|
101
|
+
on_payment_success: ->(payload) { recorder.call(payload) }
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class BaseFactory
|
|
107
|
+
def initialize(livemode:, client:, recipient:, metadata:)
|
|
108
|
+
@livemode = livemode
|
|
109
|
+
@client = client
|
|
110
|
+
@recipient = recipient
|
|
111
|
+
@metadata = metadata
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def charge(x402:)
|
|
115
|
+
raise ArgumentError, "deposit_addresses[:base] is required for Base payments" if @recipient.nil?
|
|
116
|
+
|
|
117
|
+
currency = @livemode ? Mpp::Methods::Evm::Assets::BASE_USDC : Mpp::Methods::Evm::Assets::BASE_SEPOLIA_USDC
|
|
118
|
+
recorder = CryptoPaymentRecorder.new(client: @client, network: "base", metadata: @metadata)
|
|
119
|
+
Mpp::Methods::Evm.charge(
|
|
120
|
+
currency: currency,
|
|
121
|
+
recipient: @recipient,
|
|
122
|
+
x402: x402,
|
|
123
|
+
can_offer: MachinePayments.minimum_amount(CryptoPaymentRecorder::RAW_UNITS_PER_CENT),
|
|
124
|
+
on_payment_success: ->(payload) { recorder.call(payload) }
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -9,17 +9,24 @@ module Mpp
|
|
|
9
9
|
# Stripe payment method implementation.
|
|
10
10
|
# Handles SPT-based payments through Stripe's Business Network.
|
|
11
11
|
class StripeMethod
|
|
12
|
-
attr_reader :name, :currency, :recipient, :decimals
|
|
12
|
+
attr_reader :name, :currency, :recipient, :decimals, :on_payment_success
|
|
13
13
|
attr_accessor :intents
|
|
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, external_id: nil
|
|
17
|
+
decimals: Defaults::DEFAULT_DECIMALS, external_id: nil,
|
|
18
|
+
on_payment_success: nil, can_offer: nil)
|
|
18
19
|
unless payment_methods.is_a?(Array) &&
|
|
19
20
|
payment_methods.any? &&
|
|
20
21
|
payment_methods.all? { |type| type.is_a?(String) && !type.strip.empty? }
|
|
21
22
|
raise ArgumentError, "payment_methods must be a non-empty array of Stripe payment method type strings"
|
|
22
23
|
end
|
|
24
|
+
unless on_payment_success.nil? || on_payment_success.respond_to?(:call)
|
|
25
|
+
raise ArgumentError, "on_payment_success must be callable"
|
|
26
|
+
end
|
|
27
|
+
unless can_offer.nil? || can_offer.respond_to?(:call)
|
|
28
|
+
raise ArgumentError, "can_offer must be callable"
|
|
29
|
+
end
|
|
23
30
|
|
|
24
31
|
@name = "stripe"
|
|
25
32
|
@secret_key = secret_key
|
|
@@ -27,12 +34,20 @@ module Mpp
|
|
|
27
34
|
@payment_methods = payment_methods
|
|
28
35
|
@metadata = metadata
|
|
29
36
|
@external_id = external_id
|
|
37
|
+
@on_payment_success = on_payment_success
|
|
38
|
+
@can_offer = can_offer
|
|
30
39
|
@currency = currency
|
|
31
40
|
@recipient = network_id
|
|
32
41
|
@decimals = decimals
|
|
33
42
|
@intents = {}
|
|
34
43
|
end
|
|
35
44
|
|
|
45
|
+
def can_offer?(request)
|
|
46
|
+
return true unless @can_offer
|
|
47
|
+
|
|
48
|
+
@can_offer.call(request)
|
|
49
|
+
end
|
|
50
|
+
|
|
36
51
|
# Transform request - injects Stripe-specific methodDetails.
|
|
37
52
|
def transform_request(request, _credential)
|
|
38
53
|
method_details = request.fetch("methodDetails", {})
|
|
@@ -53,6 +68,8 @@ module Mpp
|
|
|
53
68
|
metadata: nil, currency: Defaults::DEFAULT_CURRENCY,
|
|
54
69
|
decimals: Defaults::DEFAULT_DECIMALS,
|
|
55
70
|
external_id: nil,
|
|
71
|
+
on_payment_success: nil,
|
|
72
|
+
can_offer: nil,
|
|
56
73
|
api_base: Defaults::STRIPE_API_BASE)
|
|
57
74
|
charge_intent = ChargeIntent.new(secret_key: secret_key, api_base: api_base)
|
|
58
75
|
|
|
@@ -63,7 +80,9 @@ module Mpp
|
|
|
63
80
|
metadata: metadata,
|
|
64
81
|
currency: currency,
|
|
65
82
|
decimals: decimals,
|
|
66
|
-
external_id: external_id
|
|
83
|
+
external_id: external_id,
|
|
84
|
+
on_payment_success: on_payment_success,
|
|
85
|
+
can_offer: can_offer
|
|
67
86
|
)
|
|
68
87
|
|
|
69
88
|
method.intents = {"charge" => charge_intent}
|
data/lib/mpp/methods/stripe.rb
CHANGED
|
@@ -4,11 +4,34 @@
|
|
|
4
4
|
module Mpp
|
|
5
5
|
module Methods
|
|
6
6
|
module Stripe
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
7
9
|
autoload :Defaults, "mpp/methods/stripe/defaults"
|
|
8
10
|
# Eagerly require stripe_method so the Stripe.stripe factory method is available
|
|
9
11
|
require_relative "stripe/stripe_method"
|
|
10
12
|
autoload :ChargeIntent, "mpp/methods/stripe/charge_intent"
|
|
11
13
|
autoload :ClientMethod, "mpp/methods/stripe/client_method"
|
|
14
|
+
autoload :CryptoPaymentRecorder, "mpp/methods/stripe/crypto_payment_recorder"
|
|
15
|
+
autoload :MachinePayments, "mpp/methods/stripe/machine_payments"
|
|
16
|
+
|
|
17
|
+
sig do
|
|
18
|
+
params(
|
|
19
|
+
network_id: String,
|
|
20
|
+
livemode: T::Boolean,
|
|
21
|
+
client: T.untyped,
|
|
22
|
+
deposit_addresses: T.untyped,
|
|
23
|
+
metadata: T.untyped
|
|
24
|
+
).returns(MachinePayments)
|
|
25
|
+
end
|
|
26
|
+
def self.create(network_id:, livemode:, client:, deposit_addresses: nil, metadata: nil)
|
|
27
|
+
MachinePayments.new(
|
|
28
|
+
network_id: network_id,
|
|
29
|
+
livemode: livemode,
|
|
30
|
+
client: client,
|
|
31
|
+
deposit_addresses: deposit_addresses,
|
|
32
|
+
metadata: metadata
|
|
33
|
+
)
|
|
34
|
+
end
|
|
12
35
|
end
|
|
13
36
|
end
|
|
14
37
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# typed: false
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "
|
|
4
|
+
require "digest/keccak"
|
|
5
5
|
require "securerandom"
|
|
6
6
|
|
|
7
7
|
module Mpp
|
|
@@ -13,15 +13,9 @@ module Mpp
|
|
|
13
13
|
|
|
14
14
|
module_function
|
|
15
15
|
|
|
16
|
-
#
|
|
16
|
+
# Ethereum Keccak-256. SHA3-256 is not a substitute (different padding).
|
|
17
17
|
def keccak256(data)
|
|
18
|
-
|
|
19
|
-
Kernel.require "eth"
|
|
20
|
-
Eth::Util.keccak256(data)
|
|
21
|
-
rescue LoadError
|
|
22
|
-
# Fallback: use OpenSSL's SHA3-256 (not exactly keccak, but close)
|
|
23
|
-
# For production, the eth gem should be installed
|
|
24
|
-
OpenSSL::Digest.new("SHA3-256").digest(data)
|
|
18
|
+
Digest::Keccak.digest(data, 256)
|
|
25
19
|
end
|
|
26
20
|
|
|
27
21
|
# Compute TAG = keccak256("mpp")[0:4]
|
|
@@ -19,16 +19,25 @@ 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
|
|
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
|
-
expected_recipients: nil, fee_payer_allowed_fee_tokens: nil
|
|
28
|
+
expected_recipients: nil, fee_payer_allowed_fee_tokens: 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
|
+
|
|
29
37
|
@name = "tempo"
|
|
30
38
|
@account = account
|
|
31
39
|
@fee_payer = fee_payer
|
|
40
|
+
@relay = relay
|
|
32
41
|
@fee_payer_allowed_fee_tokens =
|
|
33
42
|
fee_payer_allowed_fee_tokens&.map { |token| token.to_s.downcase }
|
|
34
43
|
@root_account = root_account
|
|
@@ -39,9 +48,17 @@ module Mpp
|
|
|
39
48
|
@decimals = decimals
|
|
40
49
|
@client_id = client_id
|
|
41
50
|
@expected_recipients = expected_recipients&.map(&:downcase)&.to_set
|
|
51
|
+
@on_payment_success = on_payment_success
|
|
52
|
+
@can_offer = can_offer
|
|
42
53
|
@intents = {}
|
|
43
54
|
end
|
|
44
55
|
|
|
56
|
+
def can_offer?(request)
|
|
57
|
+
return true unless @can_offer
|
|
58
|
+
|
|
59
|
+
@can_offer.call(request)
|
|
60
|
+
end
|
|
61
|
+
|
|
45
62
|
# Create a credential to satisfy the given challenge.
|
|
46
63
|
#
|
|
47
64
|
# mode: :pull (default) — return signed transaction for server to broadcast
|
|
@@ -249,13 +266,20 @@ module Mpp
|
|
|
249
266
|
# Factory function to create a configured TempoMethod.
|
|
250
267
|
def self.tempo(intents:, account: nil, fee_payer: nil, chain_id: nil, rpc_url: nil,
|
|
251
268
|
root_account: nil, currency: nil, recipient: nil, decimals: 6, client_id: nil,
|
|
252
|
-
expected_recipients: nil, fee_payer_allowed_fee_tokens: nil
|
|
269
|
+
expected_recipients: nil, fee_payer_allowed_fee_tokens: nil, relay: nil,
|
|
270
|
+
on_payment_success: nil, can_offer: nil)
|
|
253
271
|
rpc_url ||= chain_id ? Defaults.rpc_url_for_chain(chain_id) : Defaults::RPC_URL
|
|
254
272
|
currency ||= Defaults.default_currency_for_chain(chain_id)
|
|
255
273
|
|
|
274
|
+
if fee_payer == true
|
|
275
|
+
raise ArgumentError, "fee_payer: true requires account:" unless account
|
|
276
|
+
|
|
277
|
+
fee_payer = account
|
|
278
|
+
end
|
|
279
|
+
|
|
256
280
|
method = TempoMethod.new(
|
|
257
281
|
account: account,
|
|
258
|
-
fee_payer: fee_payer,
|
|
282
|
+
fee_payer: FeePayerClient.resolve_optional(fee_payer),
|
|
259
283
|
rpc_url: rpc_url,
|
|
260
284
|
chain_id: chain_id,
|
|
261
285
|
root_account: root_account,
|
|
@@ -264,12 +288,17 @@ module Mpp
|
|
|
264
288
|
decimals: decimals,
|
|
265
289
|
client_id: client_id,
|
|
266
290
|
expected_recipients: expected_recipients,
|
|
267
|
-
fee_payer_allowed_fee_tokens: fee_payer_allowed_fee_tokens
|
|
291
|
+
fee_payer_allowed_fee_tokens: fee_payer_allowed_fee_tokens,
|
|
292
|
+
relay: Relay.resolve_optional(relay),
|
|
293
|
+
on_payment_success: on_payment_success,
|
|
294
|
+
can_offer: can_offer
|
|
268
295
|
)
|
|
269
296
|
|
|
270
297
|
intents.each_value do |intent|
|
|
271
298
|
intent.rpc_url = rpc_url if intent.respond_to?(:rpc_url=) && intent.rpc_url.nil?
|
|
272
|
-
intent.
|
|
299
|
+
if intent.respond_to?(:fee_payer) || intent.respond_to?(:relay)
|
|
300
|
+
intent.instance_variable_set(:@_method, method)
|
|
301
|
+
end
|
|
273
302
|
end
|
|
274
303
|
method.intents = intents.dup
|
|
275
304
|
method
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
require_relative "../../http/headers"
|
|
8
|
+
|
|
9
|
+
module Mpp
|
|
10
|
+
module Methods
|
|
11
|
+
module Tempo
|
|
12
|
+
# HTTP client for a hosted Tempo fee-payer JSON-RPC endpoint.
|
|
13
|
+
#
|
|
14
|
+
# `resolve` accepts:
|
|
15
|
+
# * a URL string (public / unauthenticated sponsor)
|
|
16
|
+
# * a config hash (`url:` plus optional `headers:` as a Hash or per-request proc)
|
|
17
|
+
# * any object that responds to `#cosign`
|
|
18
|
+
class FeePayerClient
|
|
19
|
+
SIGN_METHOD = "eth_signRawTransaction"
|
|
20
|
+
DEFAULT_TIMEOUT = Rpc::DEFAULT_TIMEOUT
|
|
21
|
+
|
|
22
|
+
attr_reader :base_url
|
|
23
|
+
|
|
24
|
+
def initialize(url, headers: nil)
|
|
25
|
+
@base_url = Mpp::Http::Headers.normalize_base_url(url)
|
|
26
|
+
raise ArgumentError, "fee_payer url is required" if @base_url.empty?
|
|
27
|
+
|
|
28
|
+
@headers = headers
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.resolve_optional(fee_payer)
|
|
32
|
+
return if fee_payer.nil? || fee_payer == false
|
|
33
|
+
return fee_payer unless hosted_config?(fee_payer)
|
|
34
|
+
|
|
35
|
+
resolve(fee_payer)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.resolve(fee_payer)
|
|
39
|
+
return fee_payer if fee_payer.is_a?(FeePayerClient)
|
|
40
|
+
return new(fee_payer) if fee_payer.is_a?(String) && !fee_payer.empty?
|
|
41
|
+
return from_config(fee_payer) if fee_payer.is_a?(Hash)
|
|
42
|
+
return fee_payer if duck_type?(fee_payer)
|
|
43
|
+
|
|
44
|
+
raise ArgumentError, "fee_payer must be an Account, URL, {url:, headers:}, or an object that implements #cosign"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.from_config(config)
|
|
48
|
+
cfg = Mpp::Http::Headers.symbolize(config)
|
|
49
|
+
url = cfg[:url] || cfg[:base_url] || cfg[:baseUrl]
|
|
50
|
+
new(url.to_s, headers: cfg[:headers])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.hosted_config?(fee_payer)
|
|
54
|
+
return true if fee_payer.is_a?(FeePayerClient)
|
|
55
|
+
return true if fee_payer.is_a?(String)
|
|
56
|
+
return true if fee_payer.is_a?(Hash)
|
|
57
|
+
return true if duck_type?(fee_payer) && !fee_payer.respond_to?(:sign_hash)
|
|
58
|
+
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.duck_type?(fee_payer)
|
|
63
|
+
!fee_payer.nil? && fee_payer.respond_to?(:cosign)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cosign(raw_tx)
|
|
67
|
+
result = post_rpc(SIGN_METHOD, [raw_tx])
|
|
68
|
+
unless result.is_a?(String) && !result.empty?
|
|
69
|
+
raise Mpp::VerificationFailedError.new(reason: "Fee payer returned no signed transaction")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def post_rpc(method, params)
|
|
78
|
+
uri = URI.parse(@base_url)
|
|
79
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
80
|
+
http.use_ssl = uri.scheme == "https"
|
|
81
|
+
http.read_timeout = DEFAULT_TIMEOUT
|
|
82
|
+
|
|
83
|
+
request = Net::HTTP::Post.new(uri)
|
|
84
|
+
request["Content-Type"] = "application/json"
|
|
85
|
+
Mpp::Http::Headers.resolve(@headers, "/#{method}").each { |key, value| request[key] = value }
|
|
86
|
+
request.body = JSON.generate({
|
|
87
|
+
"jsonrpc" => "2.0",
|
|
88
|
+
"method" => method,
|
|
89
|
+
"params" => params,
|
|
90
|
+
"id" => 1
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
response = http.request(request)
|
|
94
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
95
|
+
raise Mpp::VerificationFailedError.new(reason: "fee payer returned HTTP #{response.code}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
body = response.body.to_s
|
|
99
|
+
if body.empty?
|
|
100
|
+
raise Mpp::VerificationFailedError.new(reason: "fee payer returned an empty body")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
parsed = JSON.parse(body)
|
|
104
|
+
unless parsed.is_a?(Hash)
|
|
105
|
+
raise Mpp::VerificationFailedError.new(reason: "fee payer returned JSON that is not an object")
|
|
106
|
+
end
|
|
107
|
+
if parsed.key?("error")
|
|
108
|
+
raise Mpp::VerificationFailedError.new(reason: "fee payer RPC error: #{parsed["error"]}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
parsed["result"]
|
|
112
|
+
rescue JSON::ParserError
|
|
113
|
+
raise Mpp::VerificationFailedError.new(reason: "fee payer returned invalid JSON")
|
|
114
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Net::OpenTimeout, Net::ReadTimeout, SocketError => e
|
|
115
|
+
raise Mpp::VerificationFailedError.new(reason: "fee payer request failed: #{e.message}")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|