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/client.rb ADDED
@@ -0,0 +1,232 @@
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
+ # Provides the {Eth::Client} super-class to connect to Ethereum
19
+ # network's RPC-API endpoints (IPC or HTTP).
20
+ class Client
21
+
22
+ # The client's RPC-request ID starting at 0.
23
+ attr_reader :id
24
+
25
+ # The connected network's chain ID.
26
+ attr_reader :chain_id
27
+
28
+ # The connected network's client coinbase.
29
+ attr_accessor :default_account
30
+
31
+ # The default transaction max priority fee per gas in Wei.
32
+ attr_accessor :max_priority_fee_per_gas
33
+
34
+ # The default transaction max fee per gas in Wei.
35
+ attr_accessor :max_fee_per_gas
36
+
37
+ # The default gas limit for the transaction.
38
+ attr_accessor :gas_limit
39
+
40
+ # Creates a new RPC-Client, either by providing an HTTP/S host or
41
+ # an IPC path.
42
+ #
43
+ # @param host [String] either an HTTP/S host or an IPC path.
44
+ # @return [Eth::Client::Ipc] an IPC client.
45
+ # @return [Eth::Client::Http] an HTTP client.
46
+ # @raise [ArgumentError] in case it cannot determine the client type.
47
+ def self.create(host)
48
+ return Client::Ipc.new host if host.end_with? ".ipc"
49
+ return Client::Http.new host if host.start_with? "http"
50
+ raise ArgumentError, "Unable to detect client type!"
51
+ end
52
+
53
+ # Constructor for the {Eth::Client} super-class. Should not be used;
54
+ # use {Client.create} intead.
55
+ def initialize(_)
56
+ @id = 0
57
+ @max_priority_fee_per_gas = 0
58
+ @max_fee_per_gas = Tx::DEFAULT_GAS_PRICE
59
+ @gas_limit = Tx::DEFAULT_GAS_LIMIT
60
+ end
61
+
62
+ # Gets the default account (coinbase) of the connected client.
63
+ #
64
+ # @return [Eth::Address] the coinbase account address.
65
+ def default_account
66
+ @default_account ||= Address.new eth_coinbase["result"]
67
+ end
68
+
69
+ # Gets the chain ID of the connected network.
70
+ #
71
+ # @return [Integer] the chain ID.
72
+ def chain_id
73
+ @chain_id ||= eth_chain_id["result"].to_i 16
74
+ end
75
+
76
+ # Gets the balance for an address.
77
+ #
78
+ # @param address [Eth::Address] the address to get the balance for.
79
+ # @return [Integer] the balance in Wei.
80
+ def get_balance(address)
81
+ eth_get_balance(address)["result"].to_i 16
82
+ end
83
+
84
+ # Gets the next nonce for an address used to draft new transactions.
85
+ #
86
+ # @param address [Eth::Address] the address to get the nonce for.
87
+ # @return [Integer] the next nonce to be used.
88
+ def get_nonce(address)
89
+ eth_get_transaction_count(address, "pending")["result"].to_i 16
90
+ end
91
+
92
+ # Simply transfer Ether to an account and waits for it to be mined.
93
+ # Uses `eth_coinbase` and external signer if no sender key is
94
+ # provided.
95
+ #
96
+ # @param destination [Eth::Address] the destination address.
97
+ # @param amount [Integer] the transfer amount in Wei.
98
+ # @param sender_key [Eth::Key] the sender private key.
99
+ # @param legacy [Boolean] enables legacy transactions (pre-EIP-1559).
100
+ # @return [String] the transaction hash.
101
+ def transfer_and_wait(destination, amount, sender_key = nil, legacy = false)
102
+ wait_for_tx(transfer(destination, amount, sender_key, legacy))
103
+ end
104
+
105
+ # Simply transfer Ether to an account without any call data or
106
+ # access lists attached. Uses `eth_coinbase` and external signer
107
+ # if no sender key is provided.
108
+ #
109
+ # @param destination [Eth::Address] the destination address.
110
+ # @param amount [Integer] the transfer amount in Wei.
111
+ # @param sender_key [Eth::Key] the sender private key.
112
+ # @param legacy [Boolean] enables legacy transactions (pre-EIP-1559).
113
+ # @return [String] the transaction hash.
114
+ def transfer(destination, amount, sender_key = nil, legacy = false)
115
+ params = {
116
+ value: amount,
117
+ to: destination,
118
+ gas_limit: gas_limit,
119
+ chain_id: chain_id,
120
+ }
121
+ if legacy
122
+ params.merge!({
123
+ gas_price: max_fee_per_gas,
124
+ })
125
+ else
126
+ params.merge!({
127
+ priority_fee: max_priority_fee_per_gas,
128
+ max_gas_fee: max_fee_per_gas,
129
+ })
130
+ end
131
+ unless sender_key.nil?
132
+
133
+ # use the provided key as sender and signer
134
+ params.merge!({
135
+ from: sender_key.address,
136
+ nonce: get_nonce(sender_key.address),
137
+ })
138
+ tx = Eth::Tx.new(params)
139
+ tx.sign sender_key
140
+ return eth_send_raw_transaction(tx.hex)["result"]
141
+ else
142
+
143
+ # use the default account as sender and external signer
144
+ params.merge!({
145
+ from: default_account,
146
+ nonce: get_nonce(default_account),
147
+ })
148
+ return eth_send_transaction(params)["result"]
149
+ end
150
+ end
151
+
152
+ # Gives control over resetting the RPC request ID back to zero.
153
+ # Usually not needed.
154
+ #
155
+ # @return [Integer] 0
156
+ def reset_id
157
+ @id = 0
158
+ end
159
+
160
+ # Checkes wether a transaction is mined or not.
161
+ #
162
+ # @param hash [String] the transaction hash.
163
+ # @return [Boolean] true if included in a block.
164
+ def is_mined_tx?(hash)
165
+ mined_tx = eth_get_transaction_by_hash hash
166
+ !mined_tx.nil? && !mined_tx["result"].nil? && !mined_tx["result"]["blockNumber"].nil?
167
+ end
168
+
169
+ # Waits for an transaction to be mined by the connected chain.
170
+ #
171
+ # @param hash [String] the transaction hash.
172
+ # @return [String] the transactin hash once the transaction is mined.
173
+ # @raise [Timeout::Error] if it's not mined within 5 minutes.
174
+ def wait_for_tx(hash)
175
+ start_time = Time.now
176
+ timeout = 300
177
+ retry_rate = 0.1
178
+ loop do
179
+ raise Timeout::Error if ((Time.now - start_time) > timeout)
180
+ return hash if is_mined_tx? hash
181
+ sleep retry_rate
182
+ end
183
+ end
184
+
185
+ # Metafunction to provide all known RPC commands defined in
186
+ # Eth::Api as snake_case methods to the Eth::Client classes.
187
+ Api::COMMANDS.each do |cmd|
188
+ method_name = cmd.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
189
+ define_method method_name do |*args|
190
+ send_command cmd, args
191
+ end
192
+ end
193
+
194
+ private
195
+
196
+ # Prepares parameters and sends the command to the client.
197
+ def send_command(command, args)
198
+ args << "latest" if ["eth_getBalance", "eth_call"].include? command
199
+ payload = {
200
+ jsonrpc: "2.0",
201
+ method: command,
202
+ params: marshal(args),
203
+ id: next_id,
204
+ }
205
+ output = JSON.parse(send(payload.to_json))
206
+ raise IOError, output["error"]["message"] unless output["error"].nil?
207
+ return output
208
+ end
209
+
210
+ # Increments the request id.
211
+ def next_id
212
+ @id += 1
213
+ end
214
+
215
+ # Recursively marshals all request parameters.
216
+ def marshal(params)
217
+ if params.is_a? Array
218
+ return params.map! { |param| marshal(param) }
219
+ elsif params.is_a? Hash
220
+ return params.transform_values! { |param| marshal(param) }
221
+ elsif params.is_a? Numeric
222
+ return Util.prefix_hex "#{params.to_i.to_s(16)}"
223
+ elsif params.is_a? Address
224
+ return params.to_s
225
+ elsif Util.is_hex? params
226
+ return Util.prefix_hex params
227
+ else
228
+ return params
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,71 @@
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
+ # Provides the {Eth} module.
18
+ module Eth
19
+
20
+ # Provides commonly used constants, such as zero bytes or zero keys.
21
+ module Constant
22
+
23
+ # The empty byte is defined as "".
24
+ BYTE_EMPTY = "".freeze
25
+
26
+ # The zero byte is 0x00.
27
+ BYTE_ZERO = "\x00".freeze
28
+
29
+ # The byte one is 0x01.
30
+ BYTE_ONE = "\x01".freeze
31
+
32
+ # The size of a 32-bit number.
33
+ TT32 = (2 ** 32).freeze
34
+
35
+ # The size of a 256-bit number.
36
+ TT256 = (2 ** 256).freeze
37
+
38
+ # The maximum possible value of an UInt256.
39
+ UINT_MAX = (2 ** 256 - 1).freeze
40
+
41
+ # The minimum possible value of an UInt256.
42
+ UINT_MIN = 0.freeze
43
+
44
+ # The maximum possible value of an Int256.
45
+ INT_MAX = (2 ** 255 - 1).freeze
46
+
47
+ # The minimum possible value of an Int256.
48
+ INT_MIN = (-2 ** 255).freeze
49
+
50
+ # A hash containing only zeros.
51
+ HASH_ZERO = ("\x00" * 32).freeze
52
+
53
+ # The RLP short length limit.
54
+ SHORT_LENGTH_LIMIT = 56.freeze
55
+
56
+ # The RLP long length limit.
57
+ LONG_LENGTH_LIMIT = (256 ** 8).freeze
58
+
59
+ # The RLP primitive type offset.
60
+ PRIMITIVE_PREFIX_OFFSET = 0x80.freeze
61
+
62
+ # The RLP array type offset.
63
+ LIST_PREFIX_OFFSET = 0xc0.freeze
64
+
65
+ # The binary encoding is ASCII (8-bit).
66
+ BINARY_ENCODING = "ASCII-8BIT".freeze
67
+
68
+ # Infinity as constant for convenience.
69
+ INFINITY = (1.0 / 0.0).freeze
70
+ end
71
+ end
data/lib/eth/eip712.rb ADDED
@@ -0,0 +1,184 @@
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
+ # Defines handy tools for encoding typed structured data as per EIP-712.
19
+ # Ref: https://eips.ethereum.org/EIPS/eip-712
20
+ module Eip712
21
+ extend self
22
+
23
+ # Provides a special typed-data error if data structure fails basic
24
+ # verification.
25
+ class TypedDataError < StandardError; end
26
+
27
+ # Scans all dependencies of a given type recursively and returns
28
+ # either all dependencies or none if not found.
29
+ #
30
+ # @param primary_type [String] the primary type which we want to scan.
31
+ # @param types [Array] all existing types in the data structure.
32
+ # @param result [Array] found results from previous recursions.
33
+ # @return [Array] all dependent types for the given primary type.
34
+ def type_dependencies(primary_type, types, result = [])
35
+ if result.include? primary_type
36
+
37
+ # ignore if we already have the give type in results
38
+ return result
39
+ elsif types[primary_type.to_sym].nil?
40
+
41
+ # ignore if the type is not used, e.g., a string or address.
42
+ return result
43
+ else
44
+
45
+ # we found something
46
+ result.push primary_type
47
+
48
+ # recursively look for further nested dependencies
49
+ types[primary_type.to_sym].each do |t|
50
+ dependency = type_dependencies t[:type], types, result
51
+ end
52
+ return result
53
+ end
54
+ end
55
+
56
+ # Encode types as an EIP-712 confrom string, e.g.,
57
+ # `MyType(string attribute)`.
58
+ #
59
+ # @param primary_type [String] the type which we want to encode.
60
+ # @param types [Array] all existing types in the data structure.
61
+ # @return [String] an EIP-712 encoded type-string.
62
+ # @raise [TypedDataError] if non-primary type found.
63
+ def encode_type(primary_type, types)
64
+
65
+ # get all used types
66
+ all_dependencies = type_dependencies primary_type, types
67
+
68
+ # remove primary types and sort the rest alphabetically
69
+ filtered_dependencies = all_dependencies.delete_if { |type| type.to_s == primary_type }
70
+ sorted_dependencies = filtered_dependencies.sort
71
+ dependencies = [primary_type]
72
+ sorted_dependencies.each do |sorted|
73
+ dependencies.push sorted
74
+ end
75
+
76
+ # join them all in a string with types and field names
77
+ result = ""
78
+ dependencies.each do |type|
79
+
80
+ # dependencies should not have non-primary types (such as string, address)
81
+ raise TypedDataError, "Non-primary type found: #{type}!" if types[type.to_sym].nil?
82
+
83
+ result += "#{type}("
84
+ result += types[type.to_sym].map { |t| "#{t[:type]} #{t[:name]}" }.join(",")
85
+ result += ")"
86
+ end
87
+ return result
88
+ end
89
+
90
+ # Hashes an EIP-712 confrom type-string.
91
+ #
92
+ # @param primary_type [String] the type which we want to hash.
93
+ # @param types [Array] all existing types in the data structure.
94
+ # @return [String] a Keccak-256 hash of an EIP-712 encoded type-string.
95
+ def hash_type(primary_type, types)
96
+ encoded_type = encode_type primary_type, types
97
+ return Util.keccak256 encoded_type
98
+ end
99
+
100
+ # Recursively ABI-encodes all data and types according to EIP-712.
101
+ #
102
+ # @param primary_type [String] the primary type which we want to encode.
103
+ # @param data [Array] the data in the data structure we want to encode.
104
+ # @param types [Array] all existing types in the data structure.
105
+ # @return [String] an ABI-encoded representation of the data and the types.
106
+ def encode_data(primary_type, data, types)
107
+
108
+ # first data field is the type hash
109
+ encoded_types = ["bytes32"]
110
+ encoded_values = [hash_type(primary_type, types)]
111
+
112
+ # adds field contents
113
+ types[primary_type.to_sym].each do |field|
114
+ value = data[field[:name].to_sym]
115
+ type = field[:type]
116
+ raise NotImplementedError, "Arrays currently unimplemented for EIP-712." if type.end_with? "]"
117
+ if type == "string" or type == "bytes"
118
+ encoded_types.push "bytes32"
119
+ encoded_values.push Util.keccak256 value
120
+ elsif !types[type.to_sym].nil?
121
+ encoded_types.push "bytes32"
122
+ value = encode_data type, value, types
123
+ encoded_values.push Util.keccak256 value
124
+ else
125
+ encoded_types.push type
126
+ encoded_values.push value
127
+ end
128
+ end
129
+
130
+ # all data is abi-encoded
131
+ return Abi.encode encoded_types, encoded_values
132
+ end
133
+
134
+ # Recursively ABI-encodes and hashes all data and types.
135
+ #
136
+ # @param primary_type [String] the primary type which we want to hash.
137
+ # @param data [Array] the data in the data structure we want to hash.
138
+ # @param types [Array] all existing types in the data structure.
139
+ # @return [String] a Keccak-256 hash of the ABI-encoded data and types.
140
+ def hash_data(primary_type, data, types)
141
+ encoded_data = encode_data primary_type, data, types
142
+ return Util.keccak256 encoded_data
143
+ end
144
+
145
+ # Enforces basic properties to be represented in the EIP-712 typed
146
+ # data structure: types, domain, message, etc.
147
+ #
148
+ # @param data [Array] the data in the data structure we want to hash.
149
+ # @return [Array] the data in the data structure we want to hash.
150
+ # @raise [TypedDataError] if the data fails validation.
151
+ def enforce_typed_data(data)
152
+ data = JSON.parse data if Util.is_hex? data
153
+ raise TypedDataError, "Data is missing, try again with data." if data.nil? or data.empty?
154
+ raise TypedDataError, "Data types are missing." if data[:types].nil? or data[:types].empty?
155
+ raise TypedDataError, "Data primaryType is missing." if data[:primaryType].nil? or data[:primaryType].empty?
156
+ raise TypedDataError, "Data domain is missing." if data[:domain].nil?
157
+ raise TypedDataError, "Data message is missing." if data[:message].nil? or data[:message].empty?
158
+ raise TypedDataError, "Data EIP712Domain is missing." if data[:types][:EIP712Domain].nil?
159
+ return data
160
+ end
161
+
162
+ # Hashes a typed data structure with Keccak-256 to prepare a signed
163
+ # typed data operation respecting EIP-712.
164
+ #
165
+ # @param data [Array] all the data in the typed data structure.
166
+ # @return [String] a Keccak-256 hash of the EIP-712-encoded typed data.
167
+ def hash(data)
168
+ data = enforce_typed_data data
169
+
170
+ # EIP-191 prefix byte
171
+ buffer = Signature::EIP191_PREFIX_BYTE
172
+
173
+ # EIP-712 version byte
174
+ buffer += Signature::EIP712_VERSION_BYTE
175
+
176
+ # hashed domain data
177
+ buffer += hash_data "EIP712Domain", data[:domain], data[:types]
178
+
179
+ # hashed message data
180
+ buffer += hash_data data[:primaryType], data[:message], data[:types]
181
+ return Util.keccak256 buffer
182
+ end
183
+ end
184
+ end