eth 0.5.14 → 0.5.15

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.
@@ -53,5 +53,26 @@ module Eth
53
53
  def self.encoded_function_signature(signature)
54
54
  Util.bin_to_hex Util.keccak256(signature)[0..3]
55
55
  end
56
+
57
+ # Encodes a function call arguments
58
+ #
59
+ # @param args [Array] function arguments
60
+ # @return [String] encoded function call data
61
+ def encode_call(*args)
62
+ types = inputs.map(&:parsed_type)
63
+ encoded_str = Util.bin_to_hex(Eth::Abi.encode(types, args))
64
+ Util.prefix_hex(signature + (encoded_str.empty? ? "0" * 64 : encoded_str))
65
+ end
66
+
67
+ # Decodes a function call result
68
+ #
69
+ # @param data [String] eth_call result in hex format
70
+ # @return [Array]
71
+ def decode_call_result(data)
72
+ return nil if data == "0x"
73
+
74
+ types = outputs.map(&:parsed_type)
75
+ Eth::Abi.decode(types, data)
76
+ end
56
77
  end
57
78
  end
@@ -19,19 +19,27 @@ module Eth
19
19
 
20
20
  # Provide classes for contract function output.
21
21
  class Contract::FunctionOutput
22
- attr_accessor :type, :name
22
+ attr_accessor :type, :raw_type, :name
23
23
 
24
24
  # Constructor of the {Eth::Contract::FunctionOutput} class.
25
25
  #
26
26
  # @param data [Hash] contract abi data.
27
27
  def initialize(data)
28
- @type = Eth::Abi::Type.parse(data["type"])
28
+ @raw_type = data["type"]
29
+ @type = Eth::Abi::Type.parse(data["type"], data["components"])
29
30
  @name = data["name"]
30
31
  end
31
32
 
32
33
  # Returns complete types with subtypes, e.g., `uint256`.
33
34
  def type
34
- @type.base_type + @type.sub_type + @type.dimensions.map { |dimension| "[#{dimension > 0 ? dimension : ""}]" }.join("")
35
+ @type.base_type +
36
+ @type.sub_type +
37
+ @type.dimensions.map { |dimension| "[#{dimension > 0 ? dimension : ""}]" }.join("")
38
+ end
39
+
40
+ # Returns parsed types.
41
+ def parsed_type
42
+ @type
35
43
  end
36
44
  end
37
45
  end
data/lib/eth/contract.rb CHANGED
@@ -25,7 +25,7 @@ module Eth
25
25
  attr_accessor :key
26
26
  attr_accessor :gas_limit, :gas_price, :max_fee_per_gas, :max_priority_fee_per_gas, :nonce
27
27
  attr_accessor :bin, :name, :abi, :class_object
28
- attr_accessor :events, :functions, :constructor_inputs
28
+ attr_accessor :events, :functions, :constructor_inputs, :errors
29
29
 
30
30
  # Constructor of the {Eth::Contract} class.
31
31
  #
@@ -44,7 +44,7 @@ module Eth
44
44
  @name = _name
45
45
  @bin = bin
46
46
  @abi = abi
47
- @constructor_inputs, @functions, @events = parse_abi(abi)
47
+ @constructor_inputs, @functions, @events, @errors = parse_abi(abi)
48
48
  end
49
49
 
50
50
  # Creates a contract wrapper from a Solidity file.
@@ -106,6 +106,52 @@ module Eth
106
106
  end
107
107
  end
108
108
 
109
+ # Finds a function by name.
110
+ #
111
+ # @param name [String] function name.
112
+ # @param args [Integer, nil] number of arguments of a function.
113
+ # @return [Eth::Contract::Function] function object.
114
+ # @raise [ArgumentError] if function not found.
115
+ def function(name, args: nil)
116
+ functions.find do |f|
117
+ f.name == name && (args.nil? || args == f.inputs.size)
118
+ end || raise(ArgumentError, "this function does not exist!")
119
+ end
120
+
121
+ # Finds an error by name.
122
+ #
123
+ # @param name [String] error name.
124
+ # @param args [Integer, nil] number of arguments of an error.
125
+ # @return [Eth::Contract::Error] error object.
126
+ # @raise [ArgumentError] if error not found.
127
+ def error(name, args: nil)
128
+ errors.find do |e|
129
+ e.name == name && (args.nil? || args == e.inputs.size)
130
+ end || raise(ArgumentError, "this error does not exist!")
131
+ end
132
+
133
+ # Decodes a custom error returned by an RPC error using the contract ABI.
134
+ #
135
+ # @param rpc_error [RpcError] the RPC error containing revert data.
136
+ # @return [String] a human readable error message.
137
+ def decode_error(rpc_error)
138
+ data = rpc_error.data
139
+ return rpc_error.message if data.nil? || errors.nil?
140
+
141
+ signature = data[0, 10]
142
+ if (err = errors.find { |e| e.signature == signature })
143
+ values = err.decode(data)
144
+ args = values&.map { |v| v.is_a?(String) ? v : v.inspect }&.join(",")
145
+ args ||= ""
146
+ "execution reverted: #{err.name}(#{args})"
147
+ elsif signature == "0x08c379a0"
148
+ reason = Abi.decode(["string"], "0x" + data[10..])&.first
149
+ "execution reverted: #{reason}"
150
+ else
151
+ rpc_error.message
152
+ end
153
+ end
154
+
109
155
  # Create meta classes for smart contracts.
110
156
  def build
111
157
  class_name = @name
@@ -116,9 +162,12 @@ module Eth
116
162
  def_delegators :parent, :name, :abi, :bin
117
163
  def_delegators :parent, :gas_limit, :gas_price, :gas_limit=, :gas_price=, :nonce, :nonce=
118
164
  def_delegators :parent, :max_fee_per_gas, :max_fee_per_gas=, :max_priority_fee_per_gas, :max_priority_fee_per_gas=
119
- def_delegators :parent, :events
165
+ def_delegators :parent, :events, :errors
120
166
  def_delegators :parent, :address, :address=
121
167
  def_delegator :parent, :functions
168
+ def_delegator :parent, :function
169
+ def_delegator :parent, :error
170
+ def_delegator :parent, :decode_error
122
171
  def_delegator :parent, :constructor_inputs
123
172
  define_method :parent do
124
173
  parent
@@ -140,7 +189,8 @@ module Eth
140
189
  end
141
190
  functions = abi.select { |x| x["type"] == "function" }.map { |fun| Eth::Contract::Function.new(fun) }
142
191
  events = abi.select { |x| x["type"] == "event" }.map { |evt| Eth::Contract::Event.new(evt) }
143
- [constructor_inputs, functions, events]
192
+ errors = abi.select { |x| x["type"] == "error" }.map { |err| Eth::Contract::Error.new(err) }
193
+ [constructor_inputs, functions, events, errors]
144
194
  end
145
195
  end
146
196
  end
@@ -151,3 +201,4 @@ require "eth/contract/function"
151
201
  require "eth/contract/function_input"
152
202
  require "eth/contract/function_output"
153
203
  require "eth/contract/initializer"
204
+ require "eth/contract/error"
data/lib/eth/eip712.rb CHANGED
@@ -47,7 +47,12 @@ module Eth
47
47
 
48
48
  # recursively look for further nested dependencies
49
49
  types[primary_type.to_sym].each do |t|
50
- dependency = type_dependencies t[:type], types, result
50
+ nested_type = t[:type]
51
+ # unpack arrays to their inner types to resolve dependencies
52
+ if nested_type.end_with?("]")
53
+ nested_type = nested_type.partition("[").first
54
+ end
55
+ dependency = type_dependencies nested_type, types, result
51
56
  end
52
57
  return result
53
58
  end
@@ -113,19 +118,12 @@ module Eth
113
118
  types[primary_type.to_sym].each do |field|
114
119
  value = data[field[:name].to_sym]
115
120
  type = field[:type]
116
- if type == "string"
117
- encoded_types.push "bytes32"
118
- encoded_values.push Util.keccak256 value
119
- elsif type == "bytes"
120
- encoded_types.push "bytes32"
121
- value = Util.hex_to_bin value
122
- encoded_values.push Util.keccak256 value
123
- elsif !types[type.to_sym].nil?
121
+ if type.end_with?("]")
122
+ encoded_types.push type
123
+ encoded_values.push encode_array(type, value, types)
124
+ elsif type == "string" || type == "bytes" || !types[type.to_sym].nil?
124
125
  encoded_types.push "bytes32"
125
- value = encode_data type, value, types
126
- encoded_values.push Util.keccak256 value
127
- elsif type.end_with? "]"
128
- raise NotImplementedError, "Arrays currently unimplemented for EIP-712."
126
+ encoded_values.push encode_value(type, value, types)
129
127
  else
130
128
  encoded_types.push type
131
129
  encoded_values.push value
@@ -136,6 +134,43 @@ module Eth
136
134
  return Abi.encode encoded_types, encoded_values
137
135
  end
138
136
 
137
+ # Encodes a single value according to its type following EIP-712 rules.
138
+ # Returns a 32-byte binary string.
139
+ def encode_value(type, value, types)
140
+ if type == "string"
141
+ return Util.keccak256 value
142
+ elsif type == "bytes"
143
+ value = Util.hex_to_bin value
144
+ return Util.keccak256 value
145
+ elsif !types[type.to_sym].nil?
146
+ nested = encode_data type, value, types
147
+ return Util.keccak256 nested
148
+ else
149
+ # encode basic types via ABI to get 32-byte representation
150
+ return Abi.encode([type], [value])
151
+ end
152
+ end
153
+
154
+ # Prepares array values by encoding each element according to its
155
+ # base type. Returns an array compatible with Abi.encode.
156
+ def encode_array(type, value, types)
157
+ inner_type = type.slice(0, type.rindex("["))
158
+ return [] if value.nil?
159
+ value.map do |v|
160
+ if inner_type.end_with?("]")
161
+ encode_array inner_type, v, types
162
+ elsif inner_type == "string"
163
+ Util.keccak256 v
164
+ elsif inner_type == "bytes"
165
+ Util.keccak256 Util.hex_to_bin(v)
166
+ elsif !types[inner_type.to_sym].nil?
167
+ Util.keccak256 encode_data(inner_type, v, types)
168
+ else
169
+ v
170
+ end
171
+ end
172
+ end
173
+
139
174
  # Recursively ABI-encodes and hashes all data and types.
140
175
  #
141
176
  # @param primary_type [String] the primary type which we want to hash.
data/lib/eth/key.rb CHANGED
@@ -75,7 +75,7 @@ module Eth
75
75
  signature = compact.bytes
76
76
  v = Chain.to_v recovery_id, chain_id
77
77
  leading_zero = true
78
- [v].pack("N").unpack("C*").each do |byte|
78
+ [v].pack("Q>").unpack("C*").each do |byte|
79
79
  leading_zero = false if byte > 0 and leading_zero
80
80
  signature.append byte unless leading_zero and byte === 0
81
81
  end
@@ -78,7 +78,7 @@ module Eth
78
78
  # @option params [Integer] :max_gas_fee the max transaction fee per gas.
79
79
  # @option params [Integer] :gas_limit the gas limit.
80
80
  # @option params [Eth::Address] :from the sender address.
81
- # @option params [Eth::Address] :to the reciever address.
81
+ # @option params [Eth::Address] :to the receiver address.
82
82
  # @option params [Integer] :value the transaction value.
83
83
  # @option params [String] :data the transaction data payload.
84
84
  # @option params [Array] :access_list an optional access list.
@@ -148,13 +148,13 @@ module Eth
148
148
  raise ParameterError, "Transaction missing fields!" if tx.size < 9
149
149
 
150
150
  # populate the 9 payload fields
151
- chain_id = Util.deserialize_big_endian_to_int tx[0]
152
- nonce = Util.deserialize_big_endian_to_int tx[1]
153
- priority_fee = Util.deserialize_big_endian_to_int tx[2]
154
- max_gas_fee = Util.deserialize_big_endian_to_int tx[3]
155
- gas_limit = Util.deserialize_big_endian_to_int tx[4]
151
+ chain_id = Util.deserialize_rlp_int tx[0]
152
+ nonce = Util.deserialize_rlp_int tx[1]
153
+ priority_fee = Util.deserialize_rlp_int tx[2]
154
+ max_gas_fee = Util.deserialize_rlp_int tx[3]
155
+ gas_limit = Util.deserialize_rlp_int tx[4]
156
156
  to = Util.bin_to_hex tx[5]
157
- value = Util.deserialize_big_endian_to_int tx[6]
157
+ value = Util.deserialize_rlp_int tx[6]
158
158
  data = tx[7]
159
159
  access_list = tx[8]
160
160
 
@@ -257,6 +257,31 @@ module Eth
257
257
  return hash
258
258
  end
259
259
 
260
+ # Signs the transaction with a provided signature blob.
261
+ #
262
+ # @param signature [String] the concatenated `r`, `s`, and `v` values.
263
+ # @return [String] a transaction hash.
264
+ # @raise [Signature::SignatureError] if transaction is already signed.
265
+ # @raise [Signature::SignatureError] if sender address does not match signer.
266
+ def sign_with(signature)
267
+ if Tx.signed? self
268
+ raise Signature::SignatureError, "Transaction is already signed!"
269
+ end
270
+
271
+ # ensure the sender address matches the signature
272
+ unless @sender.nil? or sender.empty?
273
+ public_key = Signature.recover(unsigned_hash, signature, @chain_id)
274
+ signer_address = Tx.sanitize_address Util.public_key_to_address(public_key).to_s
275
+ from_address = Tx.sanitize_address @sender
276
+ raise Signature::SignatureError, "Signer does not match sender" unless signer_address == from_address
277
+ end
278
+
279
+ r, s, v = Signature.dissect signature
280
+ recovery_id = Chain.to_recovery_id v.to_i(16), @chain_id
281
+ send :_set_signature, recovery_id, r, s
282
+ return hash
283
+ end
284
+
260
285
  # Encodes a raw transaction object, wraps it in an EIP-2718 envelope
261
286
  # with an EIP-1559 type prefix.
262
287
  #
@@ -76,7 +76,7 @@ module Eth
76
76
  # @option params [Integer] :gas_price the gas price.
77
77
  # @option params [Integer] :gas_limit the gas limit.
78
78
  # @option params [Eth::Address] :from the sender address.
79
- # @option params [Eth::Address] :to the reciever address.
79
+ # @option params [Eth::Address] :to the receiver address.
80
80
  # @option params [Integer] :value the transaction value.
81
81
  # @option params [String] :data the transaction data payload.
82
82
  # @option params [Array] :access_list an optional access list.
@@ -145,12 +145,12 @@ module Eth
145
145
  raise ParameterError, "Transaction missing fields!" if tx.size < 8
146
146
 
147
147
  # populate the 8 payload fields
148
- chain_id = Util.deserialize_big_endian_to_int tx[0]
149
- nonce = Util.deserialize_big_endian_to_int tx[1]
150
- gas_price = Util.deserialize_big_endian_to_int tx[2]
151
- gas_limit = Util.deserialize_big_endian_to_int tx[3]
148
+ chain_id = Util.deserialize_rlp_int tx[0]
149
+ nonce = Util.deserialize_rlp_int tx[1]
150
+ gas_price = Util.deserialize_rlp_int tx[2]
151
+ gas_limit = Util.deserialize_rlp_int tx[3]
152
152
  to = Util.bin_to_hex tx[4]
153
- value = Util.deserialize_big_endian_to_int tx[5]
153
+ value = Util.deserialize_rlp_int tx[5]
154
154
  data = tx[6]
155
155
  access_list = tx[7]
156
156
 
@@ -251,6 +251,31 @@ module Eth
251
251
  return hash
252
252
  end
253
253
 
254
+ # Signs the transaction with a provided signature blob.
255
+ #
256
+ # @param signature [String] the concatenated `r`, `s`, and `v` values.
257
+ # @return [String] a transaction hash.
258
+ # @raise [Signature::SignatureError] if transaction is already signed.
259
+ # @raise [Signature::SignatureError] if sender address does not match signer.
260
+ def sign_with(signature)
261
+ if Tx.signed? self
262
+ raise Signature::SignatureError, "Transaction is already signed!"
263
+ end
264
+
265
+ # ensure the sender address matches the signature
266
+ unless @sender.nil? or sender.empty?
267
+ public_key = Signature.recover(unsigned_hash, signature, @chain_id)
268
+ signer_address = Tx.sanitize_address Util.public_key_to_address(public_key).to_s
269
+ from_address = Tx.sanitize_address @sender
270
+ raise Signature::SignatureError, "Signer does not match sender" unless signer_address == from_address
271
+ end
272
+
273
+ r, s, v = Signature.dissect signature
274
+ recovery_id = Chain.to_recovery_id v.to_i(16), @chain_id
275
+ send :_set_signature, recovery_id, r, s
276
+ return hash
277
+ end
278
+
254
279
  # Encodes a raw transaction object, wraps it in an EIP-2718 envelope
255
280
  # with an EIP-2930 type prefix.
256
281
  #