eth 0.4.12 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/eth/abi.rb ADDED
@@ -0,0 +1,390 @@
1
+ # Copyright (c) 2016-2022 The Ruby-Eth Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -*- encoding : ascii-8bit -*-
16
+
17
+ require "konstructor"
18
+
19
+ require "eth/abi/constant"
20
+ require "eth/abi/type"
21
+
22
+ # Provides the `Eth` module.
23
+ module Eth
24
+
25
+ # Provides a Ruby implementation of the Ethereum Applicatoin Binary Interface (ABI).
26
+ # ref: https://docs.soliditylang.org/en/develop/abi-spec.html
27
+ module Abi
28
+ extend self
29
+ include Constant
30
+
31
+ # Provides a special encoding error if anything fails to encode.
32
+ class EncodingError < StandardError; end
33
+
34
+ # Provides a special decoding error if anything fails to decode.
35
+ class DecodingError < StandardError; end
36
+
37
+ # Provides a special out-of-bounds error for values.
38
+ class ValueOutOfBounds < StandardError; end
39
+
40
+ # Encodes Application Binary Interface (ABI) data. It accepts multiple
41
+ # arguments and encodes using the head/tail mechanism.
42
+ #
43
+ # @param types [Array] types to be ABI-encoded.
44
+ # @param args [Array] values to be ABI-encoded.
45
+ # @return [String] the encoded ABI data.
46
+ def encode(types, args)
47
+
48
+ # prase all types
49
+ parsed_types = types.map { |t| Type.parse(t) }
50
+
51
+ # prepare the "head"
52
+ head_size = (0...args.size)
53
+ .map { |i| parsed_types[i].size or 32 }
54
+ .reduce(0, &:+)
55
+ head, tail = "", ""
56
+
57
+ # encode types and arguments
58
+ args.each_with_index do |arg, i|
59
+ if parsed_types[i].is_dynamic?
60
+ head += encode_type Type.size_type, head_size + tail.size
61
+ tail += encode_type parsed_types[i], arg
62
+ else
63
+ head += encode_type parsed_types[i], arg
64
+ end
65
+ end
66
+
67
+ # return the encoded ABI blob
68
+ return "#{head}#{tail}"
69
+ end
70
+
71
+ # Encodes a specific value, either static or dynamic.
72
+ #
73
+ # @param type [Eth::Abi::Type] type to be encoded.
74
+ # @param arg [String, Number] value to be encoded.
75
+ # @return [String] the encoded type.
76
+ # @raise [EncodingError] if value does not match type.
77
+ def encode_type(type, arg)
78
+ if %w(string bytes).include? type.base_type and type.sub_type.empty?
79
+ raise EncodingError, "Argument must be a String" unless arg.instance_of? String
80
+
81
+ # encodes strings and bytes
82
+ size = encode_type Type.size_type, arg.size
83
+ padding = BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)
84
+ return "#{size}#{arg}#{padding}"
85
+ elsif type.is_dynamic?
86
+ raise EncodingError, "Argument must be an Array" unless arg.instance_of? Array
87
+
88
+ # encodes dynamic-sized arrays
89
+ head, tail = "", ""
90
+ head += encode_type Type.size_type, arg.size
91
+ nested_sub = type.nested_sub
92
+ nested_sub_size = type.nested_sub.size
93
+ arg.size.times do |i|
94
+
95
+ # ref https://github.com/ethereum/tests/issues/691
96
+ raise NotImplementedError, "Encoding dynamic arrays with nested dynamic sub-types is not implemented for ABI." if nested_sub.is_dynamic?
97
+ head += encode_type nested_sub, arg[i]
98
+ end
99
+ return "#{head}#{tail}"
100
+ else
101
+ if type.dimensions.empty?
102
+
103
+ # encode a primitive type
104
+ return encode_primitive_type type, arg
105
+ else
106
+
107
+ # encode static-size arrays
108
+ return arg.map { |x| encode_type(type.nested_sub, x) }.join
109
+ end
110
+ end
111
+ end
112
+
113
+ # Encodes primitive types.
114
+ #
115
+ # @param type [Eth::Abi::Type] type to be encoded.
116
+ # @param arg [String, Number] value to be encoded.
117
+ # @return [String] the encoded primitive type.
118
+ # @raise [EncodingError] if value does not match type.
119
+ # @raise [ValueOutOfBounds] if value is out of bounds for type.
120
+ # @raise [EncodingError] if encoding fails for type.
121
+ def encode_primitive_type(type, arg)
122
+ case type.base_type
123
+ when "uint"
124
+ return encode_uint arg, type
125
+ when "bool"
126
+ return encode_bool arg
127
+ when "int"
128
+ return encode_int arg, type
129
+ when "ureal", "ufixed"
130
+ return encode_ufixed arg, type
131
+ when "real", "fixed"
132
+ return encode_fixed arg, type
133
+ when "string", "bytes"
134
+ return encode_bytes arg, type
135
+ when "hash"
136
+ return encode_hash arg, type
137
+ when "address"
138
+ return encode_address arg
139
+ else
140
+ raise EncodingError, "Unhandled type: #{type.base_type} #{type.sub_type}"
141
+ end
142
+ end
143
+
144
+ # Decodes Application Binary Interface (ABI) data. It accepts multiple
145
+ # arguments and decodes using the head/tail mechanism.
146
+ #
147
+ # @param types [Array] the ABI to be decoded.
148
+ # @param data [String] ABI data to be decoded.
149
+ # @return [Array] the decoded ABI data.
150
+ def decode(types, data)
151
+
152
+ # accept hex abi but decode it first
153
+ data = Util.hex_to_bin data if Util.is_hex? data
154
+
155
+ # parse all types
156
+ parsed_types = types.map { |t| Type.parse(t) }
157
+
158
+ # prepare output data
159
+ outputs = [nil] * types.size
160
+ start_positions = [nil] * types.size + [data.size]
161
+ pos = 0
162
+ parsed_types.each_with_index do |t, i|
163
+ if t.is_dynamic?
164
+
165
+ # record start position for dynamic type
166
+ start_positions[i] = Util.deserialize_big_endian_to_int(data[pos, 32])
167
+ j = i - 1
168
+ while j >= 0 and start_positions[j].nil?
169
+ start_positions[j] = start_positions[i]
170
+ j -= 1
171
+ end
172
+ pos += 32
173
+ else
174
+
175
+ # get data directly for static types
176
+ outputs[i] = data[pos, t.size]
177
+ pos += t.size
178
+ end
179
+ end
180
+
181
+ # add start position equal the length of the entire data
182
+ j = types.size - 1
183
+ while j >= 0 and start_positions[j].nil?
184
+ start_positions[j] = start_positions[types.size]
185
+ j -= 1
186
+ end
187
+ raise DecodingError, "Not enough data for head" unless pos <= data.size
188
+
189
+ # add dynamic types
190
+ parsed_types.each_with_index do |t, i|
191
+ if t.is_dynamic?
192
+ offset, next_offset = start_positions[i, 2]
193
+ outputs[i] = data[offset...next_offset]
194
+ end
195
+ end
196
+
197
+ # return the decoded ABI types and data
198
+ return parsed_types.zip(outputs).map { |(type, out)| decode_type(type, out) }
199
+ end
200
+
201
+ # Decodes a specific value, either static or dynamic.
202
+ #
203
+ # @param type [Eth::Abi::Type] type to be decoded.
204
+ # @param arg [String] encoded type data string.
205
+ # @return [String] the decoded data for the type.
206
+ # @raise [DecodingError] if decoding fails for type.
207
+ def decode_type(type, arg)
208
+ if %w(string bytes).include?(type.base_type) and type.sub_type.empty?
209
+ l = Util.deserialize_big_endian_to_int arg[0, 32]
210
+ data = arg[32..-1]
211
+ raise DecodingError, "Wrong data size for string/bytes object" unless data.size == Util.ceil32(l)
212
+
213
+ # decoded strings and bytes
214
+ return data[0, l]
215
+ elsif type.is_dynamic?
216
+ l = Util.deserialize_big_endian_to_int arg[0, 32]
217
+ nested_sub = type.nested_sub
218
+
219
+ # ref https://github.com/ethereum/tests/issues/691
220
+ raise NotImplementedError, "Decoding dynamic arrays with nested dynamic sub-types is not implemented for ABI." if nested_sub.is_dynamic?
221
+
222
+ # decoded dynamic-sized arrays
223
+ return (0...l).map { |i| decode_type(nested_sub, arg[32 + nested_sub.size * i, nested_sub.size]) }
224
+ elsif !type.dimensions.empty?
225
+ l = type.dimensions.last[0]
226
+ nested_sub = type.nested_sub
227
+
228
+ # decoded static-size arrays
229
+ return (0...l).map { |i| decode_type(nested_sub, arg[nested_sub.size * i, nested_sub.size]) }
230
+ else
231
+
232
+ # decoded primitive types
233
+ return decode_primitive_type type, arg
234
+ end
235
+ end
236
+
237
+ # Decodes primitive types.
238
+ #
239
+ # @param type [Eth::Abi::Type] type to be decoded.
240
+ # @param data [String] encoded primitive type data string.
241
+ # @return [String] the decoded data for the type.
242
+ # @raise [DecodingError] if decoding fails for type.
243
+ def decode_primitive_type(type, data)
244
+ case type.base_type
245
+ when "address"
246
+
247
+ # decoded address with 0x-prefix
248
+ return "0x#{Util.bin_to_hex data[12..-1]}"
249
+ when "string", "bytes"
250
+ if type.sub_type.empty?
251
+ size = Util.deserialize_big_endian_to_int data[0, 32]
252
+
253
+ # decoded dynamic-sized array
254
+ return data[32..-1][0, size]
255
+ else
256
+
257
+ # decoded static-sized array
258
+ return data[0, type.sub_type.to_i]
259
+ end
260
+ when "hash"
261
+
262
+ # decoded hash
263
+ return data[(32 - type.sub_type.to_i), type.sub_type.to_i]
264
+ when "uint"
265
+
266
+ # decoded unsigned integer
267
+ return Util.deserialize_big_endian_to_int data
268
+ when "int"
269
+ u = Util.deserialize_big_endian_to_int data
270
+ i = u >= 2 ** (type.sub_type.to_i - 1) ? (u - 2 ** type.sub_type.to_i) : u
271
+
272
+ # decoded integer
273
+ return i
274
+ when "ureal", "ufixed"
275
+ high, low = type.sub_type.split("x").map(&:to_i)
276
+
277
+ # decoded unsigned fixed point numeric
278
+ return Util.deserialize_big_endian_to_int(data) * 1.0 / 2 ** low
279
+ when "real", "fixed"
280
+ high, low = type.sub_type.split("x").map(&:to_i)
281
+ u = Util.deserialize_big_endian_to_int data
282
+ i = u >= 2 ** (high + low - 1) ? (u - 2 ** (high + low)) : u
283
+
284
+ # decoded fixed point numeric
285
+ return i * 1.0 / 2 ** low
286
+ when "bool"
287
+
288
+ # decoded boolean
289
+ return data[-1] == BYTE_ONE
290
+ else
291
+ raise DecodingError, "Unknown primitive type: #{type.base_type}"
292
+ end
293
+ end
294
+
295
+ private
296
+
297
+ def encode_uint(arg, type)
298
+ raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > UINT_MAX or arg < UINT_MIN
299
+ real_size = type.sub_type.to_i
300
+ i = arg.to_i
301
+ raise ValueOutOfBounds, arg unless i >= 0 and i < 2 ** real_size
302
+ return Util.zpad_int i
303
+ end
304
+
305
+ def encode_int(arg, type)
306
+ raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > INT_MAX or arg < INT_MIN
307
+ real_size = type.sub_type.to_i
308
+ i = arg.to_i
309
+ raise ValueOutOfBounds, arg unless i >= -2 ** (real_size - 1) and i < 2 ** (real_size - 1)
310
+ return Util.zpad_int(i % 2 ** type.sub_type.to_i)
311
+ end
312
+
313
+ def encode_bool(arg)
314
+ raise EncodingError, "Argument is not bool: #{arg}" unless arg.instance_of? TrueClass or arg.instance_of? FalseClass
315
+ return Util.zpad_int(arg ? 1 : 0)
316
+ end
317
+
318
+ def encode_ufixed(arg, type)
319
+ high, low = type.sub_type.split("x").map(&:to_i)
320
+ raise ValueOutOfBounds, arg unless arg >= 0 and arg < 2 ** high
321
+ return Util.zpad_int((arg * 2 ** low).to_i)
322
+ end
323
+
324
+ def encode_fixed(arg, type)
325
+ high, low = type.sub_type.split("x").map(&:to_i)
326
+ raise ValueOutOfBounds, arg unless arg >= -2 ** (high - 1) and arg < 2 ** (high - 1)
327
+ i = (arg * 2 ** low).to_i
328
+ return Util.zpad_int(i % 2 ** (high + low))
329
+ end
330
+
331
+ def encode_bytes(arg, type)
332
+ raise EncodingError, "Expecting String: #{arg}" unless arg.instance_of? String
333
+ if type.sub_type.empty?
334
+ size = Util.zpad_int arg.size
335
+ padding = BYTE_ZERO * (Util.ceil32(arg.size) - arg.size)
336
+
337
+ # variable length string/bytes
338
+ return "#{size}#{arg}#{padding}"
339
+ else
340
+ raise ValueOutOfBounds, arg unless arg.size <= type.sub_type.to_i
341
+ padding = BYTE_ZERO * (32 - arg.size)
342
+
343
+ # fixed length string/bytes
344
+ return "#{arg}#{padding}"
345
+ end
346
+ end
347
+
348
+ def encode_hash(arg, type)
349
+ size = type.sub_type.to_i
350
+ raise EncodingError, "Argument too long: #{arg}" unless size > 0 and size <= 32
351
+ if arg.is_a? Integer
352
+
353
+ # hash from integer
354
+ return Util.zpad_int arg
355
+ elsif arg.size == size
356
+
357
+ # hash from encoded hash
358
+ return Util.zpad arg, 32
359
+ elsif arg.size == size * 2
360
+
361
+ # hash from hexa-decimal hash
362
+ return Util.zpad_hex arg
363
+ else
364
+ raise EncodingError, "Could not parse hash: #{arg}"
365
+ end
366
+ end
367
+
368
+ def encode_address(arg)
369
+ if arg.is_a? Integer
370
+
371
+ # address from integer
372
+ return Util.zpad_int arg
373
+ elsif arg.size == 20
374
+
375
+ # address from encoded address
376
+ return Util.zpad arg, 32
377
+ elsif arg.size == 40
378
+
379
+ # address from hexa-decimal address with 0x prefix
380
+ return Util.zpad_hex arg
381
+ elsif arg.size == 42 and arg[0, 2] == "0x"
382
+
383
+ # address from hexa-decimal address
384
+ return Util.zpad_hex arg[2..-1]
385
+ else
386
+ raise EncodingError, "Could not parse address: #{arg}"
387
+ end
388
+ end
389
+ end
390
+ end
data/lib/eth/address.rb CHANGED
@@ -1,10 +1,46 @@
1
+ # Copyright (c) 2016-2022 The Ruby-Eth Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Provides the `Eth` module.
1
16
  module Eth
17
+
18
+ # The `Eth::Address` class to handle checksummed Ethereum addresses.
2
19
  class Address
3
20
 
21
+ # Provides a special checksum error if EIP-55 is violated.
22
+ class CheckSumError < StandardError; end
23
+
24
+ # The prefixed and checksummed Ethereum address.
25
+ attr_reader :address
26
+
27
+ # Constructor of the `Eth::Address` class. Creates a new hex
28
+ # prefixed address.
29
+ #
30
+ # @param address [String] hex string representing an ethereum address.
4
31
  def initialize(address)
5
- @address = Utils.prefix_hex(address)
32
+ unless Util.is_hex? address
33
+ raise CheckSumError, "Unknown address type #{address}!"
34
+ end
35
+ @address = Util.prefix_hex address
36
+ unless self.valid?
37
+ raise CheckSumError, "Invalid address provided #{address}"
38
+ end
6
39
  end
7
40
 
41
+ # Check that the address is valid.
42
+ #
43
+ # @return [Bool] true if valid address.
8
44
  def valid?
9
45
  if !matches_any_format?
10
46
  false
@@ -15,21 +51,23 @@ module Eth
15
51
  end
16
52
  end
17
53
 
54
+ # Generate a checksummed address.
55
+ #
56
+ # @return [String] prefixed hexstring representing an checksummed address.
18
57
  def checksummed
19
- raise "Invalid address: #{address}" unless matches_any_format?
58
+ raise CheckSumError, "Invalid address: #{address}" unless matches_any_format?
20
59
 
21
60
  cased = unprefixed.chars.zip(checksum.chars).map do |char, check|
22
61
  check.match(/[0-7]/) ? char.downcase : char.upcase
23
62
  end
24
63
 
25
- Utils.prefix_hex(cased.join)
64
+ Util.prefix_hex cased.join
26
65
  end
27
66
 
67
+ alias :to_s :checksummed
28
68
 
29
69
  private
30
70
 
31
- attr_reader :address
32
-
33
71
  def checksum_matches?
34
72
  address == checksummed
35
73
  end
@@ -39,24 +77,23 @@ module Eth
39
77
  end
40
78
 
41
79
  def all_uppercase?
42
- address.match(/(?:0[xX])[A-F0-9]{40}/)
80
+ address.match /(?:0[xX])[A-F0-9]{40}/
43
81
  end
44
82
 
45
83
  def all_lowercase?
46
- address.match(/(?:0[xX])[a-f0-9]{40}/)
84
+ address.match /(?:0[xX])[a-f0-9]{40}/
47
85
  end
48
86
 
49
87
  def matches_any_format?
50
- address.match(/\A(?:0[xX])[a-fA-F0-9]{40}\z/)
88
+ address.match /\A(?:0[xX])[a-fA-F0-9]{40}\z/
51
89
  end
52
90
 
53
91
  def checksum
54
- Utils.bin_to_hex(Utils.keccak256 unprefixed.downcase)
92
+ Util.bin_to_hex Util.keccak256 unprefixed.downcase
55
93
  end
56
94
 
57
95
  def unprefixed
58
- Utils.remove_hex_prefix address
96
+ Util.remove_hex_prefix address
59
97
  end
60
-
61
98
  end
62
99
  end
data/lib/eth/chain.rb ADDED
@@ -0,0 +1,148 @@
1
+ # Copyright (c) 2016-2022 The Ruby-Eth Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Provides the `Eth` module.
16
+ module Eth
17
+
18
+ # Encapsulates `Chain` IDs and utilities for EIP-155 compatibility.
19
+ # ref: https://eips.ethereum.org/EIPS/eip-155
20
+ module Chain
21
+ extend self
22
+
23
+ # Provides a special replay protection error if EIP-155 is violated.
24
+ class ReplayProtectionError < StandardError; end
25
+
26
+ # Chain ID for Ethereum mainnet
27
+ ETHEREUM = 1.freeze
28
+
29
+ # Chain ID for Expanse mainnet
30
+ EXPANSE = 2.freeze
31
+
32
+ # Chain ID for Optimistic Ethereum mainnet
33
+ OPTIMISM = 10.freeze
34
+
35
+ # Chain ID for Ethereum Classic mainnet
36
+ CLASSIC = 61.freeze
37
+
38
+ # Chain ID for POA Network mainnet
39
+ POA_NET = 99.freeze
40
+
41
+ # Chain ID for xDAI mainnet
42
+ XDAI = 100.freeze
43
+
44
+ # Chain ID for Arbitrum mainnet
45
+ ARBITRUM = 42161.freeze
46
+
47
+ # Chain ID for Morden (Ethereum) testnet
48
+ MORDEN = 2.freeze
49
+
50
+ # Chain ID for Ropsten testnet
51
+ ROPSTEN = 3.freeze
52
+
53
+ # Chain ID for Rinkeby testnet
54
+ RINKEBY = 4.freeze
55
+
56
+ # Chain ID for Goerli testnet
57
+ GOERLI = 5.freeze
58
+
59
+ # Chain ID for Kotti testnet
60
+ KOTTI = 6.freeze
61
+
62
+ # Chain ID for Kovan testnet
63
+ KOVAN = 42.freeze
64
+
65
+ # Chain ID for Morden (Classic) testnet
66
+ MORDEN_CLASSIC = 62.freeze
67
+
68
+ # Chain ID for Mordor testnet
69
+ MORDOR = 63.freeze
70
+
71
+ # Chain ID for Optimistik Kovan testnet
72
+ KOVAN_OPTIMISM = 69.freeze
73
+
74
+ # Chain ID for Arbitrum xDAI testnet
75
+ XDAI_ARBITRUM = 200.freeze
76
+
77
+ # Chain ID for Optimistic Goerli testnet
78
+ GOERLI_OPTIMISM = 420.freeze
79
+
80
+ # Chain ID for Arbitrum Rinkeby testnet
81
+ RINKEBY_ARBITRUM = 421611.freeze
82
+
83
+ # Chain ID for the geth private network preset
84
+ PRIVATE_GETH = 1337.freeze
85
+
86
+ # Indicates wether the given `v` indicates a legacy chain value
87
+ # without EIP-155 replay protection.
88
+ #
89
+ # @param v [Integer] the signature's `v` value
90
+ # @return [Boolean] true if legacy value
91
+ def is_legacy?(v)
92
+ [27, 28].include? v
93
+ end
94
+
95
+ # Convert a given `v` value to an ECDSA recovery id for the given
96
+ # EIP-155 chain ID.
97
+ #
98
+ # @param v [Integer] the signature's `v` value
99
+ # @param chain_id [Integer] the chain id the signature was generated on.
100
+ # @return [Integer] the recovery id corresponding to `v`.
101
+ # @raise [ReplayProtectionError] if the given `v` is invalid.
102
+ def to_recovery_id(v, chain_id = ETHEREUM)
103
+ e = 0 + 2 * chain_id + 35
104
+ i = 1 + 2 * chain_id + 35
105
+ if [0, 1].include? v
106
+
107
+ # some wallets are using a `v` of 0 or 1 (ledger)
108
+ return v
109
+ elsif is_legacy? v
110
+
111
+ # this is the pre-EIP-155 legacy case
112
+ return v - 27
113
+ elsif [e, i].include? v
114
+
115
+ # this is the EIP-155 case
116
+ return v - 35 - 2 * chain_id
117
+ else
118
+ raise ReplayProtectionError, "Invalid v #{v} value for chain ID #{chain_id}. Invalid chain ID?"
119
+ end
120
+ end
121
+
122
+ # Converts a recovery ID into the expected `v` on a given chain.
123
+ #
124
+ # @param recovery_id [Integer] signature recovery id.
125
+ # @param chain_id [Integer] the chain id the signature was generated on.
126
+ # @return [Integer] the signature's `v` value.
127
+ def to_v(recovery_id, chain_id = nil)
128
+ if chain_id.nil? or chain_id < 1
129
+ v = 27 + recovery_id
130
+ else
131
+ v = 2 * chain_id + 35 + recovery_id
132
+ end
133
+ return v
134
+ end
135
+
136
+ # Converst a `v` value into a chain ID. This does not work for legacy signatures
137
+ # with v < 36 that do not conform with EIP-155.
138
+ #
139
+ # @param v [Integer] the signature's `v` value.
140
+ # @return [Integer] the chain id as per EIP-155 or nil if there is no replay protection.
141
+ def to_chain_id(v)
142
+ return nil if v < 36
143
+ chain_id = (v - 35) / 2
144
+ return nil if chain_id < 1
145
+ return chain_id
146
+ end
147
+ end
148
+ end