block_given 0.1.0 → 0.2.0
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/CHANGELOG.md +34 -1
- data/README.md +92 -55
- data/lib/block_given/abi/codec/decoder.rb +111 -0
- data/lib/block_given/abi/codec.rb +170 -0
- data/lib/block_given/abi/coder.rb +122 -11
- data/lib/block_given/abi/custom_error.rb +33 -1
- data/lib/block_given/abi/event.rb +85 -3
- data/lib/block_given/abi/function.rb +94 -2
- data/lib/block_given/abi/interface.rb +99 -4
- data/lib/block_given/abi/parameter.rb +60 -3
- data/lib/block_given/abi/standards/erc1155.rb +41 -0
- data/lib/block_given/abi/standards/erc20.rb +33 -0
- data/lib/block_given/abi/standards/erc4626.rb +41 -0
- data/lib/block_given/abi/standards/erc721.rb +47 -0
- data/lib/block_given/abi/standards.rb +124 -0
- data/lib/block_given/abi/type.rb +138 -0
- data/lib/block_given/chain.rb +114 -2
- data/lib/block_given/client.rb +322 -20
- data/lib/block_given/configuration.rb +85 -3
- data/lib/block_given/connectors/alchemy.rb +38 -4
- data/lib/block_given/connectors/base.rb +32 -5
- data/lib/block_given/connectors/http.rb +91 -5
- data/lib/block_given/connectors/stub.rb +70 -4
- data/lib/block_given/contract.rb +422 -26
- data/lib/block_given/crypto/keccak.rb +152 -0
- data/lib/block_given/crypto/secp256k1.rb +168 -0
- data/lib/block_given/crypto.rb +22 -0
- data/lib/block_given/eip712.rb +199 -0
- data/lib/block_given/errors.rb +134 -14
- data/lib/block_given/event.rb +51 -1
- data/lib/block_given/normalizer.rb +27 -2
- data/lib/block_given/poller.rb +177 -15
- data/lib/block_given/receipt.rb +63 -3
- data/lib/block_given/rlp.rb +146 -0
- data/lib/block_given/signed_transaction.rb +148 -47
- data/lib/block_given/transaction.rb +103 -12
- data/lib/block_given/transaction_envelope/fields.rb +104 -0
- data/lib/block_given/transaction_envelope.rb +183 -0
- data/lib/block_given/utils.rb +143 -9
- data/lib/block_given/version.rb +2 -1
- data/lib/block_given/wallet.rb +216 -35
- data/lib/block_given.rb +67 -4
- metadata +19 -23
data/lib/block_given/poller.rb
CHANGED
|
@@ -3,15 +3,32 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
5
|
module BlockGiven
|
|
6
|
-
# Blocking polling helper.
|
|
6
|
+
# Blocking polling helper: calls a block repeatedly until it returns a truthy value or a timeout elapses.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
8
|
+
# Used by {Client#wait_for_transaction_receipt}; background (non-blocking) polling is {Watcher}'s job.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# @example
|
|
11
|
+
# receipt = BlockGiven::Poller.poll(interval: 2, timeout: 120, description: "receipt") do
|
|
12
|
+
# client.get_transaction_receipt(hash)
|
|
13
|
+
# end
|
|
12
14
|
module Poller
|
|
13
15
|
module_function
|
|
14
16
|
|
|
17
|
+
# Calls the block until it returns a non-nil / non-false value, sleeping `interval` seconds between
|
|
18
|
+
# attempts, and returns that value.
|
|
19
|
+
#
|
|
20
|
+
# The timeout is checked after each failed attempt against a monotonic clock, and the last sleep is
|
|
21
|
+
# shortened so the deadline is never overshot by more than one interval. With `timeout: nil` the loop
|
|
22
|
+
# runs until the block succeeds.
|
|
23
|
+
#
|
|
24
|
+
# @param interval [Numeric] seconds slept between two attempts
|
|
25
|
+
# @param timeout [Numeric, nil] seconds after which {TimeoutError} is raised; nil waits forever
|
|
26
|
+
# @param description [String] what is being waited for, used in the {TimeoutError} message
|
|
27
|
+
# @yield [attempt] once per attempt, starting immediately (no initial sleep)
|
|
28
|
+
# @yieldparam attempt [Integer] zero-based attempt counter
|
|
29
|
+
# @yieldreturn [Object, nil, false] the value to return, or nil / false to keep polling
|
|
30
|
+
# @return [Object] the first truthy value returned by the block
|
|
31
|
+
# @raise [TimeoutError] when `timeout` seconds elapsed without the block returning a truthy value
|
|
15
32
|
def poll(interval:, timeout: nil, description: "condition")
|
|
16
33
|
started = monotonic_now
|
|
17
34
|
attempt = 0
|
|
@@ -28,47 +45,95 @@ module BlockGiven
|
|
|
28
45
|
end
|
|
29
46
|
end
|
|
30
47
|
|
|
48
|
+
# Current reading of the monotonic clock, in seconds (immune to wall-clock adjustments).
|
|
49
|
+
#
|
|
50
|
+
# @api private
|
|
51
|
+
# @return [Float]
|
|
31
52
|
def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
32
53
|
end
|
|
33
54
|
|
|
34
|
-
# Background polling loop running in its own thread
|
|
35
|
-
# `watch_*` helpers; call #stop (alias #unwatch) to end it.
|
|
55
|
+
# Background polling loop running in its own thread, returned by the `watch_*` helpers of {Client}.
|
|
36
56
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
57
|
+
# A watcher calls its tick block, sleeps `interval` seconds (interruptible by {#stop}), and repeats until
|
|
58
|
+
# stopped. Exceptions raised by the tick are caught: they are recorded in {#last_error} / {#last_error_at}
|
|
59
|
+
# and either passed to the {#on_error} handler or logged as warnings, and polling continues. The thread is
|
|
60
|
+
# named `block_given:<id>` and does not report exceptions itself.
|
|
39
61
|
#
|
|
62
|
+
# Every running watcher is registered under a unique {#id}, so watchers can be listed ({Watcher.all},
|
|
63
|
+
# {BlockGiven.watchers}) and stopped ({Watcher.stop}, {Watcher.stop_all}) even when the object reference
|
|
64
|
+
# was lost. A watcher leaves the registry when its thread ends (or is killed). Starting a second watcher
|
|
65
|
+
# with the id of a running one raises {InvalidArgumentError}.
|
|
66
|
+
#
|
|
67
|
+
# {#cursor} is free storage for the tick: {Client#watch_logs} keeps the last fully processed block number
|
|
68
|
+
# there, so a supervisor can read where the watcher stands.
|
|
69
|
+
#
|
|
70
|
+
# @example Registry lookups by id
|
|
40
71
|
# watcher = client.watch_block_number(id: "blocks") { |n| puts n }
|
|
41
|
-
# BlockGiven.watchers
|
|
42
|
-
# BlockGiven::Watcher.find("blocks").
|
|
72
|
+
# BlockGiven.watchers # => [#<BlockGiven::Watcher blocks block_number running ...>]
|
|
73
|
+
# BlockGiven::Watcher.find("blocks").status # => :running
|
|
74
|
+
# BlockGiven::Watcher.stop("blocks", join: 5)
|
|
43
75
|
# BlockGiven::Watcher.stop_all
|
|
76
|
+
# @example Custom error handling
|
|
77
|
+
# watcher.on_error { |error, w| Sentry.capture_exception(error, extra: w.to_h) }
|
|
44
78
|
class Watcher
|
|
45
79
|
@registry = {}
|
|
46
80
|
@registry_mutex = Mutex.new
|
|
47
81
|
|
|
48
82
|
class << self
|
|
49
83
|
# Running (or stopping) watchers, oldest first.
|
|
84
|
+
#
|
|
85
|
+
# @return [Array<Watcher>] a snapshot copy of the registry
|
|
50
86
|
def all
|
|
51
87
|
@registry_mutex.synchronize { @registry.values.dup }
|
|
52
88
|
end
|
|
53
89
|
|
|
90
|
+
# Looks up a running watcher by id.
|
|
91
|
+
#
|
|
92
|
+
# @example
|
|
93
|
+
# BlockGiven::Watcher.find("usdc-transfers")&.cursor
|
|
94
|
+
# @param id [String, Symbol, #to_s] watcher id
|
|
95
|
+
# @return [Watcher, nil] the watcher, or nil when no running watcher has this id
|
|
54
96
|
def find(id)
|
|
55
97
|
@registry_mutex.synchronize { @registry[id.to_s] }
|
|
56
98
|
end
|
|
57
99
|
|
|
100
|
+
# Looks up a running watcher by id, raising when it is unknown.
|
|
101
|
+
#
|
|
102
|
+
# @param id [String, Symbol, #to_s] watcher id
|
|
103
|
+
# @return [Watcher]
|
|
104
|
+
# @raise [InvalidArgumentError] when no running watcher has this id (the message lists the running ids)
|
|
58
105
|
def find!(id)
|
|
59
106
|
find(id) || raise(InvalidArgumentError, "no running watcher with id #{id.inspect} (running: #{ids.join(', ')})")
|
|
60
107
|
end
|
|
61
108
|
|
|
109
|
+
# Ids of the running watchers, oldest first.
|
|
110
|
+
#
|
|
111
|
+
# @return [Array<String>]
|
|
62
112
|
def ids = all.map(&:id)
|
|
63
113
|
|
|
64
|
-
#
|
|
114
|
+
# Gracefully stops a watcher by id (see {#stop}) and optionally waits for its thread to end.
|
|
115
|
+
#
|
|
116
|
+
# @example
|
|
117
|
+
# BlockGiven::Watcher.stop("usdc-transfers", join: 5) # => the watcher, or nil if it was not running
|
|
118
|
+
# @param id [String, Symbol, #to_s] watcher id
|
|
119
|
+
# @param join [Numeric, nil] seconds to wait for the thread after asking it to stop; nil waits until it
|
|
120
|
+
# ends, but note that {#join} is only called when the watcher exists
|
|
121
|
+
# @return [Watcher, nil] the stopped watcher, or nil when no running watcher has this id
|
|
65
122
|
def stop(id, join: nil)
|
|
66
123
|
watcher = find(id)
|
|
67
124
|
watcher&.stop&.join(join)
|
|
68
125
|
end
|
|
69
126
|
|
|
127
|
+
# Forcefully kills a watcher by id (see {#kill}).
|
|
128
|
+
#
|
|
129
|
+
# @param id [String, Symbol, #to_s] watcher id
|
|
130
|
+
# @return [Watcher, nil] the killed watcher, or nil when no running watcher has this id
|
|
70
131
|
def kill(id) = find(id)&.kill
|
|
71
132
|
|
|
133
|
+
# Gracefully stops every running watcher, then waits for each thread.
|
|
134
|
+
#
|
|
135
|
+
# @param join [Numeric, nil] seconds to wait for each thread; nil waits until it ends
|
|
136
|
+
# @return [Array<Watcher>] the watchers that were running
|
|
72
137
|
def stop_all(join: nil)
|
|
73
138
|
watchers = all
|
|
74
139
|
watchers.each(&:stop)
|
|
@@ -76,9 +141,17 @@ module BlockGiven
|
|
|
76
141
|
watchers
|
|
77
142
|
end
|
|
78
143
|
|
|
144
|
+
# Forcefully kills every running watcher (see {#kill}).
|
|
145
|
+
#
|
|
146
|
+
# @return [Array<Watcher>] the watchers that were running
|
|
79
147
|
def kill_all = all.each(&:kill)
|
|
80
148
|
|
|
149
|
+
# Adds a watcher to the registry under its id; called by {#start}.
|
|
150
|
+
#
|
|
81
151
|
# @api private
|
|
152
|
+
# @param watcher [Watcher]
|
|
153
|
+
# @return [Watcher] the registered watcher
|
|
154
|
+
# @raise [InvalidArgumentError] when a different watcher with the same id is already registered
|
|
82
155
|
def register(watcher)
|
|
83
156
|
@registry_mutex.synchronize do
|
|
84
157
|
if (existing = @registry[watcher.id]) && !existing.equal?(watcher)
|
|
@@ -89,18 +162,60 @@ module BlockGiven
|
|
|
89
162
|
end
|
|
90
163
|
end
|
|
91
164
|
|
|
165
|
+
# Removes a watcher from the registry, only if the registered object is this very watcher.
|
|
166
|
+
#
|
|
92
167
|
# @api private
|
|
168
|
+
# @param watcher [Watcher]
|
|
169
|
+
# @return [Watcher, nil] the removed watcher, or nil when it was not registered
|
|
93
170
|
def unregister(watcher)
|
|
94
171
|
@registry_mutex.synchronize { @registry.delete(watcher.id) if @registry[watcher.id].equal?(watcher) }
|
|
95
172
|
end
|
|
96
173
|
|
|
174
|
+
# Builds a unique id from a name: the name with unsafe characters replaced by `_`, plus a random suffix.
|
|
175
|
+
#
|
|
176
|
+
# @example
|
|
177
|
+
# BlockGiven::Watcher.generate_id("logs@0xAbc") # => "logs@0xAbc-3f9a1c"
|
|
178
|
+
# @param name [String, #to_s] human-readable name
|
|
179
|
+
# @return [String] `"<sanitized name>-<6 hex chars>"`
|
|
97
180
|
def generate_id(name) = "#{name.to_s.gsub(/[^a-zA-Z0-9_.:@-]/, '_')}-#{SecureRandom.hex(3)}"
|
|
98
181
|
end
|
|
99
182
|
|
|
183
|
+
# @!attribute [r] id
|
|
184
|
+
# @return [String] unique identifier used by the registry ({Watcher.find}, {Watcher.stop})
|
|
185
|
+
# @!attribute [r] interval
|
|
186
|
+
# @return [Numeric] seconds slept between two ticks
|
|
187
|
+
# @!attribute [r] name
|
|
188
|
+
# @return [String] human-readable name (e.g. `"block_number"`, `"logs@0x..."`)
|
|
189
|
+
# @!attribute [r] started_at
|
|
190
|
+
# @return [Time, nil] when {#start} was last called; nil while idle
|
|
191
|
+
# @!attribute [r] ticks
|
|
192
|
+
# @return [Integer] number of completed ticks, including those that raised
|
|
193
|
+
# @!attribute [r] last_error
|
|
194
|
+
# @return [StandardError, nil] the most recent exception raised by the tick, if any
|
|
195
|
+
# @!attribute [r] last_error_at
|
|
196
|
+
# @return [Time, nil] when {#last_error} was recorded
|
|
197
|
+
# @!attribute [r] last_tick_at
|
|
198
|
+
# @return [Time, nil] when the most recent tick finished
|
|
199
|
+
|
|
100
200
|
attr_reader :id, :interval, :name, :started_at, :ticks, :last_error, :last_error_at, :last_tick_at
|
|
101
|
-
# Last fully processed position
|
|
201
|
+
# Last fully processed position, set by the tick: {Client#watch_logs} stores the last block number whose
|
|
202
|
+
# logs were handed to the caller, and only after they were. nil until the first tick ran.
|
|
203
|
+
#
|
|
204
|
+
# @return [Integer, Object, nil]
|
|
102
205
|
attr_accessor :cursor
|
|
103
206
|
|
|
207
|
+
# Builds a watcher without starting it; call {#start}.
|
|
208
|
+
#
|
|
209
|
+
# @param interval [Numeric] seconds slept between two ticks
|
|
210
|
+
# @param name [String, #to_s] human-readable name, also the base of the generated id
|
|
211
|
+
# @param id [String, #to_s, nil] stable identifier for the registry. Defaults to {Watcher.generate_id}.
|
|
212
|
+
# @param logger [Logger, nil] receives a warning per failed tick when no `on_error` handler is set.
|
|
213
|
+
# Defaults to `BlockGiven.config.logger`.
|
|
214
|
+
# @param on_error [#call, nil] error handler called with `(error, watcher)`; see {#on_error}
|
|
215
|
+
# @yield [watcher] on every tick, from the watcher thread
|
|
216
|
+
# @yieldparam watcher [Watcher] the watcher itself, to check {#stopped?} or update {#cursor}
|
|
217
|
+
# @yieldreturn [void]
|
|
218
|
+
# @raise [ArgumentError] when no block is given
|
|
104
219
|
def initialize(interval:, name: "watcher", id: nil, logger: nil, on_error: nil, &tick)
|
|
105
220
|
raise ::ArgumentError, "a block is required" unless tick
|
|
106
221
|
|
|
@@ -121,6 +236,12 @@ module BlockGiven
|
|
|
121
236
|
@last_error_at = nil
|
|
122
237
|
end
|
|
123
238
|
|
|
239
|
+
# Registers the watcher and starts its thread (named `block_given:<id>`). No-op when already running.
|
|
240
|
+
#
|
|
241
|
+
# The first tick runs immediately in the new thread. A watcher can be restarted after it stopped.
|
|
242
|
+
#
|
|
243
|
+
# @return [self]
|
|
244
|
+
# @raise [InvalidArgumentError] when another watcher with the same id is running
|
|
124
245
|
def start
|
|
125
246
|
return self if running?
|
|
126
247
|
|
|
@@ -133,7 +254,10 @@ module BlockGiven
|
|
|
133
254
|
self
|
|
134
255
|
end
|
|
135
256
|
|
|
136
|
-
#
|
|
257
|
+
# Asks the watcher to stop gracefully: the current tick finishes, the sleep is interrupted, then the
|
|
258
|
+
# thread exits and the watcher leaves the registry. Returns at once; use {#join} to wait.
|
|
259
|
+
#
|
|
260
|
+
# @return [self]
|
|
137
261
|
def stop
|
|
138
262
|
@mutex.synchronize do
|
|
139
263
|
@stopped = true
|
|
@@ -143,7 +267,10 @@ module BlockGiven
|
|
|
143
267
|
end
|
|
144
268
|
alias unwatch stop
|
|
145
269
|
|
|
146
|
-
#
|
|
270
|
+
# Stops forcefully: kills the thread even in the middle of a tick and unregisters the watcher at once.
|
|
271
|
+
# Use it when {#stop} does not return because a tick hangs.
|
|
272
|
+
#
|
|
273
|
+
# @return [self]
|
|
147
274
|
def kill
|
|
148
275
|
stop
|
|
149
276
|
@thread&.kill
|
|
@@ -151,9 +278,20 @@ module BlockGiven
|
|
|
151
278
|
self
|
|
152
279
|
end
|
|
153
280
|
|
|
281
|
+
# Whether the watcher thread is alive (also true while it is stopping).
|
|
282
|
+
#
|
|
283
|
+
# @return [Boolean]
|
|
154
284
|
def running? = !!@thread&.alive?
|
|
285
|
+
|
|
286
|
+
# Whether {#stop} (or {#kill}) was requested; the tick can check it to exit long loops early.
|
|
287
|
+
#
|
|
288
|
+
# @return [Boolean]
|
|
155
289
|
def stopped? = @stopped
|
|
156
290
|
|
|
291
|
+
# Lifecycle state of the watcher.
|
|
292
|
+
#
|
|
293
|
+
# @return [Symbol] `:idle` (never started), `:running`, `:stopping` (stop requested, thread still alive)
|
|
294
|
+
# or `:stopped`
|
|
157
295
|
def status
|
|
158
296
|
return :idle if @thread.nil?
|
|
159
297
|
return :running if running? && !stopped?
|
|
@@ -162,19 +300,40 @@ module BlockGiven
|
|
|
162
300
|
:stopped
|
|
163
301
|
end
|
|
164
302
|
|
|
303
|
+
# Waits for the watcher thread to end.
|
|
304
|
+
#
|
|
305
|
+
# @param timeout [Numeric, nil] seconds to wait; nil waits until the thread ends
|
|
306
|
+
# @return [self] whether or not the thread ended within `timeout`
|
|
165
307
|
def join(timeout = nil)
|
|
166
308
|
@thread&.join(timeout)
|
|
167
309
|
self
|
|
168
310
|
end
|
|
169
311
|
|
|
170
|
-
#
|
|
312
|
+
# Replaces the error handler called when a tick raises.
|
|
313
|
+
#
|
|
314
|
+
# Without a handler, errors are logged as warnings and polling continues; with one, the handler decides
|
|
315
|
+
# (it may call {#stop}). Either way the error is recorded in {#last_error} first.
|
|
316
|
+
#
|
|
317
|
+
# @yield [error, watcher] for each exception raised by the tick, from the watcher thread
|
|
318
|
+
# @yieldparam error [StandardError] the exception
|
|
319
|
+
# @yieldparam watcher [Watcher] the watcher itself
|
|
320
|
+
# @yieldreturn [void]
|
|
321
|
+
# @return [self]
|
|
171
322
|
def on_error(&handler)
|
|
172
323
|
@on_error = handler
|
|
173
324
|
self
|
|
174
325
|
end
|
|
175
326
|
|
|
327
|
+
# Seconds elapsed since {#start}.
|
|
328
|
+
#
|
|
329
|
+
# @return [Numeric] 0 when the watcher was never started
|
|
176
330
|
def uptime = started_at ? Time.now - started_at : 0
|
|
177
331
|
|
|
332
|
+
# Snapshot of the watcher state, suited for health endpoints and logs.
|
|
333
|
+
#
|
|
334
|
+
# @return [Hash{Symbol => Object}] `:id`, `:name`, `:status`, `:interval`, `:cursor`, `:ticks`,
|
|
335
|
+
# `:started_at`, `:last_tick_at`, `:last_error` (as `"Class: message"` or nil), `:last_error_at` and
|
|
336
|
+
# `:thread` (the thread name, or nil while idle)
|
|
178
337
|
def to_h
|
|
179
338
|
{
|
|
180
339
|
id: id, name: name, status: status, interval: interval, cursor: cursor, ticks: ticks,
|
|
@@ -184,6 +343,9 @@ module BlockGiven
|
|
|
184
343
|
}
|
|
185
344
|
end
|
|
186
345
|
|
|
346
|
+
# One-line description: id, name, status, cursor, tick count and the class of the last error if any.
|
|
347
|
+
#
|
|
348
|
+
# @return [String]
|
|
187
349
|
def inspect
|
|
188
350
|
error = last_error ? " last_error=#{last_error.class}" : ""
|
|
189
351
|
"#<BlockGiven::Watcher #{id} #{name} #{status} cursor=#{cursor.inspect} ticks=#{ticks}#{error}>"
|
data/lib/block_given/receipt.rb
CHANGED
|
@@ -1,41 +1,101 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module BlockGiven
|
|
4
|
-
#
|
|
4
|
+
# A transaction receipt (`eth_getTransactionReceipt`) with snake_case symbol keys and Integer quantities.
|
|
5
|
+
#
|
|
6
|
+
# Hashes are `0x` hex Strings; addresses are `0x` hex Strings as returned by the node (not re-checksummed);
|
|
7
|
+
# every QUANTITY field (block number, gas, status...) is decoded to an Integer by {Normalizer}.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# receipt = tx.wait
|
|
11
|
+
# receipt.status # => :success
|
|
12
|
+
# receipt.block_number # => 12_345_678
|
|
13
|
+
# receipt.fee # => 21_000 * effective_gas_price, in wei
|
|
14
|
+
# receipt[:logs_bloom] # any raw field, by snake_case key
|
|
5
15
|
class Receipt
|
|
16
|
+
# @return [Hash{Symbol => Object}] every receipt field, with snake_case symbol keys and Integer quantities
|
|
6
17
|
attr_reader :to_h
|
|
7
18
|
|
|
19
|
+
# Wraps a raw receipt from the node.
|
|
20
|
+
#
|
|
21
|
+
# @param raw [Hash, Receipt] the JSON-RPC receipt object (String or Symbol keys, hex quantities), or another
|
|
22
|
+
# {Receipt} whose fields are reused as is
|
|
8
23
|
def initialize(raw)
|
|
9
24
|
@to_h = raw.is_a?(Receipt) ? raw.to_h : Normalizer.normalize(raw)
|
|
10
25
|
end
|
|
11
26
|
|
|
27
|
+
# @return [String] the transaction hash as `0x` hex
|
|
12
28
|
def transaction_hash = to_h[:transaction_hash]
|
|
29
|
+
|
|
30
|
+
# @return [Integer, nil] the number of the block the transaction was included in
|
|
13
31
|
def block_number = to_h[:block_number]
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] the hash of the including block as `0x` hex
|
|
14
34
|
def block_hash = to_h[:block_hash]
|
|
35
|
+
|
|
36
|
+
# @return [String] the sender address as `0x` hex, as returned by the node
|
|
15
37
|
def from = to_h[:from]
|
|
38
|
+
|
|
39
|
+
# @return [String, nil] the recipient address as `0x` hex, or nil for a contract creation
|
|
16
40
|
def to = to_h[:to]
|
|
41
|
+
|
|
42
|
+
# @return [String, nil] the address of the created contract as `0x` hex, nil unless the transaction was a
|
|
43
|
+
# contract creation
|
|
17
44
|
def contract_address = to_h[:contract_address]
|
|
45
|
+
|
|
46
|
+
# @return [Integer, nil] gas consumed by this transaction alone
|
|
18
47
|
def gas_used = to_h[:gas_used]
|
|
48
|
+
|
|
49
|
+
# @return [Integer, nil] the gas price actually paid, in wei per gas (base fee + priority fee for EIP-1559)
|
|
19
50
|
def effective_gas_price = to_h[:effective_gas_price]
|
|
51
|
+
|
|
52
|
+
# @return [Integer, nil] gas consumed by the block up to and including this transaction
|
|
20
53
|
def cumulative_gas_used = to_h[:cumulative_gas_used]
|
|
54
|
+
|
|
55
|
+
# @return [Integer, nil] the position of the transaction in its block
|
|
21
56
|
def transaction_index = to_h[:transaction_index]
|
|
57
|
+
|
|
58
|
+
# Raw logs emitted by the transaction, in emission order. Decode them with a contract's `events_from`.
|
|
59
|
+
#
|
|
60
|
+
# @return [Array<Hash{Symbol => Object}>] normalized log objects (`:address`, `:topics`, `:data`,
|
|
61
|
+
# `:block_number`, `:log_index`, `:removed`...); an empty Array when the receipt has none
|
|
22
62
|
def logs = to_h[:logs] || []
|
|
23
63
|
|
|
24
|
-
#
|
|
64
|
+
# Outcome of the transaction.
|
|
65
|
+
#
|
|
66
|
+
# `:success` when the receipt status is 1, `:reverted` otherwise. Pre-Byzantium receipts have no status
|
|
67
|
+
# field: they are treated as `:success`.
|
|
68
|
+
#
|
|
69
|
+
# @return [Symbol] `:success` or `:reverted`
|
|
70
|
+
# @example
|
|
71
|
+
# receipt.status # => :reverted
|
|
72
|
+
# receipt.reverted? # => true
|
|
25
73
|
def status
|
|
26
74
|
return :success if to_h[:status].nil?
|
|
27
75
|
|
|
28
76
|
to_h[:status] == 1 ? :success : :reverted
|
|
29
77
|
end
|
|
30
78
|
|
|
79
|
+
# @return [Boolean] true when {#status} is `:success`
|
|
31
80
|
def success? = status == :success
|
|
81
|
+
|
|
82
|
+
# @return [Boolean] true when {#status} is `:reverted`
|
|
32
83
|
def reverted? = status == :reverted
|
|
33
84
|
|
|
34
|
-
# Total fee paid
|
|
85
|
+
# Total fee paid for the transaction: `gas_used * effective_gas_price`.
|
|
86
|
+
#
|
|
87
|
+
# @return [Integer, nil] the fee in wei, or nil when the node did not return both fields
|
|
35
88
|
def fee = gas_used && effective_gas_price ? gas_used * effective_gas_price : nil
|
|
36
89
|
|
|
90
|
+
# Reads any receipt field by name, including chain-specific ones (e.g. `:l1_fee` on OP-stack chains).
|
|
91
|
+
#
|
|
92
|
+
# @param key [Symbol, String] the snake_case field name (Strings are symbolized as is)
|
|
93
|
+
# @return [Object, nil] the field value, or nil when absent
|
|
37
94
|
def [](key) = to_h[key.to_sym]
|
|
38
95
|
|
|
96
|
+
# Debug representation with the hash, status, block, gas used and log count.
|
|
97
|
+
#
|
|
98
|
+
# @return [String] e.g. `#<BlockGiven::Receipt 0x... status=success block=123 gas_used=21000 logs=0>`
|
|
39
99
|
def inspect
|
|
40
100
|
"#<BlockGiven::Receipt #{transaction_hash} status=#{status} block=#{block_number} " \
|
|
41
101
|
"gas_used=#{gas_used} logs=#{logs.size}>"
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
# Recursive Length Prefix serialization, the encoding of Ethereum transactions.
|
|
5
|
+
#
|
|
6
|
+
# Items are binary Strings, non-negative Integers (encoded big-endian without leading zeros, 0 being the
|
|
7
|
+
# empty string) and Arrays of items. Decoding returns binary Strings and Arrays only: the caller knows which
|
|
8
|
+
# fields are integers ({.to_int}).
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module Rlp
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Serializes an item.
|
|
15
|
+
#
|
|
16
|
+
# @param item [String, Integer, Array] a binary String, a non-negative Integer or a (nested) Array of them
|
|
17
|
+
# @return [String] the RLP bytes, binary
|
|
18
|
+
# @raise [BlockGiven::InvalidArgumentError] for a negative Integer or an unsupported type
|
|
19
|
+
def encode(item)
|
|
20
|
+
case item
|
|
21
|
+
when Array
|
|
22
|
+
payload = item.map { |element| encode(element) }.join.b
|
|
23
|
+
length_prefix(payload.bytesize, 0xc0) + payload
|
|
24
|
+
when Integer
|
|
25
|
+
raise InvalidArgumentError, "RLP cannot encode negative integer #{item}" if item.negative?
|
|
26
|
+
|
|
27
|
+
encode(int_to_bytes(item))
|
|
28
|
+
when String
|
|
29
|
+
bytes = item.b
|
|
30
|
+
return bytes if bytes.bytesize == 1 && bytes.getbyte(0) < 0x80
|
|
31
|
+
|
|
32
|
+
length_prefix(bytes.bytesize, 0x80) + bytes
|
|
33
|
+
else raise InvalidArgumentError, "RLP cannot encode #{item.class}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Deserializes exactly one item spanning the whole input.
|
|
38
|
+
#
|
|
39
|
+
# @param bytes [String] RLP bytes, binary
|
|
40
|
+
# @return [String, Array] binary Strings and nested Arrays
|
|
41
|
+
# @raise [BlockGiven::InvalidArgumentError] when the input is truncated, has trailing bytes or is not in
|
|
42
|
+
# canonical form
|
|
43
|
+
def decode(bytes)
|
|
44
|
+
bytes = bytes.b
|
|
45
|
+
item, consumed = decode_at(bytes, 0)
|
|
46
|
+
raise InvalidArgumentError, "RLP: #{bytes.bytesize - consumed} trailing byte(s)" if consumed != bytes.bytesize
|
|
47
|
+
|
|
48
|
+
item
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Reads a decoded String field as an unsigned big-endian Integer.
|
|
52
|
+
#
|
|
53
|
+
# @param bytes [String] binary String
|
|
54
|
+
# @return [Integer] 0 for the empty String
|
|
55
|
+
# @raise [BlockGiven::InvalidArgumentError] when the value is a list or has leading zero bytes
|
|
56
|
+
def to_int(bytes)
|
|
57
|
+
raise InvalidArgumentError, "RLP: expected an integer, got a list" unless bytes.is_a?(String)
|
|
58
|
+
raise InvalidArgumentError, "RLP: integer with leading zero" if bytes.start_with?("\x00".b)
|
|
59
|
+
|
|
60
|
+
bytes.empty? ? 0 : bytes.unpack1("H*").to_i(16)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Big-endian bytes of a non-negative Integer, without leading zeros.
|
|
64
|
+
#
|
|
65
|
+
# @param int [Integer]
|
|
66
|
+
# @return [String] binary, empty for 0
|
|
67
|
+
def int_to_bytes(int)
|
|
68
|
+
return "".b if int.zero?
|
|
69
|
+
|
|
70
|
+
hex = int.to_s(16)
|
|
71
|
+
[hex.length.odd? ? "0#{hex}" : hex].pack("H*")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Prefix announcing a payload length.
|
|
75
|
+
#
|
|
76
|
+
# @param length [Integer] payload size in bytes
|
|
77
|
+
# @param offset [Integer] 0x80 for a String, 0xc0 for a list
|
|
78
|
+
# @return [String] binary prefix
|
|
79
|
+
def length_prefix(length, offset)
|
|
80
|
+
return (offset + length).chr.b if length < 56
|
|
81
|
+
|
|
82
|
+
length_bytes = int_to_bytes(length)
|
|
83
|
+
(offset + 55 + length_bytes.bytesize).chr.b + length_bytes
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Decodes the item starting at a position.
|
|
87
|
+
#
|
|
88
|
+
# @param bytes [String] the whole input, binary
|
|
89
|
+
# @param position [Integer] where the item starts
|
|
90
|
+
# @return [Array(Object, Integer)] the item and the position right after it
|
|
91
|
+
# @raise [BlockGiven::InvalidArgumentError] for truncated or non-canonical input
|
|
92
|
+
def decode_at(bytes, position)
|
|
93
|
+
prefix = bytes.getbyte(position)
|
|
94
|
+
raise InvalidArgumentError, "RLP: unexpected end of input" if prefix.nil?
|
|
95
|
+
return [bytes.byteslice(position, 1), position + 1] if prefix < 0x80
|
|
96
|
+
|
|
97
|
+
list = prefix >= 0xc0
|
|
98
|
+
start, length = payload_bounds(bytes, position, prefix - (list ? 0xc0 : 0x80))
|
|
99
|
+
raise InvalidArgumentError, "RLP: unexpected end of input" if start + length > bytes.bytesize
|
|
100
|
+
return [decode_list(bytes, start, start + length), start + length] if list
|
|
101
|
+
|
|
102
|
+
payload = bytes.byteslice(start, length)
|
|
103
|
+
raise InvalidArgumentError, "RLP: non-canonical single byte" if length == 1 && payload.getbyte(0) < 0x80
|
|
104
|
+
|
|
105
|
+
[payload, start + length]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Start and length of the payload following a prefix.
|
|
109
|
+
#
|
|
110
|
+
# @param bytes [String] the whole input, binary
|
|
111
|
+
# @param position [Integer] position of the prefix byte
|
|
112
|
+
# @param short [Integer] prefix minus its type offset (0x80 or 0xc0)
|
|
113
|
+
# @return [Array(Integer, Integer)] payload start and length
|
|
114
|
+
# @raise [BlockGiven::InvalidArgumentError] for a truncated or non-canonical length
|
|
115
|
+
def payload_bounds(bytes, position, short)
|
|
116
|
+
return [position + 1, short] if short < 56
|
|
117
|
+
|
|
118
|
+
size = short - 55
|
|
119
|
+
length_bytes = bytes.byteslice(position + 1, size).to_s
|
|
120
|
+
raise InvalidArgumentError, "RLP: unexpected end of input" if length_bytes.bytesize != size
|
|
121
|
+
|
|
122
|
+
length = to_int(length_bytes)
|
|
123
|
+
raise InvalidArgumentError, "RLP: non-canonical length" if length < 56
|
|
124
|
+
|
|
125
|
+
[position + 1 + size, length]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Decodes the items of a list payload.
|
|
129
|
+
#
|
|
130
|
+
# @param bytes [String] the whole input, binary
|
|
131
|
+
# @param position [Integer] start of the list payload
|
|
132
|
+
# @param stop [Integer] end of the list payload
|
|
133
|
+
# @return [Array] the decoded items
|
|
134
|
+
# @raise [BlockGiven::InvalidArgumentError] when an item overflows the list
|
|
135
|
+
def decode_list(bytes, position, stop)
|
|
136
|
+
items = []
|
|
137
|
+
while position < stop
|
|
138
|
+
item, position = decode_at(bytes, position)
|
|
139
|
+
items << item
|
|
140
|
+
end
|
|
141
|
+
raise InvalidArgumentError, "RLP: list item overflows its list" if position != stop
|
|
142
|
+
|
|
143
|
+
items
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|