eth 0.4.18 → 0.5.2

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 (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