mpp-rb 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +133 -0
- data/lib/mpp/body_digest.rb +37 -0
- data/lib/mpp/challenge.rb +115 -0
- data/lib/mpp/challenge_echo.rb +19 -0
- data/lib/mpp/challenge_id.rb +54 -0
- data/lib/mpp/client/transport.rb +137 -0
- data/lib/mpp/client.rb +9 -0
- data/lib/mpp/credential.rb +20 -0
- data/lib/mpp/errors.rb +190 -0
- data/lib/mpp/expires.rb +60 -0
- data/lib/mpp/extensions/mcp/capabilities.rb +23 -0
- data/lib/mpp/extensions/mcp/constants.rb +17 -0
- data/lib/mpp/extensions/mcp/decorator.rb +44 -0
- data/lib/mpp/extensions/mcp/errors.rb +110 -0
- data/lib/mpp/extensions/mcp/types.rb +205 -0
- data/lib/mpp/extensions/mcp/verify.rb +152 -0
- data/lib/mpp/extensions/mcp.rb +16 -0
- data/lib/mpp/json.rb +32 -0
- data/lib/mpp/methods/stripe/charge_intent.rb +90 -0
- data/lib/mpp/methods/stripe/client_method.rb +42 -0
- data/lib/mpp/methods/stripe/defaults.rb +14 -0
- data/lib/mpp/methods/stripe/stripe_method.rb +63 -0
- data/lib/mpp/methods/stripe.rb +14 -0
- data/lib/mpp/methods/tempo/account.rb +52 -0
- data/lib/mpp/methods/tempo/attribution.rb +112 -0
- data/lib/mpp/methods/tempo/client_method.rb +259 -0
- data/lib/mpp/methods/tempo/defaults.rb +77 -0
- data/lib/mpp/methods/tempo/fee_payer_envelope.rb +74 -0
- data/lib/mpp/methods/tempo/intents.rb +377 -0
- data/lib/mpp/methods/tempo/keychain.rb +31 -0
- data/lib/mpp/methods/tempo/proof.rb +127 -0
- data/lib/mpp/methods/tempo/rpc.rb +60 -0
- data/lib/mpp/methods/tempo/schemas.rb +96 -0
- data/lib/mpp/methods/tempo/transaction.rb +144 -0
- data/lib/mpp/methods/tempo.rb +22 -0
- data/lib/mpp/parsing.rb +252 -0
- data/lib/mpp/receipt.rb +31 -0
- data/lib/mpp/secure_compare.rb +25 -0
- data/lib/mpp/server/decorator.rb +32 -0
- data/lib/mpp/server/defaults.rb +45 -0
- data/lib/mpp/server/intent.rb +40 -0
- data/lib/mpp/server/method.rb +27 -0
- data/lib/mpp/server/middleware.rb +51 -0
- data/lib/mpp/server/mpp_handler.rb +97 -0
- data/lib/mpp/server/verify.rb +129 -0
- data/lib/mpp/server.rb +15 -0
- data/lib/mpp/store.rb +49 -0
- data/lib/mpp/units.rb +57 -0
- data/lib/mpp/version.rb +6 -0
- data/lib/mpp-rb.rb +3 -0
- data/lib/mpp.rb +68 -0
- metadata +111 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "defaults"
|
|
5
|
+
require_relative "transaction"
|
|
6
|
+
|
|
7
|
+
module Mpp
|
|
8
|
+
module Methods
|
|
9
|
+
module Tempo
|
|
10
|
+
DEFAULT_GAS_LIMIT = 1_000_000
|
|
11
|
+
EXPIRING_NONCE_KEY = (1 << 256) - 1 # U256::MAX
|
|
12
|
+
FEE_PAYER_VALID_BEFORE_SECS = 25
|
|
13
|
+
|
|
14
|
+
class TransactionError < StandardError; end
|
|
15
|
+
|
|
16
|
+
# Tempo payment method implementation.
|
|
17
|
+
# Handles client-side credential creation for Tempo payments.
|
|
18
|
+
class TempoMethod
|
|
19
|
+
attr_reader :name, :account, :fee_payer, :root_account, :rpc_url,
|
|
20
|
+
:chain_id, :currency, :recipient, :decimals, :client_id,
|
|
21
|
+
:expected_recipients
|
|
22
|
+
attr_accessor :intents
|
|
23
|
+
|
|
24
|
+
def initialize(account: nil, fee_payer: nil, root_account: nil,
|
|
25
|
+
rpc_url: Defaults::RPC_URL, chain_id: nil, currency: nil,
|
|
26
|
+
recipient: nil, decimals: 6, client_id: nil,
|
|
27
|
+
expected_recipients: nil)
|
|
28
|
+
@name = "tempo"
|
|
29
|
+
@account = account
|
|
30
|
+
@fee_payer = fee_payer
|
|
31
|
+
@root_account = root_account
|
|
32
|
+
@rpc_url = rpc_url
|
|
33
|
+
@chain_id = chain_id
|
|
34
|
+
@currency = currency
|
|
35
|
+
@recipient = recipient
|
|
36
|
+
@decimals = decimals
|
|
37
|
+
@client_id = client_id
|
|
38
|
+
@expected_recipients = expected_recipients&.map(&:downcase)&.to_set
|
|
39
|
+
@intents = {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Create a credential to satisfy the given challenge.
|
|
43
|
+
#
|
|
44
|
+
# mode: :pull (default) — return signed transaction for server to broadcast
|
|
45
|
+
# :push — broadcast on-chain, return transaction hash
|
|
46
|
+
# :proof — zero-amount transaction proving account ownership
|
|
47
|
+
def create_credential(challenge, mode: nil)
|
|
48
|
+
raise ArgumentError, "No account configured for signing" unless @account
|
|
49
|
+
raise ArgumentError, "Unsupported intent: #{challenge.intent}" unless challenge.intent == "charge"
|
|
50
|
+
|
|
51
|
+
mode ||= :pull
|
|
52
|
+
request = challenge.request
|
|
53
|
+
method_details = request["methodDetails"]
|
|
54
|
+
method_details = {} unless method_details.is_a?(Hash)
|
|
55
|
+
|
|
56
|
+
validate_recipients(request, method_details) if @expected_recipients
|
|
57
|
+
|
|
58
|
+
use_fee_payer = method_details.fetch("feePayer", false)
|
|
59
|
+
|
|
60
|
+
nonce_key = request.fetch("nonce_key", 0)
|
|
61
|
+
if nonce_key.is_a?(String)
|
|
62
|
+
nonce_key = nonce_key.start_with?("0x") ? nonce_key.to_i(16) : nonce_key.to_i
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
memo = method_details["memo"]
|
|
66
|
+
memo ||= Attribution.encode(server_id: challenge.realm, client_id: @client_id, challenge_id: challenge.id)
|
|
67
|
+
|
|
68
|
+
# Resolve RPC URL from challenge's chainId
|
|
69
|
+
resolved_rpc_url = @rpc_url
|
|
70
|
+
expected_chain_id = nil
|
|
71
|
+
challenge_chain_id = method_details["chainId"]
|
|
72
|
+
if challenge_chain_id
|
|
73
|
+
begin
|
|
74
|
+
parsed_chain_id = Integer(challenge_chain_id)
|
|
75
|
+
resolved = Defaults::CHAIN_RPC_URLS[parsed_chain_id]
|
|
76
|
+
if resolved
|
|
77
|
+
resolved_rpc_url = resolved
|
|
78
|
+
expected_chain_id = parsed_chain_id
|
|
79
|
+
end
|
|
80
|
+
rescue ArgumentError, TypeError
|
|
81
|
+
# ignore
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
expected_chain_id ||= @chain_id
|
|
86
|
+
|
|
87
|
+
# Proof mode: sign EIP-712 typed data (no transaction needed)
|
|
88
|
+
if mode == :proof
|
|
89
|
+
chain_id = expected_chain_id || @chain_id
|
|
90
|
+
raise ArgumentError, "chain_id required for proof mode" unless chain_id
|
|
91
|
+
|
|
92
|
+
signature = Proof.sign(
|
|
93
|
+
account: @account,
|
|
94
|
+
chain_id: chain_id,
|
|
95
|
+
challenge_id: challenge.id
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return Mpp::Credential.new(
|
|
99
|
+
challenge: challenge.to_echo,
|
|
100
|
+
payload: {"type" => "proof", "signature" => signature},
|
|
101
|
+
source: Proof.source(address: @account.address, chain_id: chain_id)
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
raw_tx, chain_id = build_tempo_transfer(
|
|
106
|
+
amount: request["amount"],
|
|
107
|
+
currency: request["currency"],
|
|
108
|
+
recipient: request["recipient"],
|
|
109
|
+
nonce_key: nonce_key,
|
|
110
|
+
memo: memo,
|
|
111
|
+
rpc_url: resolved_rpc_url,
|
|
112
|
+
expected_chain_id: expected_chain_id,
|
|
113
|
+
awaiting_fee_payer: use_fee_payer
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
payload = if mode == :push
|
|
117
|
+
tx_hash = Rpc.call(resolved_rpc_url, "eth_sendRawTransaction", [raw_tx])
|
|
118
|
+
raise TransactionError, "No transaction hash returned" unless tx_hash
|
|
119
|
+
{"type" => "hash", "hash" => tx_hash}
|
|
120
|
+
else
|
|
121
|
+
{"type" => "transaction", "signature" => raw_tx}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
Mpp::Credential.new(
|
|
125
|
+
challenge: challenge.to_echo,
|
|
126
|
+
payload: payload,
|
|
127
|
+
source: "did:pkh:eip155:#{chain_id}:#{@account.address}"
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Transform request - adds default methodDetails if needed.
|
|
132
|
+
def transform_request(request, _credential)
|
|
133
|
+
request
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def validate_recipients(request, method_details)
|
|
139
|
+
recipient = request["recipient"]
|
|
140
|
+
if recipient && !@expected_recipients.include?(recipient.downcase)
|
|
141
|
+
raise ArgumentError, "Unexpected recipient: #{recipient}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
splits = method_details["splits"]
|
|
145
|
+
return unless splits.is_a?(Array)
|
|
146
|
+
|
|
147
|
+
splits.each do |split|
|
|
148
|
+
addr = split["recipient"]
|
|
149
|
+
next unless addr
|
|
150
|
+
unless @expected_recipients.include?(addr.downcase)
|
|
151
|
+
raise ArgumentError, "Unexpected split recipient: #{addr}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def build_tempo_transfer(amount:, currency:, recipient:, nonce_key: 0,
|
|
157
|
+
memo: nil, rpc_url: nil, expected_chain_id: nil,
|
|
158
|
+
awaiting_fee_payer: false)
|
|
159
|
+
raise ArgumentError, "No account configured" unless @account
|
|
160
|
+
|
|
161
|
+
resolved_rpc = rpc_url || @rpc_url
|
|
162
|
+
|
|
163
|
+
transfer_data = if memo
|
|
164
|
+
encode_transfer_with_memo(recipient, Integer(amount), memo)
|
|
165
|
+
else
|
|
166
|
+
encode_transfer(recipient, Integer(amount))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
chain_id, on_chain_nonce, gas_price = Rpc.get_tx_params(resolved_rpc, @account.address)
|
|
170
|
+
|
|
171
|
+
if expected_chain_id && chain_id != expected_chain_id
|
|
172
|
+
raise TransactionError,
|
|
173
|
+
"Chain ID mismatch: RPC returned #{chain_id}, expected #{expected_chain_id} from challenge"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if awaiting_fee_payer
|
|
177
|
+
resolved_nonce_key = EXPIRING_NONCE_KEY
|
|
178
|
+
resolved_nonce = 0
|
|
179
|
+
valid_before = Time.now.to_i + FEE_PAYER_VALID_BEFORE_SECS
|
|
180
|
+
else
|
|
181
|
+
resolved_nonce_key = nonce_key
|
|
182
|
+
resolved_nonce = on_chain_nonce
|
|
183
|
+
valid_before = nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
gas_limit = DEFAULT_GAS_LIMIT
|
|
187
|
+
begin
|
|
188
|
+
estimated = Rpc.estimate_gas(resolved_rpc, @account.address, currency, transfer_data)
|
|
189
|
+
gas_limit = [gas_limit, estimated + 5_000].max
|
|
190
|
+
rescue
|
|
191
|
+
# fallback to default
|
|
192
|
+
end
|
|
193
|
+
Transaction.build_signed_transfer(
|
|
194
|
+
account: @account,
|
|
195
|
+
chain_id: chain_id,
|
|
196
|
+
gas_limit: gas_limit,
|
|
197
|
+
gas_price: gas_price,
|
|
198
|
+
nonce: resolved_nonce,
|
|
199
|
+
nonce_key: resolved_nonce_key,
|
|
200
|
+
currency: currency,
|
|
201
|
+
transfer_data: transfer_data,
|
|
202
|
+
valid_before: valid_before,
|
|
203
|
+
awaiting_fee_payer: awaiting_fee_payer
|
|
204
|
+
)
|
|
205
|
+
rescue LoadError => e
|
|
206
|
+
raise TransactionError, e.message
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def encode_transfer(to, amount)
|
|
210
|
+
selector = "a9059cbb"
|
|
211
|
+
to_padded = to.delete_prefix("0x").downcase.rjust(64, "0")
|
|
212
|
+
amount_padded = amount.to_s(16).rjust(64, "0")
|
|
213
|
+
"0x#{selector}#{to_padded}#{amount_padded}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def encode_transfer_with_memo(to, amount, memo)
|
|
217
|
+
selector = "95777d59"
|
|
218
|
+
to_padded = to.delete_prefix("0x").downcase.rjust(64, "0")
|
|
219
|
+
amount_padded = amount.to_s(16).rjust(64, "0")
|
|
220
|
+
memo_clean = memo.delete_prefix("0x")
|
|
221
|
+
unless memo_clean.length == 64
|
|
222
|
+
raise ArgumentError,
|
|
223
|
+
"memo must be exactly 32 bytes (64 hex chars), got #{memo_clean.length}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
"0x#{selector}#{to_padded}#{amount_padded}#{memo_clean.downcase}"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Factory function to create a configured TempoMethod.
|
|
231
|
+
def self.tempo(intents:, account: nil, fee_payer: nil, chain_id: nil, rpc_url: nil,
|
|
232
|
+
root_account: nil, currency: nil, recipient: nil, decimals: 6, client_id: nil,
|
|
233
|
+
expected_recipients: nil)
|
|
234
|
+
rpc_url ||= chain_id ? Defaults.rpc_url_for_chain(chain_id) : Defaults::RPC_URL
|
|
235
|
+
currency ||= Defaults.default_currency_for_chain(chain_id)
|
|
236
|
+
|
|
237
|
+
method = TempoMethod.new(
|
|
238
|
+
account: account,
|
|
239
|
+
fee_payer: fee_payer,
|
|
240
|
+
rpc_url: rpc_url,
|
|
241
|
+
chain_id: chain_id,
|
|
242
|
+
root_account: root_account,
|
|
243
|
+
currency: currency,
|
|
244
|
+
recipient: recipient,
|
|
245
|
+
decimals: decimals,
|
|
246
|
+
client_id: client_id,
|
|
247
|
+
expected_recipients: expected_recipients
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
intents.each_value do |intent|
|
|
251
|
+
intent.rpc_url = rpc_url if intent.respond_to?(:rpc_url=) && intent.rpc_url.nil?
|
|
252
|
+
intent.instance_variable_set(:@_method, method) if intent.respond_to?(:fee_payer)
|
|
253
|
+
end
|
|
254
|
+
method.intents = intents.dup
|
|
255
|
+
method
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mpp
|
|
5
|
+
module Methods
|
|
6
|
+
module Tempo
|
|
7
|
+
module Defaults
|
|
8
|
+
# Mainnet
|
|
9
|
+
CHAIN_ID = 4217
|
|
10
|
+
RPC_URL = "https://rpc.tempo.xyz"
|
|
11
|
+
PATH_USD = "0x20c0000000000000000000000000000000000000"
|
|
12
|
+
USDC = "0x20C000000000000000000000b9537d11c60E8b50"
|
|
13
|
+
PATH_USD_DECIMALS = 6
|
|
14
|
+
|
|
15
|
+
# Testnet (Moderato)
|
|
16
|
+
TESTNET_CHAIN_ID = 42_431
|
|
17
|
+
TESTNET_RPC_URL = "https://rpc.moderato.tempo.xyz"
|
|
18
|
+
|
|
19
|
+
# Testnet only - fee payer service sponsors gas on testnet
|
|
20
|
+
DEFAULT_FEE_PAYER_URL = "https://sponsor.moderato.tempo.xyz"
|
|
21
|
+
|
|
22
|
+
# Chain ID -> default currency mapping
|
|
23
|
+
DEFAULT_CURRENCIES = T.let({
|
|
24
|
+
CHAIN_ID => USDC,
|
|
25
|
+
TESTNET_CHAIN_ID => PATH_USD
|
|
26
|
+
}.freeze, T::Hash[T.untyped, T.untyped])
|
|
27
|
+
|
|
28
|
+
# Chain ID -> default RPC URL mapping
|
|
29
|
+
CHAIN_RPC_URLS = T.let({
|
|
30
|
+
CHAIN_ID => RPC_URL,
|
|
31
|
+
TESTNET_CHAIN_ID => TESTNET_RPC_URL
|
|
32
|
+
}.freeze, T::Hash[T.untyped, T.untyped])
|
|
33
|
+
|
|
34
|
+
# Chain ID -> escrow contract address mapping
|
|
35
|
+
ESCROW_CONTRACTS = T.let({
|
|
36
|
+
CHAIN_ID => "0x33b901018174DDabE4841042ab76ba85D4e24f25",
|
|
37
|
+
TESTNET_CHAIN_ID => "0xe1c4d3dce17bc111181ddf716f75bae49e61a336"
|
|
38
|
+
}.freeze, T::Hash[T.untyped, T.untyped])
|
|
39
|
+
|
|
40
|
+
extend T::Sig
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
sig { params(chain_id: Integer).returns(String) }
|
|
45
|
+
def rpc_url_for_chain(chain_id)
|
|
46
|
+
url = CHAIN_RPC_URLS[chain_id]
|
|
47
|
+
return url if url
|
|
48
|
+
|
|
49
|
+
Kernel.raise ArgumentError,
|
|
50
|
+
"Unknown chain_id #{chain_id}. Known chains: #{CHAIN_RPC_URLS.keys}. Pass rpc_url explicitly."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { params(chain_id: T.nilable(Integer)).returns(String) }
|
|
54
|
+
def default_currency_for_chain(chain_id)
|
|
55
|
+
return PATH_USD if chain_id.nil?
|
|
56
|
+
|
|
57
|
+
DEFAULT_CURRENCIES.fetch(chain_id, PATH_USD)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { params(chain_id: T.nilable(Integer), testnet: T::Boolean).returns(String) }
|
|
61
|
+
def resolve_currency(chain_id: nil, testnet: false)
|
|
62
|
+
id = chain_id || (testnet ? TESTNET_CHAIN_ID : CHAIN_ID)
|
|
63
|
+
DEFAULT_CURRENCIES.fetch(id, PATH_USD)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig { params(chain_id: Integer).returns(String) }
|
|
67
|
+
def escrow_contract_for_chain(chain_id)
|
|
68
|
+
addr = ESCROW_CONTRACTS[chain_id]
|
|
69
|
+
return addr if addr
|
|
70
|
+
|
|
71
|
+
Kernel.raise ArgumentError,
|
|
72
|
+
"Unknown chain_id #{chain_id}. Known chains: #{ESCROW_CONTRACTS.keys}. Pass escrow_contract explicitly."
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mpp
|
|
5
|
+
module Methods
|
|
6
|
+
module Tempo
|
|
7
|
+
module FeePayer
|
|
8
|
+
TYPE_ID = 0x78
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Encode a sender-signed transaction as a 0x78 fee payer envelope.
|
|
13
|
+
# Requires the `rlp` gem.
|
|
14
|
+
#
|
|
15
|
+
# Wire format: 0x78 || RLP([fields...])
|
|
16
|
+
def encode(signed_tx)
|
|
17
|
+
Kernel.require "rlp"
|
|
18
|
+
|
|
19
|
+
sender_sig = signed_tx.sender_signature
|
|
20
|
+
sig_bytes = sender_sig.respond_to?(:to_bytes) ? sender_sig.to_bytes : sender_sig.to_s.b
|
|
21
|
+
sender_addr = signed_tx.sender_address.to_s.b
|
|
22
|
+
|
|
23
|
+
fields = [
|
|
24
|
+
signed_tx.chain_id,
|
|
25
|
+
signed_tx.max_priority_fee_per_gas,
|
|
26
|
+
signed_tx.max_fee_per_gas,
|
|
27
|
+
signed_tx.gas_limit,
|
|
28
|
+
signed_tx.calls.map(&:as_rlp_list),
|
|
29
|
+
signed_tx.access_list.map(&:as_rlp_list),
|
|
30
|
+
signed_tx.nonce_key,
|
|
31
|
+
signed_tx.nonce,
|
|
32
|
+
encode_optional_uint(signed_tx.valid_before),
|
|
33
|
+
encode_optional_uint(signed_tx.valid_after),
|
|
34
|
+
signed_tx.fee_token ? signed_tx.fee_token.to_s.b : "".b,
|
|
35
|
+
sender_addr,
|
|
36
|
+
signed_tx.tempo_authorization_list.to_a
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
fields << RLP.decode(signed_tx.key_authorization) if signed_tx.key_authorization
|
|
40
|
+
fields << sig_bytes
|
|
41
|
+
|
|
42
|
+
[TYPE_ID].pack("C") + RLP.include(fields)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Decode a 0x78 fee payer envelope.
|
|
46
|
+
#
|
|
47
|
+
# Returns [decoded_fields, sender_address_bytes, sender_signature_bytes, key_authorization_or_nil]
|
|
48
|
+
def decode(data)
|
|
49
|
+
Kernel.require "rlp"
|
|
50
|
+
|
|
51
|
+
Kernel.raise ArgumentError, "Not a fee payer envelope (expected 0x78 prefix)" unless data.getbyte(0) == TYPE_ID
|
|
52
|
+
|
|
53
|
+
decoded = RLP.decode(data[1..])
|
|
54
|
+
Kernel.raise ArgumentError, "Malformed fee payer envelope" unless decoded.is_a?(Array) && decoded.length >= 14
|
|
55
|
+
|
|
56
|
+
sender_address = decoded[11]
|
|
57
|
+
sender_signature = decoded[-1]
|
|
58
|
+
|
|
59
|
+
# 15 fields = key_authorization present (index 13), signature at 14
|
|
60
|
+
# 14 fields = no key_authorization, signature at 13
|
|
61
|
+
key_authorization = (RLP.include(decoded[13]) if decoded.length == 15)
|
|
62
|
+
|
|
63
|
+
[decoded, sender_address.to_s.b, sender_signature.to_s.b, key_authorization]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def encode_optional_uint(value)
|
|
67
|
+
return "".b unless value
|
|
68
|
+
|
|
69
|
+
value.is_a?(Integer) ? value : value.to_i
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|