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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -1
  3. data/README.md +92 -55
  4. data/lib/block_given/abi/codec/decoder.rb +111 -0
  5. data/lib/block_given/abi/codec.rb +170 -0
  6. data/lib/block_given/abi/coder.rb +122 -11
  7. data/lib/block_given/abi/custom_error.rb +33 -1
  8. data/lib/block_given/abi/event.rb +85 -3
  9. data/lib/block_given/abi/function.rb +94 -2
  10. data/lib/block_given/abi/interface.rb +99 -4
  11. data/lib/block_given/abi/parameter.rb +60 -3
  12. data/lib/block_given/abi/standards/erc1155.rb +41 -0
  13. data/lib/block_given/abi/standards/erc20.rb +33 -0
  14. data/lib/block_given/abi/standards/erc4626.rb +41 -0
  15. data/lib/block_given/abi/standards/erc721.rb +47 -0
  16. data/lib/block_given/abi/standards.rb +124 -0
  17. data/lib/block_given/abi/type.rb +138 -0
  18. data/lib/block_given/chain.rb +114 -2
  19. data/lib/block_given/client.rb +322 -20
  20. data/lib/block_given/configuration.rb +85 -3
  21. data/lib/block_given/connectors/alchemy.rb +38 -4
  22. data/lib/block_given/connectors/base.rb +32 -5
  23. data/lib/block_given/connectors/http.rb +91 -5
  24. data/lib/block_given/connectors/stub.rb +70 -4
  25. data/lib/block_given/contract.rb +422 -26
  26. data/lib/block_given/crypto/keccak.rb +152 -0
  27. data/lib/block_given/crypto/secp256k1.rb +168 -0
  28. data/lib/block_given/crypto.rb +22 -0
  29. data/lib/block_given/eip712.rb +199 -0
  30. data/lib/block_given/errors.rb +134 -14
  31. data/lib/block_given/event.rb +51 -1
  32. data/lib/block_given/normalizer.rb +27 -2
  33. data/lib/block_given/poller.rb +177 -15
  34. data/lib/block_given/receipt.rb +63 -3
  35. data/lib/block_given/rlp.rb +146 -0
  36. data/lib/block_given/signed_transaction.rb +148 -47
  37. data/lib/block_given/transaction.rb +103 -12
  38. data/lib/block_given/transaction_envelope/fields.rb +104 -0
  39. data/lib/block_given/transaction_envelope.rb +183 -0
  40. data/lib/block_given/utils.rb +143 -9
  41. data/lib/block_given/version.rb +2 -1
  42. data/lib/block_given/wallet.rb +216 -35
  43. data/lib/block_given.rb +67 -4
  44. metadata +19 -23
@@ -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
- # receipt = BlockGiven::Poller.poll(interval: 2, timeout: 120) { client.get_transaction_receipt(hash) }
8
+ # Used by {Client#wait_for_transaction_receipt}; background (non-blocking) polling is {Watcher}'s job.
9
9
  #
10
- # The block is called until it returns a non-nil / non-false value, which is
11
- # returned. Raises BlockGiven::TimeoutError when the timeout elapses.
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. Returned by the
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
- # Every running watcher is registered under a unique id, so they can be listed
38
- # and stopped even when the object reference was lost:
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 # => [#<BlockGiven::Watcher blocks ...>]
42
- # BlockGiven::Watcher.find("blocks").stop
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
- # Graceful stop by id. Returns the watcher, or nil if unknown.
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 (block number for log watchers), set by the tick.
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
- # Graceful: the current tick finishes, then the thread exits.
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
- # Forceful: kills the thread even in the middle of a tick (use when stop does not return).
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
- # Replace the error handler. Without a handler errors are logged and polling continues.
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}>"
@@ -1,41 +1,101 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BlockGiven
4
- # Transaction receipt with symbolized, integer-decoded fields.
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
- # :success / :reverted (pre-Byzantium receipts have no status: treated as success).
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 in wei.
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