eth 0.4.18 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/codeql.yml +6 -2
  3. data/.github/workflows/docs.yml +1 -1
  4. data/.github/workflows/spec.yml +52 -0
  5. data/.gitignore +24 -24
  6. data/.gitmodules +3 -3
  7. data/.yardopts +1 -0
  8. data/AUTHORS.txt +27 -0
  9. data/CHANGELOG.md +63 -13
  10. data/Gemfile +12 -4
  11. data/LICENSE.txt +202 -22
  12. data/README.md +231 -76
  13. data/bin/console +4 -4
  14. data/bin/setup +5 -4
  15. data/codecov.yml +6 -0
  16. data/eth.gemspec +23 -19
  17. data/lib/eth/abi/type.rb +178 -0
  18. data/lib/eth/abi.rb +396 -0
  19. data/lib/eth/address.rb +57 -10
  20. data/lib/eth/api.rb +223 -0
  21. data/lib/eth/chain.rb +151 -0
  22. data/lib/eth/client/http.rb +63 -0
  23. data/lib/eth/client/ipc.rb +50 -0
  24. data/lib/eth/client.rb +232 -0
  25. data/lib/eth/constant.rb +71 -0
  26. data/lib/eth/eip712.rb +184 -0
  27. data/lib/eth/key/decrypter.rb +121 -85
  28. data/lib/eth/key/encrypter.rb +180 -99
  29. data/lib/eth/key.rb +134 -45
  30. data/lib/eth/rlp/decoder.rb +114 -0
  31. data/lib/eth/rlp/encoder.rb +78 -0
  32. data/lib/eth/rlp/sedes/big_endian_int.rb +66 -0
  33. data/lib/eth/rlp/sedes/binary.rb +97 -0
  34. data/lib/eth/rlp/sedes/list.rb +84 -0
  35. data/lib/eth/rlp/sedes.rb +74 -0
  36. data/lib/eth/rlp.rb +63 -0
  37. data/lib/eth/signature.rb +163 -0
  38. data/lib/eth/solidity.rb +75 -0
  39. data/lib/eth/tx/eip1559.rb +337 -0
  40. data/lib/eth/tx/eip2930.rb +329 -0
  41. data/lib/eth/tx/legacy.rb +297 -0
  42. data/lib/eth/tx.rb +269 -146
  43. data/lib/eth/unit.rb +49 -0
  44. data/lib/eth/util.rb +235 -0
  45. data/lib/eth/version.rb +18 -1
  46. data/lib/eth.rb +34 -67
  47. metadata +47 -95
  48. data/.github/workflows/build.yml +0 -36
  49. data/lib/eth/gas.rb +0 -7
  50. data/lib/eth/open_ssl.rb +0 -395
  51. data/lib/eth/secp256k1.rb +0 -5
  52. data/lib/eth/sedes.rb +0 -39
  53. data/lib/eth/utils.rb +0 -126
data/lib/eth/tx.rb CHANGED
@@ -1,196 +1,319 @@
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
+ require "konstructor"
16
+
17
+ require "eth/chain"
18
+ require "eth/tx/eip1559"
19
+ require "eth/tx/eip2930"
20
+ require "eth/tx/legacy"
21
+ require "eth/unit"
22
+
23
+ # Provides the {Eth} module.
1
24
  module Eth
2
- class Tx
3
- include RLP::Sedes::Serializable
4
- extend Sedes
5
-
6
- set_serializable_fields({
7
- nonce: big_endian_int,
8
- gas_price: big_endian_int,
9
- gas_limit: big_endian_int,
10
- to: address,
11
- value: big_endian_int,
12
- data_bin: binary,
13
- v: big_endian_int,
14
- r: big_endian_int,
15
- s: big_endian_int,
16
- })
17
-
18
- attr_writer :signature
19
-
20
- def self.decode(data)
21
- data = Utils.hex_to_bin(data) if data.match(/\A(?:0x)?\h+\Z/)
22
- txh = deserialize(RLP.decode data).to_h
23
-
24
- txh[:chain_id] = Eth.chain_id_from_signature(txh)
25
-
26
- self.new txh
27
- end
28
25
 
29
- def initialize(params)
30
- fields = { v: 0, r: 0, s: 0 }.merge params
31
- fields[:to] = Utils.normalize_address(fields[:to])
26
+ # Provides the `Tx` module supporting various transaction types.
27
+ module Tx
28
+ extend self
32
29
 
33
- self.chain_id = (params[:chain_id]) ? params.delete(:chain_id) : Eth.chain_id
30
+ # Provides a special transaction error if transaction type is unknown.
31
+ class TransactionTypeError < TypeError; end
34
32
 
35
- if params[:data]
36
- self.data = params.delete(:data)
37
- fields[:data_bin] = data_bin
38
- end
39
- serializable_initialize fields
33
+ # Provides an decoder error if transaction cannot be decoded.
34
+ class DecoderError < StandardError; end
40
35
 
41
- check_transaction_validity
42
- end
36
+ # Provides a parameter error if parameter types are invalid.
37
+ class ParameterError < TypeError; end
43
38
 
44
- def unsigned_encoded
45
- us = unsigned
46
- RLP.encode(us, sedes: us.sedes)
47
- end
39
+ # The minimum transaction gas limit required for a value transfer.
40
+ DEFAULT_GAS_LIMIT = 21_000.freeze
48
41
 
49
- def signing_data
50
- Utils.bin_to_prefixed_hex unsigned_encoded
51
- end
42
+ # The "default" transaction gas price of 20 GWei. Do not use.
43
+ DEFAULT_GAS_PRICE = (20 * Unit::GWEI).freeze
52
44
 
53
- def encoded
54
- RLP.encode self
55
- end
45
+ # The calldata gas cost of a non-zero byte as per EIP-2028.
46
+ COST_NON_ZERO_BYTE = 16.freeze
56
47
 
57
- def hex
58
- Utils.bin_to_prefixed_hex encoded
59
- end
48
+ # The calldata gas cost of a zero byte.
49
+ COST_ZERO_BYTE = 4.freeze
60
50
 
61
- def sign(key)
62
- sig = key.sign(unsigned_encoded)
63
- vrs = Utils.v_r_s_for sig
64
- self.v = (self.chain_id) ? ((self.chain_id * 2) + vrs[0] + 8) : vrs[0]
65
- self.r = vrs[1]
66
- self.s = vrs[2]
51
+ # The access list gas cost of a storage key as per EIP-2930.
52
+ COST_STORAGE_KEY = 1_900.freeze
67
53
 
68
- clear_signature
69
- self
70
- end
54
+ # The access list gas cost of an address as per EIP-2930.
55
+ COST_ADDRESS = 2_400.freeze
71
56
 
72
- def to_h
73
- hash_keys.inject({}) do |hash, field|
74
- hash[field] = send field
75
- hash
76
- end
77
- end
57
+ # The maximum transaction gas limit is bound by the block gas limit.
58
+ BLOCK_GAS_LIMIT = 25_000_000.freeze
78
59
 
79
- def from
80
- if ecdsa_signature
81
- public_key = OpenSsl.recover_compact(signature_hash, ecdsa_signature)
82
- Utils.public_key_to_address(public_key) if public_key
83
- end
84
- end
60
+ # The legacy transaction type is 0.
61
+ TYPE_LEGACY = 0x00.freeze
85
62
 
86
- def signature
87
- return @signature if @signature
88
- @signature = { v: v, r: r, s: s } if [v, r, s].all? && (v > 0)
89
- end
63
+ # The EIP-2930 transaction type is 1.
64
+ TYPE_2930 = 0x01.freeze
90
65
 
91
- def ecdsa_signature
92
- return @ecdsa_signature if @ecdsa_signature
66
+ # The EIP-1559 transaction type is 2.
67
+ TYPE_1559 = 0x02.freeze
93
68
 
94
- if [v, r, s].all? && (v > 0)
95
- s_v = (self.chain_id) ? (v - (self.chain_id * 2) - 8) : v
96
- @ecdsa_signature = [
97
- Utils.int_to_base256(s_v),
98
- Utils.zpad_int(r),
99
- Utils.zpad_int(s),
100
- ].join
101
- end
102
- end
69
+ # The zero byte is 0x00.
70
+ ZERO_BYTE = "\x00".freeze
103
71
 
104
- def hash
105
- "0x#{Utils.bin_to_hex Utils.keccak256_rlp(self)}"
106
- end
72
+ # Creates a new transaction of any type for given parameters and chain ID.
73
+ # Required parameters are (optional in brackets):
74
+ # - EIP-1559: chain_id, nonce, priority_fee, max_gas_fee, gas_limit(, from, to,
75
+ # value, data, access_list)
76
+ # - EIP-2930: chain_id, nonce, gas_price, gas_limit, access_list(, from, to,
77
+ # value, data)
78
+ # - Legacy: nonce, gas_price, gas_lmit(, from, to, value, data)
79
+ #
80
+ # @param params [Hash] all necessary transaction fields.
81
+ # @param chain_id [Integer] the EIP-155 Chain ID (legacy transactions only).
82
+ def new(params, chain_id = Chain::ETHEREUM)
107
83
 
108
- alias_method :id, :hash
84
+ # if we deal with max gas fee parameter, attempt EIP-1559
85
+ unless params[:max_gas_fee].nil?
86
+ params[:chain_id] = chain_id if params[:chain_id].nil?
87
+ return Tx::Eip1559.new params
88
+ end
109
89
 
110
- def data_hex
111
- Utils.bin_to_prefixed_hex data_bin
112
- end
90
+ # if we deal with access list parameter, attempt EIP-2930
91
+ unless params[:access_list].nil?
92
+ params[:chain_id] = chain_id if params[:chain_id].nil?
93
+ return Tx::Eip2930.new params
94
+ end
113
95
 
114
- def data_hex=(hex)
115
- self.data_bin = Utils.hex_to_bin(hex)
96
+ # if nothing else, go with legacy transactions
97
+ chain_id = params[:chain_id] if !params[:chain_id].nil? and params[:chain_id] != chain_id
98
+ return Tx::Legacy.new params, chain_id
116
99
  end
117
100
 
118
- def data
119
- Eth.tx_data_hex? ? data_hex : data_bin
120
- end
101
+ # Decodes a transaction hex of any known type (2, 1, or legacy).
102
+ #
103
+ # @param hex [String] the raw transaction hex-string.
104
+ # @return [Eth::Tx] transaction payload.
105
+ # @raise [TransactionTypeError] if the transaction type is unknown.
106
+ def decode(hex)
107
+ hex = Util.remove_hex_prefix hex
108
+ type = hex[0, 2].to_i(16)
109
+ case type
110
+ when TYPE_1559
111
+
112
+ # EIP-1559 transaction (type 2)
113
+ return Tx::Eip1559.decode hex
114
+ when TYPE_2930
115
+
116
+ # EIP-2930 transaction (type 1)
117
+ return Tx::Eip2930.decode hex
118
+ else
121
119
 
122
- def data=(string)
123
- Eth.tx_data_hex? ? self.data_hex = (string) : self.data_bin = (string)
120
+ # Legacy transaction if first byte is RLP (>= 192)
121
+ if type >= 0xc0
122
+ return Tx::Legacy.decode hex
123
+ else
124
+ raise TransactionTypeError, "Cannot decode unknown transaction type #{type}!"
125
+ end
126
+ end
124
127
  end
125
128
 
126
- def chain_id
127
- @chain_id
129
+ # Creates an unsigned copy of any transaction object.
130
+ #
131
+ # @param tx [Eth::Tx] any transaction payload.
132
+ # @return [Eth::Tx] an unsigned transaction payload of the same type.
133
+ # @raise [TransactionTypeError] if the transaction type is unknown.
134
+ def unsigned_copy(tx)
135
+ case tx.type
136
+ when TYPE_1559
137
+
138
+ # EIP-1559 transaction (type 2)
139
+ return Tx::Eip1559.unsigned_copy tx
140
+ when TYPE_2930
141
+
142
+ # EIP-2930 transaction (type 1)
143
+ return Tx::Eip2930.unsigned_copy tx
144
+ when TYPE_LEGACY
145
+
146
+ # Legacy transaction ("type 0")
147
+ return Tx::Legacy.unsigned_copy tx
148
+ end
149
+ raise TransactionTypeError, "Cannot copy unknown transaction type #{tx.type}!"
128
150
  end
129
151
 
130
- def chain_id=(cid)
131
- if cid != @chain_id
132
- self.v = 0
133
- self.r = 0
134
- self.s = 0
135
-
136
- clear_signature
152
+ # Estimates intrinsic gas for provided call data (EIP-2028) and
153
+ # access lists (EIP-2930).
154
+ #
155
+ # @param data [String] the call data.
156
+ # @param list [Array] the access list.
157
+ # @return [Integer] the estimated intrinsic gas cost.
158
+ def estimate_intrinsic_gas(data = "", list = [])
159
+ gas = DEFAULT_GAS_LIMIT
160
+ unless data.nil? or data.empty?
161
+ data = Util.hex_to_bin data if Util.is_hex? data
162
+
163
+ # count zero bytes
164
+ zero = data.count ZERO_BYTE
165
+ gas += zero * COST_ZERO_BYTE
166
+
167
+ # count non-zero bytes
168
+ none = data.size - zero
169
+ gas += none * COST_NON_ZERO_BYTE
137
170
  end
171
+ unless list.nil? or list.empty?
172
+ list.each do |entry|
138
173
 
139
- @chain_id = (cid == 0) ? nil : cid
140
- end
141
-
142
- def prevent_replays?
143
- !self.chain_id.nil?
144
- end
174
+ # count addresses
175
+ gas += COST_ADDRESS
145
176
 
146
- private
177
+ entry.last.each do |key|
147
178
 
148
- def clear_signature
149
- @signature = nil
150
- @ecdsa_signature = nil
179
+ # count storage keys
180
+ gas += COST_STORAGE_KEY
181
+ end
182
+ end
183
+ end
184
+ return gas
151
185
  end
152
186
 
153
- def hash_keys
154
- keys = self.class.serializable_fields.keys
155
- keys.delete(:data_bin)
156
- keys + [:data, :chain_id]
187
+ # Validates the common transaction fields such as nonce, gas limit,
188
+ # amount, and access list.
189
+ #
190
+ # @param fields [Hash] the transaction fields.
191
+ # @return [Hash] the validated transaction fields.
192
+ # @raise [ParameterError] if nonce is an invalid integer.
193
+ # @raise [ParameterError] if gas limit is invalid.
194
+ # @raise [ParameterError] if amount is invalid.
195
+ # @raise [ParameterError] if access list is invalid.
196
+ def validate_params(fields)
197
+ if fields[:nonce].nil? or fields[:nonce] < 0
198
+ raise ParameterError, "Invalid signer nonce #{fields[:nonce]}!"
199
+ end
200
+ if fields[:gas_limit].nil? or fields[:gas_limit] < DEFAULT_GAS_LIMIT or fields[:gas_limit] > BLOCK_GAS_LIMIT
201
+ raise ParameterError, "Invalid gas limit #{fields[:gas_limit]}!"
202
+ end
203
+ unless fields[:value] >= 0
204
+ raise ParameterError, "Invalid transaction value #{fields[:value]}!"
205
+ end
206
+ unless fields[:access_list].nil? or fields[:access_list].is_a? Array
207
+ raise ParameterError, "Invalid access list #{fields[:access_list]}!"
208
+ end
209
+ return fields
157
210
  end
158
211
 
159
- def check_transaction_validity
160
- if [gas_price, gas_limit, value, nonce].max > UINT_MAX
161
- raise InvalidTransaction, "Values way too high!"
162
- elsif gas_limit < intrinsic_gas_used
163
- raise InvalidTransaction, "Gas limit too low"
212
+ # Validates the common type-2 transaction fields such as priority
213
+ # fee and max gas fee.
214
+ #
215
+ # @param fields [Hash] the transaction fields.
216
+ # @return [Hash] the validated transaction fields.
217
+ # @raise [ParameterError] if priority fee is invalid.
218
+ # @raise [ParameterError] if max gas fee is invalid.
219
+ def validate_eip1559_params(fields)
220
+ if fields[:priority_fee].nil? or fields[:priority_fee] < 0
221
+ raise ParameterError, "Invalid gas priority fee #{fields[:priority_fee]}!"
222
+ end
223
+ if fields[:max_gas_fee].nil? or fields[:max_gas_fee] < 0
224
+ raise ParameterError, "Invalid max gas fee #{fields[:max_gas_fee]}!"
164
225
  end
226
+ return fields
165
227
  end
166
228
 
167
- def intrinsic_gas_used
168
- num_zero_bytes = data_bin.count(BYTE_ZERO)
169
- num_non_zero_bytes = data_bin.size - num_zero_bytes
229
+ # Validates the common legacy transaction fields such as gas price.
230
+ #
231
+ # @param fields [Hash] the transaction fields.
232
+ # @return [Hash] the validated transaction fields.
233
+ # @raise [ParameterError] if gas price is invalid.
234
+ def validate_legacy_params(fields)
235
+ if fields[:gas_price].nil? or fields[:gas_price] < 0
236
+ raise ParameterError, "Invalid gas price #{fields[:gas_price]}!"
237
+ end
238
+ return fields
239
+ end
170
240
 
171
- Gas::GTXCOST +
172
- Gas::GTXDATAZERO * num_zero_bytes +
173
- Gas::GTXDATANONZERO * num_non_zero_bytes
241
+ # Populates the transaction chain id field with a serializable default
242
+ # value (1) in case it is undefined.
243
+ #
244
+ # @param id [Integer] the transaction chain id.
245
+ # @return [Integer] the sanitized transaction chain id.
246
+ def sanitize_chain(id)
247
+ id = Chain::ETHEREUM if id.nil?
248
+ return id
174
249
  end
175
250
 
176
- def signature_hash
177
- Utils.keccak256 unsigned_encoded
251
+ # Populates the transaction destination address with a serializable
252
+ # empty value in case it is undefined; also ensures the address is
253
+ # checksummed but not prefixed for consistency.
254
+ #
255
+ # @param addr [String] the transaction destination address.
256
+ # @return [String] the sanitized transaction destination address.
257
+ def sanitize_address(addr)
258
+ addr = "" if addr.nil?
259
+ if addr.is_a? String and !addr.empty?
260
+ addr = Address.new(addr).to_s
261
+ addr = Util.remove_hex_prefix addr
262
+ end
263
+ return addr
178
264
  end
179
265
 
180
- def unsigned
181
- Tx.new to_h.merge(v: (self.chain_id) ? self.chain_id : 0, r: 0, s: 0)
266
+ # Populates the transaction value field with a serializable empty value
267
+ # in case it is undefined.
268
+ #
269
+ # @param val [Integer] the transaction value.
270
+ # @return [Integer] the sanitized transaction value.
271
+ def sanitize_amount(val)
272
+ val = 0 if val.nil?
273
+ return val
182
274
  end
183
275
 
184
- protected
276
+ # Populates the transaction payload field with a serializable empty value
277
+ # in case it is undefined; also ensures the data is binary not hex.
278
+ #
279
+ # @param data [String] the transaction payload data.
280
+ # @return [String] the sanitized transaction payload data.
281
+ def sanitize_data(data)
282
+ data = "" if data.nil?
283
+
284
+ # ensure payload to be binary if it's hex, otherwise we'll treat it raw
285
+ data = Util.hex_to_bin data if Util.is_hex? data
286
+ return data
287
+ end
185
288
 
186
- def sedes
187
- if self.prevent_replays? && !(Eth.replayable_v? v)
188
- self.class
189
- else
190
- UnsignedTx
289
+ # Populates the transaction access list field with a serializable empty
290
+ # array in case it is undefined; also ensures the nested data is binary
291
+ # not hex.
292
+ #
293
+ # @param list [Array] the transaction access list.
294
+ # @return [Array] the sanitized transaction access list.
295
+ def sanitize_list(list)
296
+ list = [] if list.nil?
297
+ list.each_with_index do |value, index|
298
+ if value.is_a? Array
299
+
300
+ # recursively check the entire array
301
+ list[index] = sanitize_list value
302
+ elsif Util.is_hex? value
303
+
304
+ # only modify if we find a hex value
305
+ list[index] = Util.hex_to_bin value
306
+ end
191
307
  end
308
+ return list
192
309
  end
193
- end
194
310
 
195
- UnsignedTx = Tx.exclude([:v, :r, :s])
311
+ # Allows to check wether a transaction is signed already.
312
+ #
313
+ # @return [Bool] true if transaction is already signed.
314
+ def is_signed?(tx)
315
+ !tx.signature_r.nil? and tx.signature_r != 0 and
316
+ !tx.signature_s.nil? and tx.signature_s != 0
317
+ end
318
+ end
196
319
  end
data/lib/eth/unit.rb ADDED
@@ -0,0 +1,49 @@
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
+ require "bigdecimal"
16
+
17
+ # Provides the {Eth} module.
18
+ module Eth
19
+
20
+ # Provides constants for common Ethereum units.
21
+ module Unit
22
+ extend self
23
+
24
+ # Ethereum unit 1 wei := 0.000000000000000001 Ether.
25
+ WEI = BigDecimal("1e0").freeze
26
+
27
+ # Ethereum unit 1 babbage := 0.000000000000001 Ether or 1_000 wei.
28
+ BABBAGE = BigDecimal("1e3").freeze
29
+
30
+ # Ethereum unit 1 lovelace := 0.000000000001 Ether or 1_000_000 wei.
31
+ LOVELACE = BigDecimal("1e6").freeze
32
+
33
+ # Ethereum unit 1 shannon := 0.000000001 Ether or 1_000_000_000 wei.
34
+ SHANNON = BigDecimal("1e9").freeze
35
+
36
+ # Ethereum unit 1 szabo := 0.000_001 Ether or 1_000_000_000_000 wei.
37
+ SZABO = BigDecimal("1e12").freeze
38
+
39
+ # Ethereum unit 1 finney := 0.001 Ether or 1_000_000_000_000_000 wei.
40
+ FINNEY = BigDecimal("1e15").freeze
41
+
42
+ # Ethereum unit 1 Ether := 1_000_000_000_000_000_000 wei.
43
+ ETHER = BigDecimal("1e18").freeze
44
+
45
+ # Ethereum unit 1 Gwei := 0.000000001 Ether or 1_000_000_000 wei.
46
+ # Same as shannon, but more commonly used (billion wei).
47
+ GWEI = SHANNON.freeze
48
+ end
49
+ end