eth 0.4.12 → 0.5.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.
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