eth 0.4.16 → 0.5.1

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