eth 0.5.14 → 0.5.17
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.
- checksums.yaml +4 -4
- data/.github/workflows/codeql.yml +4 -4
- data/.github/workflows/docs.yml +2 -2
- data/.github/workflows/spec.yml +32 -14
- data/CHANGELOG.md +53 -0
- data/CODE_OF_CONDUCT.md +3 -5
- data/Gemfile +3 -3
- data/README.md +9 -7
- data/SECURITY.md +2 -2
- data/eth.gemspec +11 -2
- data/lib/eth/abi/decoder.rb +94 -39
- data/lib/eth/abi/encoder.rb +85 -58
- data/lib/eth/abi/function.rb +124 -0
- data/lib/eth/abi/type.rb +69 -5
- data/lib/eth/abi.rb +2 -0
- data/lib/eth/bls.rb +68 -0
- data/lib/eth/chain.rb +3 -0
- data/lib/eth/client/http.rb +5 -8
- data/lib/eth/client/ws.rb +323 -0
- data/lib/eth/client.rb +48 -41
- data/lib/eth/contract/error.rb +62 -0
- data/lib/eth/contract/function.rb +21 -0
- data/lib/eth/contract/function_output.rb +11 -3
- data/lib/eth/contract.rb +55 -4
- data/lib/eth/eip712.rb +48 -13
- data/lib/eth/key.rb +1 -1
- data/lib/eth/tx/eip1559.rb +32 -7
- data/lib/eth/tx/eip2930.rb +31 -6
- data/lib/eth/tx/eip4844.rb +401 -0
- data/lib/eth/tx/eip7702.rb +34 -9
- data/lib/eth/tx/legacy.rb +30 -6
- data/lib/eth/tx.rb +45 -4
- data/lib/eth/util.rb +19 -7
- data/lib/eth/version.rb +1 -1
- data/lib/eth.rb +1 -0
- metadata +54 -16
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# Copyright (c) 2016-2025 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 "socket"
|
|
16
|
+
require "openssl"
|
|
17
|
+
require "uri"
|
|
18
|
+
require "base64"
|
|
19
|
+
require "securerandom"
|
|
20
|
+
require "digest/sha1"
|
|
21
|
+
require "thread"
|
|
22
|
+
require "ipaddr"
|
|
23
|
+
|
|
24
|
+
# Provides the {Eth} module.
|
|
25
|
+
module Eth
|
|
26
|
+
|
|
27
|
+
# Provides a WS/S-RPC client with automatic reconnection support.
|
|
28
|
+
class Client::Ws < Client
|
|
29
|
+
|
|
30
|
+
# The host of the WebSocket endpoint.
|
|
31
|
+
attr_reader :host
|
|
32
|
+
|
|
33
|
+
# The port of the WebSocket endpoint.
|
|
34
|
+
attr_reader :port
|
|
35
|
+
|
|
36
|
+
# The full URI of the WebSocket endpoint, including path.
|
|
37
|
+
attr_reader :uri
|
|
38
|
+
|
|
39
|
+
# Attribute indicator for SSL.
|
|
40
|
+
attr_reader :ssl
|
|
41
|
+
|
|
42
|
+
# Constructor for the WebSocket client. Should not be used; use
|
|
43
|
+
# {Client.create} instead.
|
|
44
|
+
#
|
|
45
|
+
# @param host [String] a URI pointing to a WebSocket RPC-API.
|
|
46
|
+
def initialize(host)
|
|
47
|
+
super
|
|
48
|
+
@uri = URI.parse(host)
|
|
49
|
+
raise ArgumentError, "Unable to parse the WebSocket-URI!" unless %w[ws wss].include?(@uri.scheme)
|
|
50
|
+
@host = @uri.host
|
|
51
|
+
@port = @uri.port
|
|
52
|
+
@ssl = @uri.scheme == "wss"
|
|
53
|
+
@path = build_path(@uri)
|
|
54
|
+
@mutex = Mutex.new
|
|
55
|
+
@socket = nil
|
|
56
|
+
@fragments = nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Sends an RPC request to the connected WebSocket endpoint.
|
|
60
|
+
#
|
|
61
|
+
# @param payload [Hash] the RPC request parameters.
|
|
62
|
+
# @return [String] a JSON-encoded response.
|
|
63
|
+
def send_request(payload)
|
|
64
|
+
attempts = 0
|
|
65
|
+
begin
|
|
66
|
+
attempts += 1
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
ensure_socket
|
|
69
|
+
write_frame(@socket, payload)
|
|
70
|
+
return read_message(@socket)
|
|
71
|
+
end
|
|
72
|
+
rescue IOError, SystemCallError => e
|
|
73
|
+
@mutex.synchronize { close_socket }
|
|
74
|
+
retry if attempts < 2
|
|
75
|
+
raise e
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Closes the underlying WebSocket connection.
|
|
80
|
+
#
|
|
81
|
+
# @return [void]
|
|
82
|
+
def close
|
|
83
|
+
@mutex.synchronize { close_socket }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def ensure_socket
|
|
89
|
+
return if @socket && !@socket.closed?
|
|
90
|
+
|
|
91
|
+
socket = open_socket
|
|
92
|
+
begin
|
|
93
|
+
perform_handshake(socket)
|
|
94
|
+
@socket = socket
|
|
95
|
+
@fragments = nil
|
|
96
|
+
rescue StandardError
|
|
97
|
+
begin
|
|
98
|
+
socket.close unless socket.closed?
|
|
99
|
+
rescue IOError, SystemCallError
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
@socket = nil
|
|
103
|
+
raise
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Establishes the TCP socket for the RPC connection and upgrades it to TLS
|
|
108
|
+
# when a secure endpoint is requested. TLS sessions enforce peer
|
|
109
|
+
# verification, load the default system trust store, and enable hostname
|
|
110
|
+
# verification when the current OpenSSL bindings support it.
|
|
111
|
+
#
|
|
112
|
+
# @return [TCPSocket, OpenSSL::SSL::SSLSocket] the established socket.
|
|
113
|
+
# @raise [IOError, SystemCallError, OpenSSL::SSL::SSLError] if the socket
|
|
114
|
+
# cannot be opened or the TLS handshake fails.
|
|
115
|
+
def open_socket
|
|
116
|
+
tcp = TCPSocket.new(@host, @port)
|
|
117
|
+
return tcp unless @ssl
|
|
118
|
+
|
|
119
|
+
context = OpenSSL::SSL::SSLContext.new
|
|
120
|
+
params = { verify_mode: OpenSSL::SSL::VERIFY_PEER }
|
|
121
|
+
params[:verify_hostname] = true if context.respond_to?(:verify_hostname=)
|
|
122
|
+
context.set_params(params)
|
|
123
|
+
context.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
|
|
124
|
+
|
|
125
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp, context)
|
|
126
|
+
ssl_socket.hostname = @host
|
|
127
|
+
ssl_socket.sync_close = true
|
|
128
|
+
ssl_socket.connect
|
|
129
|
+
ssl_socket
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def perform_handshake(socket)
|
|
133
|
+
key = Base64.strict_encode64(SecureRandom.random_bytes(16))
|
|
134
|
+
request = build_handshake_request(key)
|
|
135
|
+
socket.write(request)
|
|
136
|
+
response = read_handshake_response(socket)
|
|
137
|
+
verify_handshake!(response, key)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_handshake_request(key)
|
|
141
|
+
origin = build_origin_header
|
|
142
|
+
host_header = build_host_header
|
|
143
|
+
"GET #{@path} HTTP/1.1\r\n" \
|
|
144
|
+
"Host: #{host_header}\r\n" \
|
|
145
|
+
"Upgrade: websocket\r\n" \
|
|
146
|
+
"Connection: Upgrade\r\n" \
|
|
147
|
+
"Sec-WebSocket-Version: 13\r\n" \
|
|
148
|
+
"Sec-WebSocket-Key: #{key}\r\n" \
|
|
149
|
+
"Origin: #{origin}\r\n\r\n"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def read_handshake_response(socket)
|
|
153
|
+
response = +""
|
|
154
|
+
until response.end_with?("\r\n\r\n")
|
|
155
|
+
chunk = socket.readpartial(1024)
|
|
156
|
+
raise IOError, "Incomplete WebSocket handshake" if chunk.nil?
|
|
157
|
+
response << chunk
|
|
158
|
+
end
|
|
159
|
+
response
|
|
160
|
+
rescue EOFError
|
|
161
|
+
raise IOError, "Incomplete WebSocket handshake"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def verify_handshake!(response, key)
|
|
165
|
+
status_line = response.lines.first&.strip
|
|
166
|
+
unless status_line&.start_with?("HTTP/1.1 101")
|
|
167
|
+
raise IOError, "WebSocket handshake failed (status: #{status_line || "unknown"})"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
accept = response[/Sec-WebSocket-Accept:\s*(.+)\r/i, 1]&.strip
|
|
171
|
+
expected = Base64.strict_encode64(Digest::SHA1.digest("#{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
|
|
172
|
+
raise IOError, "WebSocket handshake failed (missing accept header)" unless accept
|
|
173
|
+
raise IOError, "WebSocket handshake failed (invalid accept header)" unless accept == expected
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def write_frame(socket, payload, opcode = 0x1)
|
|
177
|
+
frame_payload = payload.is_a?(String) ? payload.dup : payload.to_s
|
|
178
|
+
mask_key = SecureRandom.random_bytes(4)
|
|
179
|
+
header = [0x80 | opcode]
|
|
180
|
+
|
|
181
|
+
length = frame_payload.bytesize
|
|
182
|
+
if length <= 125
|
|
183
|
+
header << (0x80 | length)
|
|
184
|
+
elsif length <= 0xFFFF
|
|
185
|
+
header << (0x80 | 126)
|
|
186
|
+
header.concat([length].pack("n").bytes)
|
|
187
|
+
else
|
|
188
|
+
header << (0x80 | 127)
|
|
189
|
+
header.concat([length].pack("Q>").bytes)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
masked_payload = apply_mask(frame_payload, mask_key)
|
|
193
|
+
socket.write(header.pack("C*") + mask_key + masked_payload)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def read_message(socket)
|
|
197
|
+
loop do
|
|
198
|
+
frame = read_frame(socket)
|
|
199
|
+
return frame if frame
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def read_frame(socket)
|
|
204
|
+
header = read_bytes(socket, 2)
|
|
205
|
+
byte1, byte2 = header.bytes
|
|
206
|
+
opcode = byte1 & 0x0F
|
|
207
|
+
masked = (byte2 & 0x80) == 0x80
|
|
208
|
+
length = byte2 & 0x7F
|
|
209
|
+
|
|
210
|
+
length = read_bytes(socket, 2).unpack1("n") if length == 126
|
|
211
|
+
length = read_bytes(socket, 8).unpack1("Q>") if length == 127
|
|
212
|
+
|
|
213
|
+
mask_key = masked ? read_bytes(socket, 4).bytes : nil
|
|
214
|
+
payload = read_bytes(socket, length)
|
|
215
|
+
payload_bytes = payload.bytes
|
|
216
|
+
if mask_key
|
|
217
|
+
payload_bytes.map!.with_index { |byte, index| byte ^ mask_key[index % 4] }
|
|
218
|
+
end
|
|
219
|
+
data = payload_bytes.pack("C*")
|
|
220
|
+
|
|
221
|
+
case opcode
|
|
222
|
+
when 0x0
|
|
223
|
+
(@fragments ||= +"") << data
|
|
224
|
+
if (byte1 & 0x80) == 0x80
|
|
225
|
+
message = @fragments.dup
|
|
226
|
+
@fragments = nil
|
|
227
|
+
message
|
|
228
|
+
else
|
|
229
|
+
nil
|
|
230
|
+
end
|
|
231
|
+
when 0x1, 0x2
|
|
232
|
+
if (byte1 & 0x80) == 0x80
|
|
233
|
+
data
|
|
234
|
+
else
|
|
235
|
+
@fragments = data
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
when 0x8
|
|
239
|
+
close_socket
|
|
240
|
+
raise IOError, "WebSocket closed"
|
|
241
|
+
when 0x9
|
|
242
|
+
write_frame(socket, data, 0xA)
|
|
243
|
+
nil
|
|
244
|
+
when 0xA
|
|
245
|
+
nil
|
|
246
|
+
else
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def read_bytes(socket, length)
|
|
252
|
+
data = +""
|
|
253
|
+
while data.bytesize < length
|
|
254
|
+
chunk = socket.read(length - data.bytesize)
|
|
255
|
+
raise IOError, "Unexpected end of WebSocket stream" if chunk.nil? || chunk.empty?
|
|
256
|
+
data << chunk
|
|
257
|
+
end
|
|
258
|
+
data
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def apply_mask(payload, mask_key)
|
|
262
|
+
mask_bytes = mask_key.bytes
|
|
263
|
+
payload.bytes.map.with_index { |byte, index| byte ^ mask_bytes[index % 4] }.pack("C*")
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def build_origin_header
|
|
267
|
+
scheme = @ssl ? "https" : "http"
|
|
268
|
+
host = format_origin_host(@uri.host)
|
|
269
|
+
default_port = @ssl ? 443 : 80
|
|
270
|
+
port = @uri.port
|
|
271
|
+
port_suffix = port == default_port ? "" : ":#{port}"
|
|
272
|
+
"#{scheme}://#{host}#{port_suffix}"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def build_host_header
|
|
276
|
+
"#{format_host(@uri.host)}:#{@uri.port}"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def format_origin_host(host)
|
|
280
|
+
return "localhost" if loopback_host?(host)
|
|
281
|
+
format_host(host)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def format_host(host)
|
|
285
|
+
return host unless host&.include?(":")
|
|
286
|
+
host.start_with?("[") ? host : "[#{host}]"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def loopback_host?(host)
|
|
290
|
+
return false if host.nil?
|
|
291
|
+
return true if host == "localhost"
|
|
292
|
+
IPAddr.new(host).loopback?
|
|
293
|
+
rescue IPAddr::InvalidAddressError
|
|
294
|
+
false
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def close_socket
|
|
298
|
+
return unless @socket
|
|
299
|
+
|
|
300
|
+
begin
|
|
301
|
+
write_frame(@socket, [1000].pack("n"), 0x8)
|
|
302
|
+
rescue IOError, SystemCallError
|
|
303
|
+
# ignore errors while closing
|
|
304
|
+
ensure
|
|
305
|
+
begin
|
|
306
|
+
@socket.close unless @socket.closed?
|
|
307
|
+
rescue IOError, SystemCallError
|
|
308
|
+
nil
|
|
309
|
+
end
|
|
310
|
+
@socket = nil
|
|
311
|
+
@fragments = nil
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def build_path(uri)
|
|
316
|
+
path = uri.path
|
|
317
|
+
path = "/" if path.nil? || path.empty?
|
|
318
|
+
query = uri.query
|
|
319
|
+
path += "?#{query}" if query
|
|
320
|
+
path
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
data/lib/eth/client.rb
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
module Eth
|
|
17
17
|
|
|
18
18
|
# Provides the {Eth::Client} super-class to connect to Ethereum
|
|
19
|
-
# network's RPC-API endpoints (IPC or
|
|
19
|
+
# network's RPC-API endpoints (IPC, HTTP/S, or WS/S).
|
|
20
20
|
class Client
|
|
21
21
|
|
|
22
22
|
# The client's RPC-request ID starting at 0.
|
|
@@ -40,20 +40,40 @@ module Eth
|
|
|
40
40
|
# A custom error type if a contract interaction fails.
|
|
41
41
|
class ContractExecutionError < StandardError; end
|
|
42
42
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
43
|
+
# Raised when an RPC call returns an error. Carries the error code and the optional
|
|
44
|
+
# hex-encoded error data to support custom error decoding.
|
|
45
|
+
class RpcError < IOError
|
|
46
|
+
attr_reader :data
|
|
47
|
+
attr_reader :code
|
|
48
|
+
|
|
49
|
+
# Constructor for the {RpcError} class.
|
|
50
|
+
#
|
|
51
|
+
# @param message [String] the error message returned by the RPC.
|
|
52
|
+
# @param data [String] optional hex encoded error data.
|
|
53
|
+
# @param code [String] optional error code returned by the RPC.
|
|
54
|
+
def initialize(message, data = nil, code = nil)
|
|
55
|
+
super(message)
|
|
56
|
+
@data = data
|
|
57
|
+
@code = code
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Creates a new RPC-Client, either by providing an HTTP/S host, WS/S host,
|
|
62
|
+
# or an IPC path. Supports basic authentication with username and password.
|
|
45
63
|
#
|
|
46
|
-
# **Note**, this sets the
|
|
64
|
+
# **Note**, this sets the following gas defaults: {Tx::DEFAULT_PRIORITY_FEE}
|
|
47
65
|
# and {Tx::DEFAULT_GAS_PRICE. Use {#max_priority_fee_per_gas} and
|
|
48
66
|
# {#max_fee_per_gas} to set custom values prior to submitting transactions.
|
|
49
67
|
#
|
|
50
|
-
# @param host [String] either an HTTP/S host or an IPC path.
|
|
68
|
+
# @param host [String] either an HTTP/S host, WS/S host, or an IPC path.
|
|
51
69
|
# @return [Eth::Client::Ipc] an IPC client.
|
|
52
70
|
# @return [Eth::Client::Http] an HTTP client.
|
|
71
|
+
# @return [Eth::Client::Ws] a WebSocket client.
|
|
53
72
|
# @raise [ArgumentError] in case it cannot determine the client type.
|
|
54
73
|
def self.create(host)
|
|
55
74
|
return Client::Ipc.new host if host.end_with? ".ipc"
|
|
56
75
|
return Client::Http.new host if host.start_with? "http"
|
|
76
|
+
return Client::Ws.new host if host.start_with? "ws"
|
|
57
77
|
raise ArgumentError, "Unable to detect client type!"
|
|
58
78
|
end
|
|
59
79
|
|
|
@@ -251,21 +271,28 @@ module Eth
|
|
|
251
271
|
# @param **sender_key [Eth::Key] the sender private key.
|
|
252
272
|
# @param **legacy [Boolean] enables legacy transactions (pre-EIP-1559).
|
|
253
273
|
# @return [Object] returns the result of the call.
|
|
274
|
+
# @see https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call
|
|
254
275
|
def call(contract, function, *args, **kwargs)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
276
|
+
function = contract.function(function, args: args.size)
|
|
277
|
+
output = function.decode_call_result(
|
|
278
|
+
eth_call(
|
|
279
|
+
{
|
|
280
|
+
data: function.encode_call(*args),
|
|
281
|
+
to: kwargs[:address] || contract.address,
|
|
282
|
+
from: kwargs[:from],
|
|
283
|
+
gas: kwargs[:gas],
|
|
284
|
+
gasPrice: kwargs[:gas_price],
|
|
285
|
+
value: kwargs[:value],
|
|
286
|
+
}.compact
|
|
287
|
+
)["result"]
|
|
288
|
+
)
|
|
264
289
|
if output&.length == 1
|
|
265
290
|
output[0]
|
|
266
291
|
else
|
|
267
292
|
output
|
|
268
293
|
end
|
|
294
|
+
rescue RpcError => e
|
|
295
|
+
raise ContractExecutionError, contract.decode_error(e)
|
|
269
296
|
end
|
|
270
297
|
|
|
271
298
|
# Executes a contract function with a transaction (transactional
|
|
@@ -298,13 +325,12 @@ module Eth
|
|
|
298
325
|
else
|
|
299
326
|
Tx.estimate_intrinsic_gas(contract.bin)
|
|
300
327
|
end
|
|
301
|
-
fun = contract.functions.select { |func| func.name == function }[0]
|
|
302
328
|
params = {
|
|
303
329
|
value: kwargs[:tx_value] || 0,
|
|
304
330
|
gas_limit: gas_limit,
|
|
305
331
|
chain_id: chain_id,
|
|
306
332
|
to: kwargs[:address] || contract.address,
|
|
307
|
-
data:
|
|
333
|
+
data: contract.function(function, args: args.size).encode_call(*args),
|
|
308
334
|
}
|
|
309
335
|
send_transaction(params, kwargs[:legacy], kwargs[:sender_key], kwargs[:nonce])
|
|
310
336
|
end
|
|
@@ -320,8 +346,8 @@ module Eth
|
|
|
320
346
|
begin
|
|
321
347
|
hash = wait_for_tx(transact(contract, function, *args, **kwargs))
|
|
322
348
|
return hash, tx_succeeded?(hash)
|
|
323
|
-
rescue
|
|
324
|
-
raise ContractExecutionError, e
|
|
349
|
+
rescue RpcError => e
|
|
350
|
+
raise ContractExecutionError, contract.decode_error(e)
|
|
325
351
|
end
|
|
326
352
|
end
|
|
327
353
|
|
|
@@ -442,28 +468,6 @@ module Eth
|
|
|
442
468
|
end
|
|
443
469
|
end
|
|
444
470
|
|
|
445
|
-
# Non-transactional function call called from call().
|
|
446
|
-
# @see https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call
|
|
447
|
-
def call_raw(contract, func, *args, **kwargs)
|
|
448
|
-
params = {
|
|
449
|
-
data: call_payload(func, args),
|
|
450
|
-
to: kwargs[:address] || contract.address,
|
|
451
|
-
from: kwargs[:from],
|
|
452
|
-
}.compact
|
|
453
|
-
|
|
454
|
-
raw_result = eth_call(params)["result"]
|
|
455
|
-
types = func.outputs.map { |i| i.type }
|
|
456
|
-
return nil if raw_result == "0x"
|
|
457
|
-
Eth::Abi.decode(types, raw_result)
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
# Encodes function call payloads.
|
|
461
|
-
def call_payload(fun, args)
|
|
462
|
-
types = fun.inputs.map(&:parsed_type)
|
|
463
|
-
encoded_str = Util.bin_to_hex(Eth::Abi.encode(types, args))
|
|
464
|
-
Util.prefix_hex(fun.signature + (encoded_str.empty? ? "0" * 64 : encoded_str))
|
|
465
|
-
end
|
|
466
|
-
|
|
467
471
|
# Encodes constructor params
|
|
468
472
|
def encode_constructor_params(contract, args)
|
|
469
473
|
types = contract.constructor_inputs.map { |input| input.type }
|
|
@@ -481,7 +485,9 @@ module Eth
|
|
|
481
485
|
id: next_id,
|
|
482
486
|
}
|
|
483
487
|
output = JSON.parse(send_request(payload.to_json))
|
|
484
|
-
|
|
488
|
+
if (err = output["error"])
|
|
489
|
+
raise RpcError.new(err["message"], err["data"], err["code"])
|
|
490
|
+
end
|
|
485
491
|
output
|
|
486
492
|
end
|
|
487
493
|
|
|
@@ -523,3 +529,4 @@ end
|
|
|
523
529
|
# Load the client/* libraries
|
|
524
530
|
require "eth/client/http"
|
|
525
531
|
require "eth/client/ipc"
|
|
532
|
+
require "eth/client/ws"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright (c) 2016-2025 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
|
+
# Provide classes for contract custom errors.
|
|
20
|
+
class Contract::Error
|
|
21
|
+
attr_accessor :name, :inputs, :signature, :error_string
|
|
22
|
+
|
|
23
|
+
# Constructor of the {Eth::Contract::Error} class.
|
|
24
|
+
#
|
|
25
|
+
# @param data [Hash] contract abi data for the error.
|
|
26
|
+
def initialize(data)
|
|
27
|
+
@name = data["name"]
|
|
28
|
+
@inputs = data.fetch("inputs", []).map do |input|
|
|
29
|
+
Eth::Contract::FunctionInput.new(input)
|
|
30
|
+
end
|
|
31
|
+
@error_string = self.class.calc_signature(@name, @inputs)
|
|
32
|
+
@signature = self.class.encoded_error_signature(@error_string)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Creates error strings.
|
|
36
|
+
#
|
|
37
|
+
# @param name [String] error name.
|
|
38
|
+
# @param inputs [Array<Eth::Contract::FunctionInput>] error input class list.
|
|
39
|
+
# @return [String] error string.
|
|
40
|
+
def self.calc_signature(name, inputs)
|
|
41
|
+
"#{name}(#{inputs.map { |x| x.parsed_type.to_s }.join(",")})"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Encodes an error signature.
|
|
45
|
+
#
|
|
46
|
+
# @param signature [String] error signature.
|
|
47
|
+
# @return [String] encoded error signature string.
|
|
48
|
+
def self.encoded_error_signature(signature)
|
|
49
|
+
Util.prefix_hex(Util.bin_to_hex(Util.keccak256(signature)[0..3]))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Decodes a revert error payload.
|
|
53
|
+
#
|
|
54
|
+
# @param data [String] the hex-encoded revert data including selector.
|
|
55
|
+
# @return [Array] decoded error arguments.
|
|
56
|
+
def decode(data)
|
|
57
|
+
types = inputs.map(&:type)
|
|
58
|
+
payload = "0x" + data[10..]
|
|
59
|
+
Eth::Abi.decode(types, payload)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -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
|
-
@
|
|
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 +
|
|
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
|