block_given 0.1.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 +7 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +463 -0
- data/lib/block_given/abi/coder.rb +134 -0
- data/lib/block_given/abi/custom_error.rb +29 -0
- data/lib/block_given/abi/event.rb +87 -0
- data/lib/block_given/abi/function.rb +72 -0
- data/lib/block_given/abi/interface.rb +109 -0
- data/lib/block_given/abi/parameter.rb +57 -0
- data/lib/block_given/chain.rb +112 -0
- data/lib/block_given/client.rb +234 -0
- data/lib/block_given/configuration.rb +48 -0
- data/lib/block_given/connectors/alchemy.rb +36 -0
- data/lib/block_given/connectors/base.rb +26 -0
- data/lib/block_given/connectors/http.rb +150 -0
- data/lib/block_given/connectors/stub.rb +66 -0
- data/lib/block_given/contract.rb +287 -0
- data/lib/block_given/errors.rb +157 -0
- data/lib/block_given/event.rb +36 -0
- data/lib/block_given/normalizer.rb +37 -0
- data/lib/block_given/poller.rb +226 -0
- data/lib/block_given/railtie.rb +17 -0
- data/lib/block_given/receipt.rb +44 -0
- data/lib/block_given/signed_transaction.rb +145 -0
- data/lib/block_given/transaction.rb +85 -0
- data/lib/block_given/utils.rb +134 -0
- data/lib/block_given/version.rb +5 -0
- data/lib/block_given/wallet.rb +137 -0
- data/lib/block_given.rb +68 -0
- metadata +130 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
# Base class for typed contracts. Declare the ABI (and optionally a default
|
|
5
|
+
# address / chain) and every ABI function becomes a Ruby method:
|
|
6
|
+
#
|
|
7
|
+
# class Usdc < BlockGiven::Contract
|
|
8
|
+
# abi_file "abis/erc20.json" # your app's ABI file (see BlockGiven.config.abi_path)
|
|
9
|
+
# address "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# usdc = Usdc.new(wallet: BlockGiven::Wallet.new(private_key: "0x..."))
|
|
13
|
+
# usdc.balance_of(wallet.address) # eth_call, decoded
|
|
14
|
+
# tx = usdc.transfer(to: "0x...", value: 1e6) # signed + broadcast -> BlockGiven::Transaction
|
|
15
|
+
# tx.wait! # polls the receipt
|
|
16
|
+
#
|
|
17
|
+
# Transaction / call overrides go in the reserved `tx:` keyword:
|
|
18
|
+
# vault.deposit(amount, tx: { value: BlockGiven::Utils.parse_ether("0.1"), gas: 200_000 })
|
|
19
|
+
# token.balance_of(addr, tx: { block: 18_000_000 })
|
|
20
|
+
class Contract
|
|
21
|
+
TX_OPTIONS = %i[value gas nonce max_fee_per_gas max_priority_fee_per_gas gas_price from block].freeze
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
attr_reader :interface
|
|
25
|
+
|
|
26
|
+
# Sets (or returns) the ABI. Accepts an Array, an artifact Hash, or a JSON String.
|
|
27
|
+
def abi(source = nil)
|
|
28
|
+
return interface&.raw if source.nil?
|
|
29
|
+
|
|
30
|
+
@interface = Abi::Interface.parse(source)
|
|
31
|
+
define_abi_methods!
|
|
32
|
+
@interface
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Loads the ABI from a JSON file. Relative paths are resolved against
|
|
36
|
+
# BlockGiven.config.abi_path when set (ABIs live in your app, not in the gem).
|
|
37
|
+
def abi_file(path)
|
|
38
|
+
base = BlockGiven.config.abi_path
|
|
39
|
+
path = File.join(base.to_s, path.to_s) if base && !File.absolute_path?(path.to_s)
|
|
40
|
+
raise AbiError, "ABI file not found: #{path}" unless File.file?(path)
|
|
41
|
+
|
|
42
|
+
abi(File.read(path))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Default address for instances (can be overridden with .new(address: ...) / .at(...)).
|
|
46
|
+
def address(value = nil)
|
|
47
|
+
return @default_address if value.nil?
|
|
48
|
+
|
|
49
|
+
@default_address = Utils.checksum_address(value)
|
|
50
|
+
end
|
|
51
|
+
alias default_address address
|
|
52
|
+
|
|
53
|
+
def chain(value = nil)
|
|
54
|
+
return @chain if value.nil?
|
|
55
|
+
|
|
56
|
+
@chain = Chains.resolve(value)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def at(address, **options) = new(address: address, **options)
|
|
60
|
+
|
|
61
|
+
def functions = interface&.functions || []
|
|
62
|
+
def events = interface&.events || []
|
|
63
|
+
def errors = interface&.errors || []
|
|
64
|
+
|
|
65
|
+
def inherited(subclass)
|
|
66
|
+
super
|
|
67
|
+
subclass.instance_variable_set(:@interface, @interface)
|
|
68
|
+
subclass.instance_variable_set(:@default_address, @default_address)
|
|
69
|
+
subclass.instance_variable_set(:@chain, @chain)
|
|
70
|
+
subclass.send(:define_abi_methods!) if @interface
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# One Ruby method per ABI function name (snake_case). Names clashing with
|
|
76
|
+
# existing methods (send, class, address...) are skipped: use #read / #write.
|
|
77
|
+
def define_abi_methods!
|
|
78
|
+
interface.function_names.each do |ruby_name|
|
|
79
|
+
if Contract.method_defined?(ruby_name) || Contract.private_method_defined?(ruby_name)
|
|
80
|
+
BlockGiven.config.logger.debug do
|
|
81
|
+
"[block_given] #{name}: skipping ##{ruby_name} (reserved), use read/write"
|
|
82
|
+
end
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
define_method(ruby_name) do |*args, tx: {}, **kwargs|
|
|
87
|
+
function = self.class.interface.function(ruby_name, args: args, kwargs: kwargs)
|
|
88
|
+
if function.read?
|
|
89
|
+
read(function, *args, tx: tx, **kwargs)
|
|
90
|
+
else
|
|
91
|
+
write(function, *args, tx: tx, **kwargs)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
attr_reader :address, :wallet
|
|
99
|
+
|
|
100
|
+
def initialize(address: nil, wallet: nil, client: nil, chain: nil)
|
|
101
|
+
raise AbiError, "#{self.class.name} has no ABI: declare it with `abi [...]` or `abi_file`" unless interface
|
|
102
|
+
|
|
103
|
+
resolved = address || self.class.default_address
|
|
104
|
+
raise InvalidArgumentError, "#{self.class.name}: address is required" if resolved.nil?
|
|
105
|
+
|
|
106
|
+
@address = Utils.checksum_address(resolved)
|
|
107
|
+
@wallet = wallet
|
|
108
|
+
@client = client
|
|
109
|
+
@chain = chain
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def interface = self.class.interface
|
|
113
|
+
|
|
114
|
+
def client
|
|
115
|
+
@client ||= begin
|
|
116
|
+
chain = @chain || self.class.chain
|
|
117
|
+
if wallet && (chain.nil? || wallet.client.chain == Chains.resolve(chain))
|
|
118
|
+
wallet.client
|
|
119
|
+
elsif chain
|
|
120
|
+
Client.new(chain: chain)
|
|
121
|
+
else
|
|
122
|
+
BlockGiven.client
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def chain = client.chain
|
|
128
|
+
def with_wallet(wallet) = self.class.new(address: address, wallet: wallet, client: @client, chain: @chain)
|
|
129
|
+
|
|
130
|
+
# --- Reads / writes -----------------------------------------------------
|
|
131
|
+
|
|
132
|
+
# eth_call + decode. Overrides: tx: { block:, from: }.
|
|
133
|
+
def read(name, *args, tx: {}, **kwargs)
|
|
134
|
+
function = resolve(name, args, kwargs)
|
|
135
|
+
data = function.encode(args, kwargs)
|
|
136
|
+
options = tx_options(tx)
|
|
137
|
+
raw = with_decoded_errors do
|
|
138
|
+
client.call(to: address, data: data, from: options[:from] || wallet&.address, block: options[:block] || :latest)
|
|
139
|
+
end
|
|
140
|
+
function.decode_output(raw)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Signs and broadcasts. Returns a BlockGiven::Transaction. Overrides: tx: { value:, gas:, nonce:, fees... }.
|
|
144
|
+
def write(name, *args, tx: {}, **kwargs)
|
|
145
|
+
prepare_write(name, *args, tx: tx, **kwargs).broadcast
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Signs without broadcasting. Returns a BlockGiven::SignedTransaction whose #hash and
|
|
149
|
+
# #nonce are known before any network call: persist them, then call #broadcast. The signed
|
|
150
|
+
# transaction keeps this contract's ABI, so reverts raised by #broadcast are decoded too.
|
|
151
|
+
def prepare_write(name, *args, tx: {}, **kwargs)
|
|
152
|
+
function = resolve(name, args, kwargs)
|
|
153
|
+
unless wallet
|
|
154
|
+
raise WalletRequiredError,
|
|
155
|
+
"#{self.class.name}##{function.ruby_name} needs a wallet (pass wallet: to .new)"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
options = tx_options(tx)
|
|
159
|
+
value = options[:value] || 0
|
|
160
|
+
if value != 0 && !function.payable?
|
|
161
|
+
raise InvalidArgumentError, "#{function.name} is not payable, cannot send value"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
data = function.encode(args, kwargs)
|
|
165
|
+
with_decoded_errors do
|
|
166
|
+
wallet.signed_transaction(
|
|
167
|
+
to: address, data: data, value: value, gas: options[:gas], nonce: options[:nonce],
|
|
168
|
+
max_fee_per_gas: options[:max_fee_per_gas], max_priority_fee_per_gas: options[:max_priority_fee_per_gas],
|
|
169
|
+
gas_price: options[:gas_price]
|
|
170
|
+
).with_interface(interface)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Dry-runs a write with eth_call from the wallet address and returns the decoded
|
|
175
|
+
# result. Raises BlockGiven::ContractRevertError with the decoded reason on failure.
|
|
176
|
+
def simulate(name, *args, tx: {}, **kwargs)
|
|
177
|
+
function = resolve(name, args, kwargs)
|
|
178
|
+
options = tx_options(tx)
|
|
179
|
+
data = function.encode(args, kwargs)
|
|
180
|
+
raw = with_decoded_errors do
|
|
181
|
+
client.call(to: address, data: data, from: options[:from] || wallet&.address, value: options[:value],
|
|
182
|
+
gas: options[:gas], block: options[:block] || :latest)
|
|
183
|
+
end
|
|
184
|
+
function.decode_output(raw)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def estimate_gas(name, *args, tx: {}, **kwargs)
|
|
188
|
+
function = resolve(name, args, kwargs)
|
|
189
|
+
options = tx_options(tx)
|
|
190
|
+
data = function.encode(args, kwargs)
|
|
191
|
+
with_decoded_errors do
|
|
192
|
+
client.estimate_gas(to: address, data: data, from: options[:from] || wallet&.address, value: options[:value])
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def encode_function_data(name, *args, **kwargs)
|
|
197
|
+
resolve(name, args, kwargs).encode(args, kwargs)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def decode_function_result(name, hex)
|
|
201
|
+
interface.function(name).decode_output(hex)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# --- Events -------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
# Fetches past events. `args` filters on indexed parameters.
|
|
207
|
+
# usdc.get_events(:Transfer, from_block: 18_000_000, to_block: :latest, args: { to: wallet.address })
|
|
208
|
+
# Pass max_block_range: to split a large range into several eth_getLogs calls.
|
|
209
|
+
def get_events(name = nil, from_block:, to_block: :latest, args: {}, max_block_range: nil)
|
|
210
|
+
topics = name ? interface.event(name).encode_topics(args) : nil
|
|
211
|
+
logs = if max_block_range
|
|
212
|
+
client.get_logs_in_chunks(address: address, topics: topics, from_block: from_block,
|
|
213
|
+
to_block: to_block, max_block_range: max_block_range)
|
|
214
|
+
else
|
|
215
|
+
client.get_logs(address: address, topics: topics, from_block: from_block, to_block: to_block)
|
|
216
|
+
end
|
|
217
|
+
decode_logs(logs)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Polls for new events in a background thread. Returns a BlockGiven::Watcher.
|
|
221
|
+
# watcher = usdc.watch_event(:Transfer, args: { to: me }) { |event| puts event.args }
|
|
222
|
+
# watcher.stop
|
|
223
|
+
#
|
|
224
|
+
# Resuming after a restart: pass from_block: (your persisted cursor + 1) and persist the
|
|
225
|
+
# `to` block handed to on_progress after each processed range. confirmations: keeps the
|
|
226
|
+
# watcher N blocks behind the head so reorged logs are never delivered.
|
|
227
|
+
def watch_event(name = nil, args: {}, from_block: nil, polling_interval: nil, max_block_range: nil,
|
|
228
|
+
confirmations: 0, on_progress: nil, id: nil, &block)
|
|
229
|
+
raise ::ArgumentError, "a block is required" unless block
|
|
230
|
+
|
|
231
|
+
event = name && interface.event(name)
|
|
232
|
+
topics = event&.encode_topics(args)
|
|
233
|
+
label = "#{event ? event.name : '*'}@#{self.class.name || 'Contract'}(#{address[0, 10]})"
|
|
234
|
+
client.watch_logs(address: address, topics: topics, from_block: from_block,
|
|
235
|
+
polling_interval: polling_interval, max_block_range: max_block_range,
|
|
236
|
+
confirmations: confirmations, on_progress: on_progress, id: id, name: label) do |logs|
|
|
237
|
+
decode_logs(logs).each { |event| block.call(event) }
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
alias watch_events watch_event
|
|
241
|
+
|
|
242
|
+
# Decodes raw logs with this contract's ABI. Unknown topics are skipped.
|
|
243
|
+
def decode_logs(logs)
|
|
244
|
+
logs.filter_map do |log|
|
|
245
|
+
log = Normalizer.normalize(log) unless log.is_a?(Hash) && log.key?(:topics)
|
|
246
|
+
topic = Array(log[:topics]).first
|
|
247
|
+
event = topic && interface.event_by_topic(topic)
|
|
248
|
+
event&.decode(log)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Events emitted by this contract in a receipt.
|
|
253
|
+
def events_from(receipt)
|
|
254
|
+
logs = receipt.respond_to?(:logs) ? receipt.logs : Array(receipt[:logs])
|
|
255
|
+
decode_logs(logs.select { |l| Utils.same_address?(l[:address], address) })
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def explorer_url = chain.explorer_address_url(address)
|
|
259
|
+
def ==(other) = other.class == self.class && other.address == address
|
|
260
|
+
def inspect = "#<#{self.class.name} #{address}#{" wallet=#{wallet.address}" if wallet}>"
|
|
261
|
+
|
|
262
|
+
private
|
|
263
|
+
|
|
264
|
+
def resolve(name, args, kwargs)
|
|
265
|
+
name.is_a?(Abi::Function) ? name : interface.function(name, args: args, kwargs: kwargs)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def tx_options(tx)
|
|
269
|
+
raise InvalidArgumentError, "tx: must be a Hash" unless tx.is_a?(Hash)
|
|
270
|
+
|
|
271
|
+
options = tx.transform_keys(&:to_sym)
|
|
272
|
+
unknown = options.keys - TX_OPTIONS
|
|
273
|
+
unless unknown.empty?
|
|
274
|
+
raise InvalidArgumentError,
|
|
275
|
+
"unknown tx option(s): #{unknown.join(', ')} (allowed: #{TX_OPTIONS.join(', ')})"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
options
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def with_decoded_errors
|
|
282
|
+
yield
|
|
283
|
+
rescue ContractRevertError => e
|
|
284
|
+
raise e.decode_with(interface)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
# Base class for every error raised by BlockGiven.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
class ConfigurationError < Error; end
|
|
8
|
+
class InvalidArgumentError < Error; end
|
|
9
|
+
class InvalidAddressError < InvalidArgumentError; end
|
|
10
|
+
class WalletRequiredError < Error; end
|
|
11
|
+
class TimeoutError < Error; end
|
|
12
|
+
|
|
13
|
+
class AbiError < Error; end
|
|
14
|
+
class FunctionNotFoundError < AbiError; end
|
|
15
|
+
class EventNotFoundError < AbiError; end
|
|
16
|
+
class AmbiguousFunctionError < AbiError; end
|
|
17
|
+
|
|
18
|
+
# Transport-level failure (non-2xx HTTP status, connection refused, ...).
|
|
19
|
+
class HttpError < Error
|
|
20
|
+
attr_reader :status, :body
|
|
21
|
+
|
|
22
|
+
def initialize(message, status: nil, body: nil)
|
|
23
|
+
super(message)
|
|
24
|
+
@status = status
|
|
25
|
+
@body = body
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# JSON-RPC error object returned by the node.
|
|
30
|
+
class RpcError < Error
|
|
31
|
+
attr_reader :code, :data, :rpc_method
|
|
32
|
+
|
|
33
|
+
def initialize(message, code: nil, data: nil, rpc_method: nil)
|
|
34
|
+
super(message)
|
|
35
|
+
@code = code
|
|
36
|
+
@data = data
|
|
37
|
+
@rpc_method = rpc_method
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Builds the most specific error for a JSON-RPC error payload. Reverts carry
|
|
41
|
+
# ABI-encoded data that we surface as a ContractRevertError.
|
|
42
|
+
def self.from_payload(error, rpc_method: nil)
|
|
43
|
+
error = { "message" => error.to_s } unless error.is_a?(Hash)
|
|
44
|
+
code = error["code"]
|
|
45
|
+
message = error["message"].to_s
|
|
46
|
+
data = error["data"]
|
|
47
|
+
revert_data = extract_revert_data(data)
|
|
48
|
+
|
|
49
|
+
if revert_data || message.match?(/revert/i)
|
|
50
|
+
ContractRevertError.new(message, code: code, data: data, rpc_method: rpc_method, revert_data: revert_data)
|
|
51
|
+
else
|
|
52
|
+
new(message, code: code, data: data, rpc_method: rpc_method)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Providers disagree on where the revert bytes live: Alchemy/geth put them in
|
|
57
|
+
# `data`, Hardhat/Anvil nest them under `data.data`.
|
|
58
|
+
def self.extract_revert_data(data)
|
|
59
|
+
case data
|
|
60
|
+
when String then data.start_with?("0x") ? data : nil
|
|
61
|
+
when Hash then extract_revert_data(data["data"] || data[:data])
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# eth_call / eth_estimateGas / eth_sendRawTransaction rejected by the EVM.
|
|
67
|
+
class ContractRevertError < RpcError
|
|
68
|
+
ERROR_STRING_SELECTOR = "0x08c379a0"
|
|
69
|
+
PANIC_SELECTOR = "0x4e487b71"
|
|
70
|
+
PANIC_REASONS = {
|
|
71
|
+
0x00 => "generic compiler inserted panic",
|
|
72
|
+
0x01 => "assertion failed",
|
|
73
|
+
0x11 => "arithmetic overflow or underflow",
|
|
74
|
+
0x12 => "division or modulo by zero",
|
|
75
|
+
0x21 => "invalid enum conversion",
|
|
76
|
+
0x22 => "incorrectly encoded storage byte array",
|
|
77
|
+
0x31 => "pop() on an empty array",
|
|
78
|
+
0x32 => "array index out of bounds",
|
|
79
|
+
0x41 => "too much memory allocated or array too large",
|
|
80
|
+
0x51 => "call to a zero-initialized variable of internal function type"
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
attr_reader :revert_data, :error_name, :args
|
|
84
|
+
|
|
85
|
+
def initialize(message, code: nil, data: nil, rpc_method: nil, revert_data: nil, error_name: nil, args: nil)
|
|
86
|
+
@revert_data = revert_data
|
|
87
|
+
@error_name = error_name
|
|
88
|
+
@args = args
|
|
89
|
+
super(build_message(message), code: code, data: data, rpc_method: rpc_method)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Human readable revert reason when it can be decoded (Error(string) / Panic).
|
|
93
|
+
def reason
|
|
94
|
+
return @reason if defined?(@reason)
|
|
95
|
+
|
|
96
|
+
@reason = decode_builtin_reason
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def selector
|
|
100
|
+
revert_data && revert_data.length >= 10 ? revert_data[0, 10] : nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def custom_error?
|
|
104
|
+
!selector.nil? && ![ERROR_STRING_SELECTOR, PANIC_SELECTOR].include?(selector)
|
|
105
|
+
end
|
|
106
|
+
|
|
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.
|
|
109
|
+
def decode_with(interface)
|
|
110
|
+
return self unless custom_error? && interface
|
|
111
|
+
|
|
112
|
+
custom = interface.error_by_selector(selector)
|
|
113
|
+
return self unless custom
|
|
114
|
+
|
|
115
|
+
decoded = custom.decode(revert_data)
|
|
116
|
+
self.class.new(
|
|
117
|
+
"#{custom.name}(#{decoded.values.map(&:inspect).join(', ')})",
|
|
118
|
+
code: code, data: data, rpc_method: rpc_method, revert_data: revert_data,
|
|
119
|
+
error_name: custom.name, args: decoded
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def build_message(original)
|
|
126
|
+
reason = decode_builtin_reason
|
|
127
|
+
return "execution reverted: #{reason}" if reason && !original.include?(reason)
|
|
128
|
+
|
|
129
|
+
original.empty? ? "execution reverted" : original
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def decode_builtin_reason
|
|
133
|
+
return nil unless revert_data && revert_data.length > 10
|
|
134
|
+
|
|
135
|
+
payload = "0x#{revert_data[10..]}"
|
|
136
|
+
case selector
|
|
137
|
+
when ERROR_STRING_SELECTOR
|
|
138
|
+
Eth::Abi.decode(["string"], payload).first
|
|
139
|
+
when PANIC_SELECTOR
|
|
140
|
+
panic_code = Eth::Abi.decode(["uint256"], payload).first
|
|
141
|
+
"Panic(0x#{panic_code.to_s(16).rjust(2, '0')}): #{PANIC_REASONS.fetch(panic_code, 'unknown panic')}"
|
|
142
|
+
end
|
|
143
|
+
rescue StandardError
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Raised by Transaction#wait! when the mined receipt has a failed status.
|
|
149
|
+
class TransactionRevertedError < Error
|
|
150
|
+
attr_reader :receipt
|
|
151
|
+
|
|
152
|
+
def initialize(receipt)
|
|
153
|
+
@receipt = receipt
|
|
154
|
+
super("transaction #{receipt.transaction_hash} reverted in block #{receipt.block_number}")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
# A decoded event log.
|
|
5
|
+
#
|
|
6
|
+
# event.name # => "Transfer"
|
|
7
|
+
# event.args # => { from: "0x...", to: "0x...", value: 1000000 }
|
|
8
|
+
# event[:value] # => 1000000
|
|
9
|
+
# event.block_number # => 12345
|
|
10
|
+
class Event
|
|
11
|
+
attr_reader :name, :signature, :args, :log
|
|
12
|
+
|
|
13
|
+
def initialize(name:, signature:, args:, log:)
|
|
14
|
+
@name = name
|
|
15
|
+
@signature = signature
|
|
16
|
+
@args = args
|
|
17
|
+
@log = log
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def [](key) = args[Utils.snake_case(key).to_sym]
|
|
21
|
+
def address = log[:address] && Utils.checksum_address(log[:address])
|
|
22
|
+
def block_number = log[:block_number]
|
|
23
|
+
def block_hash = log[:block_hash]
|
|
24
|
+
def transaction_hash = log[:transaction_hash]
|
|
25
|
+
def transaction_index = log[:transaction_index]
|
|
26
|
+
def log_index = log[:log_index]
|
|
27
|
+
def removed? = !!log[:removed]
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{ name: name, args: args, address: address, block_number: block_number,
|
|
31
|
+
transaction_hash: transaction_hash, log_index: log_index }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def inspect = "#<BlockGiven::Event #{name} #{args.inspect} block=#{block_number}>"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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.
|
|
6
|
+
module Normalizer
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
QUANTITY_KEYS = %i[
|
|
10
|
+
number timestamp gas_used gas_limit base_fee_per_gas block_number transaction_index log_index
|
|
11
|
+
cumulative_gas_used effective_gas_price status value gas gas_price max_fee_per_gas
|
|
12
|
+
max_priority_fee_per_gas chain_id type difficulty total_difficulty size nonce blob_gas_used
|
|
13
|
+
blob_gas_price excess_blob_gas max_fee_per_blob_gas l1_fee l1_gas_used l1_gas_price l1_fee_scalar
|
|
14
|
+
deposit_nonce deposit_receipt_version
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def normalize(value)
|
|
18
|
+
case value
|
|
19
|
+
when Hash then value.each_with_object({}) do |(k, v), h|
|
|
20
|
+
h[key = Utils.snake_case(k).to_sym] = normalize_field(key, v)
|
|
21
|
+
end
|
|
22
|
+
when Array then value.map { |v| normalize(v) }
|
|
23
|
+
else value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def normalize_field(key, value)
|
|
28
|
+
if QUANTITY_KEYS.include?(key) && Utils.hex?(value)
|
|
29
|
+
value == "0x" ? nil : Utils.hex_to_int(value)
|
|
30
|
+
elsif key == :topics && value.is_a?(Array)
|
|
31
|
+
value
|
|
32
|
+
else
|
|
33
|
+
normalize(value)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|