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
@@ -1,24 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BlockGiven
4
- # Base class for every error raised by BlockGiven.
4
+ # Base class for every error raised by BlockGiven; rescue it to catch anything coming out of the gem.
5
5
  class Error < StandardError; end
6
6
 
7
+ # Raised when the global configuration is missing or invalid: no connector, no chain, no RPC URL, a chain
8
+ # unknown to {Chains.resolve}, or a chain Alchemy does not serve.
7
9
  class ConfigurationError < Error; end
10
+
11
+ # Raised when a method receives a value it cannot use: malformed hex or decimal, unknown block tag,
12
+ # unexpected keyword on a contract method, duplicate or unknown watcher id.
8
13
  class InvalidArgumentError < Error; end
14
+
15
+ # Raised when a value is not a 20-byte `0x` hex address (see {Utils.checksum_address}).
9
16
  class InvalidAddressError < InvalidArgumentError; end
17
+
18
+ # Raised when a contract write (or its preparation) is attempted on a contract without a wallet.
10
19
  class WalletRequiredError < Error; end
20
+
21
+ # Raised when a blocking poll, such as waiting for a transaction receipt, exceeds its timeout.
11
22
  class TimeoutError < Error; end
12
23
 
24
+ # Base class for ABI problems: missing or unreadable ABI file, unknown function or event.
13
25
  class AbiError < Error; end
26
+
27
+ # Raised when a value cannot be ABI-encoded for its type: out-of-bounds integer, `bytesN` value longer than
28
+ # N bytes, wrong array length, malformed address.
29
+ class AbiEncodingError < AbiError; end
30
+
31
+ # Raised when data cannot be ABI-decoded for the expected types: too short, or an offset or length pointing
32
+ # outside of it.
33
+ class AbiDecodingError < AbiError; end
34
+
35
+ # Raised when an ABI has no function matching the requested name, signature or argument list.
14
36
  class FunctionNotFoundError < AbiError; end
37
+
38
+ # Raised when an ABI has no event with the requested name.
15
39
  class EventNotFoundError < AbiError; end
40
+
41
+ # Raised when a function name is overloaded and several overloads accept the given arguments; call it with the
42
+ # full signature instead.
16
43
  class AmbiguousFunctionError < AbiError; end
17
44
 
18
- # Transport-level failure (non-2xx HTTP status, connection refused, ...).
45
+ # Transport-level failure: non-2xx HTTP status, connection refused, invalid JSON body or exhausted retries.
46
+ #
47
+ # Endpoints in the message are redacted so API keys never leak into logs.
48
+ #
49
+ # @!attribute [r] status
50
+ # @return [Integer, nil] HTTP status code when the server answered, nil for connection failures
51
+ # @!attribute [r] body
52
+ # @return [String, nil] raw response body when one was received (truncated in the message, complete here)
19
53
  class HttpError < Error
20
54
  attr_reader :status, :body
21
55
 
56
+ # @param message [String] human readable description
57
+ # @param status [Integer, nil] HTTP status code
58
+ # @param body [String, nil] raw response body
59
+ # @return [HttpError]
22
60
  def initialize(message, status: nil, body: nil)
23
61
  super(message)
24
62
  @status = status
@@ -26,10 +64,25 @@ module BlockGiven
26
64
  end
27
65
  end
28
66
 
29
- # JSON-RPC error object returned by the node.
67
+ # JSON-RPC error object (`{ code, message, data }`) returned by the node for a request that reached it.
68
+ #
69
+ # Reverts are surfaced as the {ContractRevertError} subclass by {.from_payload}, which connectors use to turn
70
+ # payloads into exceptions.
71
+ #
72
+ # @!attribute [r] code
73
+ # @return [Integer, nil] JSON-RPC error code (`-32000` for generic node errors, `3` for execution reverted)
74
+ # @!attribute [r] data
75
+ # @return [String, Hash, nil] raw `error.data` field, untouched, as the provider returned it
76
+ # @!attribute [r] rpc_method
77
+ # @return [String, nil] JSON-RPC method whose call failed (`"eth_call"`, `"eth_sendRawTransaction"`)
30
78
  class RpcError < Error
31
79
  attr_reader :code, :data, :rpc_method
32
80
 
81
+ # @param message [String] `error.message` from the payload
82
+ # @param code [Integer, nil] `error.code`
83
+ # @param data [String, Hash, nil] `error.data`
84
+ # @param rpc_method [String, nil] JSON-RPC method that was called
85
+ # @return [RpcError]
33
86
  def initialize(message, code: nil, data: nil, rpc_method: nil)
34
87
  super(message)
35
88
  @code = code
@@ -37,8 +90,18 @@ module BlockGiven
37
90
  @rpc_method = rpc_method
38
91
  end
39
92
 
40
- # Builds the most specific error for a JSON-RPC error payload. Reverts carry
41
- # ABI-encoded data that we surface as a ContractRevertError.
93
+ # Build the most specific error for a JSON-RPC error payload.
94
+ #
95
+ # Payloads carrying `0x` revert bytes in `data` (or nested under `data.data`), or whose message mentions
96
+ # "revert", become a {ContractRevertError} with the revert bytes attached; anything else becomes a plain
97
+ # {RpcError}. A payload that is not a Hash is wrapped with its string form as message.
98
+ #
99
+ # @param error [Hash, Object] JSON-RPC `error` object with String keys `"code"`, `"message"` and `"data"`
100
+ # @param rpc_method [String, nil] method that was called, kept on the error for context
101
+ # @return [BlockGiven::RpcError, BlockGiven::ContractRevertError]
102
+ # @example
103
+ # BlockGiven::RpcError.from_payload({ "code" => 3, "message" => "execution reverted", "data" => "0x08c3..." })
104
+ # # => #<BlockGiven::ContractRevertError: execution reverted: insufficient balance>
42
105
  def self.from_payload(error, rpc_method: nil)
43
106
  error = { "message" => error.to_s } unless error.is_a?(Hash)
44
107
  code = error["code"]
@@ -53,8 +116,14 @@ module BlockGiven
53
116
  end
54
117
  end
55
118
 
56
- # Providers disagree on where the revert bytes live: Alchemy/geth put them in
57
- # `data`, Hardhat/Anvil nest them under `data.data`.
119
+ # Extract the `0x` revert bytes from an error `data` field.
120
+ #
121
+ # Providers disagree on where the revert bytes live: Alchemy/geth put them in `data`, Hardhat/Anvil nest
122
+ # them under `data.data`.
123
+ #
124
+ # @api private
125
+ # @param data [String, Hash, nil] raw `error.data`
126
+ # @return [String, nil] `0x` hex string, or nil when no revert bytes are present
58
127
  def self.extract_revert_data(data)
59
128
  case data
60
129
  when String then data.start_with?("0x") ? data : nil
@@ -63,10 +132,27 @@ module BlockGiven
63
132
  end
64
133
  end
65
134
 
66
- # eth_call / eth_estimateGas / eth_sendRawTransaction rejected by the EVM.
135
+ # `eth_call`, `eth_estimateGas` or `eth_sendRawTransaction` rejected by the EVM with a revert.
136
+ #
137
+ # The message is rewritten to `"execution reverted: <reason>"` when the revert data is a Solidity
138
+ # `Error(string)` or `Panic(uint256)`. Custom errors (`error InsufficientBalance(uint256)`) are decoded on
139
+ # demand with {#decode_with} against a contract ABI; {Contract} and {SignedTransaction} do this automatically
140
+ # before re-raising.
141
+ #
142
+ # @!attribute [r] revert_data
143
+ # @return [String, nil] ABI-encoded revert bytes as a `0x` hex string (4-byte selector followed by the
144
+ # encoded arguments), nil when the node returned none
145
+ # @!attribute [r] error_name
146
+ # @return [String, nil] name of the custom error, set once decoded with {#decode_with}
147
+ # @!attribute [r] args
148
+ # @return [Hash{Symbol => Object}, nil] custom error arguments keyed by snake_case name, set once decoded
149
+ # with {#decode_with}
67
150
  class ContractRevertError < RpcError
151
+ # 4-byte selector of Solidity's built-in `Error(string)`, the revert with a reason string.
68
152
  ERROR_STRING_SELECTOR = "0x08c379a0"
153
+ # 4-byte selector of Solidity's built-in `Panic(uint256)` (assertion failures, overflows, ...).
69
154
  PANIC_SELECTOR = "0x4e487b71"
155
+ # Human readable description of each Solidity panic code carried by `Panic(uint256)`.
70
156
  PANIC_REASONS = {
71
157
  0x00 => "generic compiler inserted panic",
72
158
  0x01 => "assertion failed",
@@ -82,6 +168,15 @@ module BlockGiven
82
168
 
83
169
  attr_reader :revert_data, :error_name, :args
84
170
 
171
+ # @param message [String] message from the RPC error payload; replaced by the decoded built-in reason when
172
+ # one is found and the original does not already contain it, or by `"execution reverted"` when empty
173
+ # @param code [Integer, nil] JSON-RPC error code
174
+ # @param data [String, Hash, nil] raw `error.data`
175
+ # @param rpc_method [String, nil] JSON-RPC method that was called
176
+ # @param revert_data [String, nil] `0x` revert bytes
177
+ # @param error_name [String, nil] decoded custom error name (set by {#decode_with})
178
+ # @param args [Hash{Symbol => Object}, nil] decoded custom error arguments (set by {#decode_with})
179
+ # @return [ContractRevertError]
85
180
  def initialize(message, code: nil, data: nil, rpc_method: nil, revert_data: nil, error_name: nil, args: nil)
86
181
  @revert_data = revert_data
87
182
  @error_name = error_name
@@ -89,23 +184,43 @@ module BlockGiven
89
184
  super(build_message(message), code: code, data: data, rpc_method: rpc_method)
90
185
  end
91
186
 
92
- # Human readable revert reason when it can be decoded (Error(string) / Panic).
187
+ # Human readable revert reason when the revert data is a built-in `Error(string)` or `Panic(uint256)`.
188
+ #
189
+ # Memoised. Custom errors and undecodable data yield nil; use {#decode_with} for custom errors.
190
+ #
191
+ # @return [String, nil] the `Error(string)` text, or `"Panic(0x11): arithmetic overflow or underflow"`
93
192
  def reason
94
193
  return @reason if defined?(@reason)
95
194
 
96
195
  @reason = decode_builtin_reason
97
196
  end
98
197
 
198
+ # 4-byte selector at the start of the revert data.
199
+ #
200
+ # @return [String, nil] `0x` followed by 8 hex chars, nil without revert data
99
201
  def selector
100
202
  revert_data && revert_data.length >= 10 ? revert_data[0, 10] : nil
101
203
  end
102
204
 
205
+ # Whether the revert data is a custom error, i.e. its selector is neither `Error(string)` nor `Panic`.
206
+ #
207
+ # @return [Boolean]
103
208
  def custom_error?
104
209
  !selector.nil? && ![ERROR_STRING_SELECTOR, PANIC_SELECTOR].include?(selector)
105
210
  end
106
211
 
107
- # Returns a copy of this error enriched with a custom error decoded from the
108
- # given ABI interface, or self when the interface does not know the selector.
212
+ # Return a copy of this error enriched with a custom error decoded from an ABI interface.
213
+ #
214
+ # @param interface [BlockGiven::Abi::Interface, nil] interface whose custom errors are matched by selector
215
+ # @return [BlockGiven::ContractRevertError] a new error whose message is `Name(arg1, arg2)` with
216
+ # {#error_name} and {#args} set; `self` when the data is not a custom error, no interface is given or the
217
+ # interface does not know the selector
218
+ # @example
219
+ # begin
220
+ # token.transfer(to, amount)
221
+ # rescue BlockGiven::ContractRevertError => e
222
+ # e.decode_with(token.interface).error_name # => "ERC20InsufficientBalance"
223
+ # end
109
224
  def decode_with(interface)
110
225
  return self unless custom_error? && interface
111
226
 
@@ -135,9 +250,9 @@ module BlockGiven
135
250
  payload = "0x#{revert_data[10..]}"
136
251
  case selector
137
252
  when ERROR_STRING_SELECTOR
138
- Eth::Abi.decode(["string"], payload).first
253
+ Abi::Codec.decode(["string"], Utils.hex_to_bin(payload)).first
139
254
  when PANIC_SELECTOR
140
- panic_code = Eth::Abi.decode(["uint256"], payload).first
255
+ panic_code = Abi::Codec.decode(["uint256"], Utils.hex_to_bin(payload)).first
141
256
  "Panic(0x#{panic_code.to_s(16).rjust(2, '0')}): #{PANIC_REASONS.fetch(panic_code, 'unknown panic')}"
142
257
  end
143
258
  rescue StandardError
@@ -145,10 +260,15 @@ module BlockGiven
145
260
  end
146
261
  end
147
262
 
148
- # Raised by Transaction#wait! when the mined receipt has a failed status.
263
+ # Raised by {Transaction#wait!} when the mined receipt has a failed status (`status == 0`).
264
+ #
265
+ # @!attribute [r] receipt
266
+ # @return [BlockGiven::Receipt] the mined receipt carrying the failed status
149
267
  class TransactionRevertedError < Error
150
268
  attr_reader :receipt
151
269
 
270
+ # @param receipt [BlockGiven::Receipt] receipt whose `transaction_hash` and `block_number` build the message
271
+ # @return [TransactionRevertedError]
152
272
  def initialize(receipt)
153
273
  @receipt = receipt
154
274
  super("transaction #{receipt.transaction_hash} reverted in block #{receipt.block_number}")
@@ -1,15 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BlockGiven
4
- # A decoded event log.
4
+ # A log decoded against an ABI event definition (see `Abi::Event#decode`).
5
5
  #
6
+ # {#args} holds the decoded parameters keyed by their snake_case name, in ABI declaration order (indexed and
7
+ # non-indexed inputs interleaved as declared). Indexed parameters of dynamic type (`string`, `bytes`,
8
+ # arrays, tuples) cannot be recovered from a log: their value is the keccak-256 topic hash as `0x` hex.
9
+ # {#log} keeps the raw normalized log the event was decoded from.
10
+ #
11
+ # @example
6
12
  # event.name # => "Transfer"
7
13
  # event.args # => { from: "0x...", to: "0x...", value: 1000000 }
8
14
  # event[:value] # => 1000000
15
+ # event["value"] # => 1000000 (String keys are snake_cased and symbolized)
9
16
  # event.block_number # => 12345
10
17
  class Event
18
+ # @!attribute [r] name
19
+ # @return [String] the event name as declared in the ABI, e.g. `"Transfer"`
20
+ # @!attribute [r] signature
21
+ # @return [String] the canonical signature, e.g. `"Transfer(address,address,uint256)"`
22
+ # @!attribute [r] args
23
+ # @return [Hash{Symbol => Object}] decoded parameters keyed by snake_case name, in ABI declaration order;
24
+ # addresses checksummed, integers as Integer, static bytes as `0x` hex, indexed dynamic types as their
25
+ # topic hash
26
+ # @!attribute [r] log
27
+ # @return [Hash{Symbol => Object}] the raw normalized log (`:address`, `:topics`, `:data`, `:block_number`,
28
+ # `:transaction_hash`, `:log_index`, `:removed`...)
11
29
  attr_reader :name, :signature, :args, :log
12
30
 
31
+ # Builds a decoded event. Instances are normally created by `Abi::Event#decode` rather than directly.
32
+ #
33
+ # @param name [String] the event name
34
+ # @param signature [String] the canonical event signature
35
+ # @param args [Hash{Symbol => Object}] the decoded parameters, keyed by snake_case Symbol
36
+ # @param log [Hash{Symbol => Object}] the normalized log the event was decoded from
13
37
  def initialize(name:, signature:, args:, log:)
14
38
  @name = name
15
39
  @signature = signature
@@ -17,20 +41,46 @@ module BlockGiven
17
41
  @log = log
18
42
  end
19
43
 
44
+ # Reads a decoded parameter by name.
45
+ #
46
+ # @param key [Symbol, String] the parameter name, in camelCase or snake_case (`:tokenId`, `"token_id"`...)
47
+ # @return [Object, nil] the decoded value, or nil when the event has no such parameter
20
48
  def [](key) = args[Utils.snake_case(key).to_sym]
49
+
50
+ # @return [String, nil] the EIP-55 checksummed address of the contract that emitted the log, or nil when
51
+ # the log carries no address
21
52
  def address = log[:address] && Utils.checksum_address(log[:address])
53
+
54
+ # @return [Integer, nil] the number of the block containing the log (nil for a pending log)
22
55
  def block_number = log[:block_number]
56
+
57
+ # @return [String, nil] the hash of the containing block as `0x` hex
23
58
  def block_hash = log[:block_hash]
59
+
60
+ # @return [String, nil] the hash of the emitting transaction as `0x` hex
24
61
  def transaction_hash = log[:transaction_hash]
62
+
63
+ # @return [Integer, nil] the position of the emitting transaction in its block
25
64
  def transaction_index = log[:transaction_index]
65
+
66
+ # @return [Integer, nil] the position of the log in its block
26
67
  def log_index = log[:log_index]
68
+
69
+ # @return [Boolean] true when the node flagged the log as removed by a chain reorganisation
27
70
  def removed? = !!log[:removed]
28
71
 
72
+ # A compact Hash view, handy for persistence or logging.
73
+ #
74
+ # @return [Hash{Symbol => Object}] `:name`, `:args`, `:address` (checksummed), `:block_number`,
75
+ # `:transaction_hash` and `:log_index`
29
76
  def to_h
30
77
  { name: name, args: args, address: address, block_number: block_number,
31
78
  transaction_hash: transaction_hash, log_index: log_index }
32
79
  end
33
80
 
81
+ # Debug representation with the name, decoded args and block number.
82
+ #
83
+ # @return [String] e.g. `#<BlockGiven::Event Transfer {:from=>"0x...", ...} block=123>`
34
84
  def inspect = "#<BlockGiven::Event #{name} #{args.inspect} block=#{block_number}>"
35
85
  end
36
86
  end
@@ -1,11 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BlockGiven
4
- # Converts raw JSON-RPC objects (blocks, transactions, receipts, logs) into
5
- # Ruby-friendly hashes: snake_case symbol keys, QUANTITY fields as Integers.
4
+ # Convert raw JSON-RPC objects (blocks, transactions, receipts, logs) into Ruby-friendly hashes.
5
+ #
6
+ # Keys become snake_case symbols (`"blockNumber"` -> `:block_number`) and the fields listed in
7
+ # {QUANTITY_KEYS} are decoded from hex QUANTITY strings to Integers. Nested hashes and arrays are normalised
8
+ # recursively, except a log's `topics`, which stay an Array of `0x` hex strings. Addresses, hashes and `data`
9
+ # fields are left exactly as the node returned them. {Client} runs every block, transaction, receipt and log
10
+ # through this module before handing it back.
11
+ #
12
+ # @example
13
+ # BlockGiven::Normalizer.normalize({ "blockNumber" => "0x10", "logIndex" => "0x0", "topics" => ["0xddf2"] })
14
+ # # => { block_number: 16, log_index: 0, topics: ["0xddf2"] }
6
15
  module Normalizer
7
16
  module_function
8
17
 
18
+ # Snake_case keys whose values are JSON-RPC QUANTITY hex strings, decoded to Integers by {.normalize}.
19
+ #
20
+ # Covers block, transaction and receipt fields, EIP-4844 blob fields, and the L1 fee fields added by
21
+ # OP Stack chains (Base, Optimism).
9
22
  QUANTITY_KEYS = %i[
10
23
  number timestamp gas_used gas_limit base_fee_per_gas block_number transaction_index log_index
11
24
  cumulative_gas_used effective_gas_price status value gas gas_price max_fee_per_gas
@@ -14,6 +27,11 @@ module BlockGiven
14
27
  deposit_nonce deposit_receipt_version
15
28
  ].freeze
16
29
 
30
+ # Normalise a raw RPC value recursively.
31
+ #
32
+ # @param value [Hash, Array, Object] a Hash is rebuilt with snake_case Symbol keys and normalised values,
33
+ # an Array is mapped element by element, anything else is returned untouched
34
+ # @return [Hash{Symbol => Object}, Array, Object]
17
35
  def normalize(value)
18
36
  case value
19
37
  when Hash then value.each_with_object({}) do |(k, v), h|
@@ -24,6 +42,13 @@ module BlockGiven
24
42
  end
25
43
  end
26
44
 
45
+ # Normalise one field of an RPC hash, given its already snake_cased key.
46
+ #
47
+ # @api private
48
+ # @param key [Symbol] snake_case key
49
+ # @param value [Object] raw value
50
+ # @return [Object] an Integer for a {QUANTITY_KEYS} key holding hex (nil when the node returned an empty
51
+ # `"0x"`), the untouched Array for `:topics`, otherwise {.normalize} of the value
27
52
  def normalize_field(key, value)
28
53
  if QUANTITY_KEYS.include?(key) && Utils.hex?(value)
29
54
  value == "0x" ? nil : Utils.hex_to_int(value)