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/client.rb
CHANGED
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module BlockGiven
|
|
4
|
-
# Public JSON-RPC client bound to a chain and a connector (viem's PublicClient).
|
|
4
|
+
# Public JSON-RPC client bound to a chain and a connector (the equivalent of viem's PublicClient).
|
|
5
5
|
#
|
|
6
|
-
#
|
|
6
|
+
# Every method wrapping a JSON-RPC call returns Ruby values rather than raw JSON: QUANTITY fields are
|
|
7
|
+
# decoded to Integer, object keys become snake_case Symbols ({Normalizer}), addresses are checksummed and
|
|
8
|
+
# transactions / receipts are wrapped in {Transaction} / {Receipt}.
|
|
9
|
+
#
|
|
10
|
+
# Arguments named `block:` accept a block number (Integer), a hex QUANTITY string ("0x10"), or one of the
|
|
11
|
+
# block tags `:latest`, `:earliest`, `:pending`, `:safe`, `:finalized` (Symbol or String); `nil` means
|
|
12
|
+
# `latest`. See {Utils.block_tag}.
|
|
13
|
+
#
|
|
14
|
+
# @example Reading from Base through Alchemy
|
|
15
|
+
# connector = BlockGiven::Connectors::Alchemy.new(api_key: ENV["ALCHEMY_API_KEY"])
|
|
7
16
|
# client = BlockGiven::Client.new(chain: :base, connector: connector)
|
|
8
|
-
# client.block_number
|
|
9
|
-
# client.get_balance("0x...")
|
|
17
|
+
# client.block_number # => 12_345_678
|
|
18
|
+
# client.get_balance("0x...") # => 1_000_000_000_000_000_000 (wei)
|
|
10
19
|
class Client
|
|
20
|
+
# @!attribute [r] chain
|
|
21
|
+
# @return [Chain] the chain this client talks to (resolved from the constructor argument or the global config)
|
|
22
|
+
# @!attribute [r] connector
|
|
23
|
+
# @return [Connectors::Base] the JSON-RPC transport used for every request
|
|
24
|
+
|
|
11
25
|
attr_reader :chain, :connector
|
|
12
26
|
|
|
27
|
+
# Builds a client. Every argument falls back to the global configuration ({BlockGiven.config}).
|
|
28
|
+
#
|
|
29
|
+
# @param chain [Chain, Symbol, String, Integer, nil] chain, symbol (`:base`), name ("base-sepolia") or chain id;
|
|
30
|
+
# resolved with {Chains.resolve}. Defaults to `BlockGiven.config.chain`.
|
|
31
|
+
# @param connector [Connectors::Base, nil] JSON-RPC transport. Defaults to `BlockGiven.config.connector`.
|
|
32
|
+
# @param polling_interval [Numeric, nil] seconds between two polls (receipts, watchers). Defaults to
|
|
33
|
+
# `BlockGiven.config.polling_interval`.
|
|
34
|
+
# @param timeout [Numeric, nil] seconds before {#wait_for_transaction_receipt} gives up. Defaults to
|
|
35
|
+
# `BlockGiven.config.timeout`.
|
|
36
|
+
# @param logger [Logger, nil] logger receiving debug lines for each RPC round trip. Defaults to
|
|
37
|
+
# `BlockGiven.config.logger`.
|
|
38
|
+
# @raise [ConfigurationError] when no chain or no connector is given and none is configured globally
|
|
13
39
|
def initialize(chain: nil, connector: nil, polling_interval: nil, timeout: nil, logger: nil)
|
|
14
40
|
@chain = chain ? Chains.resolve(chain) : BlockGiven.config.chain!
|
|
15
41
|
@connector = connector || BlockGiven.config.connector!
|
|
@@ -18,11 +44,33 @@ module BlockGiven
|
|
|
18
44
|
@logger = logger
|
|
19
45
|
end
|
|
20
46
|
|
|
47
|
+
# Seconds between two polls, falling back to `BlockGiven.config.polling_interval`.
|
|
48
|
+
#
|
|
49
|
+
# @return [Numeric]
|
|
21
50
|
def polling_interval = @polling_interval || BlockGiven.config.polling_interval
|
|
51
|
+
|
|
52
|
+
# Seconds before a blocking wait gives up, falling back to `BlockGiven.config.timeout`.
|
|
53
|
+
#
|
|
54
|
+
# @return [Numeric]
|
|
22
55
|
def timeout = @timeout || BlockGiven.config.timeout
|
|
56
|
+
|
|
57
|
+
# Logger used for RPC debug lines and watcher warnings, falling back to `BlockGiven.config.logger`.
|
|
58
|
+
#
|
|
59
|
+
# @return [Logger]
|
|
23
60
|
def logger = @logger || BlockGiven.config.logger
|
|
24
61
|
|
|
25
|
-
#
|
|
62
|
+
# Performs a raw JSON-RPC call through the connector and returns the undecoded `result`.
|
|
63
|
+
#
|
|
64
|
+
# The method name and params are logged at debug level, as is the (truncated) result.
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# client.request("eth_blockNumber") # => "0xbc614e"
|
|
68
|
+
# client.request("eth_getBalance", "0x...", "latest") # => "0xde0b6b3a7640000"
|
|
69
|
+
# @param method [String] JSON-RPC method name
|
|
70
|
+
# @param params [Array<Object>] positional JSON-RPC params, passed as-is
|
|
71
|
+
# @return [Object] the JSON-RPC `result` (String, Hash, Array, nil...) as returned by the node
|
|
72
|
+
# @raise [RpcError] when the node answers with a JSON-RPC error
|
|
73
|
+
# @raise [HttpError] when the transport fails (non-2xx status, network error, invalid JSON)
|
|
26
74
|
def request(method, *params)
|
|
27
75
|
logger.debug { "[block_given] -> #{method} #{params.inspect}" }
|
|
28
76
|
result = connector.request(method, params, chain: chain)
|
|
@@ -30,29 +78,70 @@ module BlockGiven
|
|
|
30
78
|
result
|
|
31
79
|
end
|
|
32
80
|
|
|
33
|
-
#
|
|
81
|
+
# Sends several JSON-RPC calls at once through the connector ({Connectors::Base#batch}).
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# client.batch([["eth_blockNumber"], ["eth_chainId"]]) # => ["0xbc614e", "0x2105"]
|
|
85
|
+
# @param calls [Array<Array(String, Array)>] `[method, params]` pairs; params may be omitted
|
|
86
|
+
# @return [Array<Object, RpcError>] one raw result per call, in order; a failed call is returned (not raised)
|
|
87
|
+
# as an {RpcError} instance
|
|
88
|
+
# @raise [HttpError] when the whole batch request fails at the transport level
|
|
34
89
|
def batch(calls) = connector.batch(calls, chain: chain)
|
|
35
90
|
|
|
36
91
|
# --- Chain / blocks -----------------------------------------------------
|
|
37
92
|
|
|
93
|
+
# Chain id reported by the node (`eth_chainId`).
|
|
94
|
+
#
|
|
95
|
+
# @return [Integer] the chain id, e.g. 8453 for Base
|
|
38
96
|
def chain_id = Utils.hex_to_int(request("eth_chainId"))
|
|
97
|
+
|
|
98
|
+
# Number of the most recent block (`eth_blockNumber`).
|
|
99
|
+
#
|
|
100
|
+
# @return [Integer] the current head block number
|
|
39
101
|
def block_number = Utils.hex_to_int(request("eth_blockNumber"))
|
|
40
102
|
|
|
103
|
+
# Fetches a block by number, tag or hash (`eth_getBlockByNumber`, or `eth_getBlockByHash` when `block`
|
|
104
|
+
# is a 32-byte hex hash).
|
|
105
|
+
#
|
|
106
|
+
# @param block [Integer, Symbol, String] block number, hex QUANTITY, block tag (`:latest`, `:earliest`,
|
|
107
|
+
# `:pending`, `:safe`, `:finalized`) or a 66-character `0x` block hash
|
|
108
|
+
# @param include_transactions [Boolean] when true the `:transactions` key holds full transaction objects
|
|
109
|
+
# instead of transaction hashes
|
|
110
|
+
# @return [Hash{Symbol => Object}, nil] the block with snake_case Symbol keys and Integer quantities
|
|
111
|
+
# (`:number`, `:timestamp`, `:gas_used`, `:gas_limit`, `:base_fee_per_gas`, ...), or nil when the node does
|
|
112
|
+
# not know the block
|
|
113
|
+
# @raise [InvalidArgumentError] when `block` is neither a number, a known tag nor a hex string
|
|
41
114
|
def get_block(block = :latest, include_transactions: false)
|
|
42
115
|
method = Utils.hex?(block.to_s) && block.to_s.length == 66 ? "eth_getBlockByHash" : "eth_getBlockByNumber"
|
|
43
116
|
raw = request(method, block_param(block), include_transactions)
|
|
44
117
|
raw && Normalizer.normalize(raw)
|
|
45
118
|
end
|
|
46
119
|
|
|
120
|
+
# Legacy gas price suggested by the node (`eth_gasPrice`).
|
|
121
|
+
#
|
|
122
|
+
# @return [Integer] gas price in wei
|
|
47
123
|
def gas_price = Utils.hex_to_int(request("eth_gasPrice"))
|
|
48
124
|
|
|
125
|
+
# Priority fee (tip) suggested by the node (`eth_maxPriorityFeePerGas`).
|
|
126
|
+
#
|
|
127
|
+
# Not every node implements the method: on an {RpcError} the value falls back to 1 gwei.
|
|
128
|
+
#
|
|
129
|
+
# @return [Integer] max priority fee per gas in wei
|
|
49
130
|
def max_priority_fee_per_gas
|
|
50
131
|
Utils.hex_to_int(request("eth_maxPriorityFeePerGas"))
|
|
51
132
|
rescue RpcError
|
|
52
133
|
Utils.parse_gwei("1") # method not supported by every node
|
|
53
134
|
end
|
|
54
135
|
|
|
55
|
-
# EIP-1559
|
|
136
|
+
# Estimates EIP-1559 fees with viem's semantics: `max_fee_per_gas = baseFee * multiplier + priority fee`.
|
|
137
|
+
#
|
|
138
|
+
# The base fee comes from the latest block (`eth_getBlockByNumber`), falling back to `eth_gasPrice` on
|
|
139
|
+
# chains without EIP-1559; the priority fee comes from {#max_priority_fee_per_gas}.
|
|
140
|
+
#
|
|
141
|
+
# @param base_fee_multiplier [Numeric, nil] safety margin applied to the base fee. Defaults to
|
|
142
|
+
# `BlockGiven.config.base_fee_multiplier` (1.2).
|
|
143
|
+
# @return [Hash{Symbol => Integer}] `:base_fee_per_gas`, `:max_priority_fee_per_gas` and `:max_fee_per_gas`,
|
|
144
|
+
# all in wei
|
|
56
145
|
def estimate_fees_per_gas(base_fee_multiplier: nil)
|
|
57
146
|
multiplier = base_fee_multiplier || BlockGiven.config.base_fee_multiplier
|
|
58
147
|
block = get_block(:latest)
|
|
@@ -67,54 +156,154 @@ module BlockGiven
|
|
|
67
156
|
|
|
68
157
|
# --- Accounts -----------------------------------------------------------
|
|
69
158
|
|
|
159
|
+
# Native currency balance of an account (`eth_getBalance`).
|
|
160
|
+
#
|
|
161
|
+
# @param address [String, #address] `0x` address (checksummed before being sent), or an object responding
|
|
162
|
+
# to `address` such as a {Wallet} or a {Contract}
|
|
163
|
+
# @param block [Integer, Symbol, String, nil] block number, hex QUANTITY or tag (`:latest`, `:earliest`,
|
|
164
|
+
# `:pending`, `:safe`, `:finalized`)
|
|
165
|
+
# @return [Integer] balance in wei
|
|
166
|
+
# @raise [InvalidAddressError] when `address` is not a valid 20-byte hex address
|
|
70
167
|
def get_balance(address, block: :latest)
|
|
71
168
|
Utils.hex_to_int(request("eth_getBalance", Utils.checksum_address(address), Utils.block_tag(block)))
|
|
72
169
|
end
|
|
73
170
|
|
|
171
|
+
# Number of transactions sent from an account, i.e. its next nonce (`eth_getTransactionCount`).
|
|
172
|
+
#
|
|
173
|
+
# Defaults to the `pending` tag so that queued transactions are taken into account.
|
|
174
|
+
#
|
|
175
|
+
# @param address [String, #address] `0x` address (checksummed before being sent)
|
|
176
|
+
# @param block [Integer, Symbol, String, nil] block number, hex QUANTITY or tag (`:latest`, `:earliest`,
|
|
177
|
+
# `:pending`, `:safe`, `:finalized`)
|
|
178
|
+
# @return [Integer] the transaction count
|
|
179
|
+
# @raise [InvalidAddressError] when `address` is not a valid 20-byte hex address
|
|
74
180
|
def get_transaction_count(address, block: :pending)
|
|
75
181
|
Utils.hex_to_int(request("eth_getTransactionCount", Utils.checksum_address(address), Utils.block_tag(block)))
|
|
76
182
|
end
|
|
77
183
|
|
|
184
|
+
# Bytecode deployed at an address (`eth_getCode`).
|
|
185
|
+
#
|
|
186
|
+
# @param address [String, #address] `0x` address (checksummed before being sent)
|
|
187
|
+
# @param block [Integer, Symbol, String, nil] block number, hex QUANTITY or tag (`:latest`, `:earliest`,
|
|
188
|
+
# `:pending`, `:safe`, `:finalized`)
|
|
189
|
+
# @return [String] the code as a `0x` hex string; `"0x"` when the address holds no code
|
|
190
|
+
# @raise [InvalidAddressError] when `address` is not a valid 20-byte hex address
|
|
78
191
|
def get_code(address, block: :latest)
|
|
79
192
|
request("eth_getCode", Utils.checksum_address(address), Utils.block_tag(block))
|
|
80
193
|
end
|
|
81
194
|
|
|
195
|
+
# Whether an address holds bytecode at the latest block (see {#get_code}).
|
|
196
|
+
#
|
|
197
|
+
# @param address [String, #address] `0x` address
|
|
198
|
+
# @return [Boolean] true for a contract, false for an externally owned account
|
|
199
|
+
# @raise [InvalidAddressError] when `address` is not a valid 20-byte hex address
|
|
82
200
|
def contract?(address) = get_code(address) != "0x"
|
|
83
201
|
|
|
202
|
+
# Reads a raw storage slot of a contract (`eth_getStorageAt`).
|
|
203
|
+
#
|
|
204
|
+
# @param address [String, #address] contract address (checksummed before being sent)
|
|
205
|
+
# @param slot [Integer, String] storage slot, as an Integer or a hex string
|
|
206
|
+
# @param block [Integer, Symbol, String, nil] block number, hex QUANTITY or tag (`:latest`, `:earliest`,
|
|
207
|
+
# `:pending`, `:safe`, `:finalized`)
|
|
208
|
+
# @return [String] the 32-byte slot value as a `0x` hex string
|
|
209
|
+
# @raise [InvalidAddressError] when `address` is not a valid 20-byte hex address
|
|
210
|
+
# @raise [InvalidArgumentError] when `slot` is neither an Integer nor a String
|
|
84
211
|
def get_storage_at(address, slot, block: :latest)
|
|
85
212
|
request("eth_getStorageAt", Utils.checksum_address(address), Utils.to_hex(slot), Utils.block_tag(block))
|
|
86
213
|
end
|
|
87
214
|
|
|
88
215
|
# --- Calls --------------------------------------------------------------
|
|
89
216
|
|
|
90
|
-
# Executes a read-only call
|
|
217
|
+
# Executes a read-only call against a contract (`eth_call`) and returns the raw ABI-encoded result.
|
|
218
|
+
#
|
|
219
|
+
# Higher-level decoding lives in {Contract}; use this when you already hold encoded calldata.
|
|
220
|
+
#
|
|
221
|
+
# @example Reading `totalSupply()` of an ERC20
|
|
222
|
+
# client.call(to: usdc_address, data: "0x18160ddd")
|
|
223
|
+
# # => "0x000000000000000000000000000000000000000000000000000001c6bf526340"
|
|
224
|
+
# @param to [String, #address] contract address (checksummed before being sent)
|
|
225
|
+
# @param data [String] ABI-encoded calldata as a hex string (`0x` prefix optional); omitted when empty
|
|
226
|
+
# @param from [String, #address, nil] sender address, for calls whose result depends on `msg.sender`
|
|
227
|
+
# @param value [Integer, nil] wei sent along with the call; omitted when nil or 0
|
|
228
|
+
# @param gas [Integer, nil] gas limit for the call
|
|
229
|
+
# @param block [Integer, Symbol, String, nil] block number, hex QUANTITY or tag (`:latest`, `:earliest`,
|
|
230
|
+
# `:pending`, `:safe`, `:finalized`)
|
|
231
|
+
# @return [String] the return data as a `0x` hex string
|
|
232
|
+
# @raise [ContractRevertError] when the EVM reverts (a subclass of {RpcError}, carrying the revert data)
|
|
233
|
+
# @raise [RpcError] for any other JSON-RPC error
|
|
234
|
+
# @raise [InvalidAddressError] when `to` or `from` is not a valid 20-byte hex address
|
|
91
235
|
def call(to:, data:, from: nil, value: nil, gas: nil, block: :latest)
|
|
92
236
|
request("eth_call", call_object(to: to, data: data, from: from, value: value, gas: gas), Utils.block_tag(block))
|
|
93
237
|
end
|
|
94
238
|
|
|
239
|
+
# Estimates the gas needed by a transaction (`eth_estimateGas`).
|
|
240
|
+
#
|
|
241
|
+
# @param to [String, #address] recipient / contract address (checksummed before being sent)
|
|
242
|
+
# @param data [String, nil] ABI-encoded calldata as a hex string; omitted when nil or empty
|
|
243
|
+
# @param from [String, #address, nil] sender address
|
|
244
|
+
# @param value [Integer, nil] wei sent along with the transaction; omitted when nil or 0
|
|
245
|
+
# @return [Integer] the estimated gas units
|
|
246
|
+
# @raise [ContractRevertError] when the simulated execution reverts
|
|
247
|
+
# @raise [RpcError] for any other JSON-RPC error
|
|
248
|
+
# @raise [InvalidAddressError] when `to` or `from` is not a valid 20-byte hex address
|
|
95
249
|
def estimate_gas(to:, data: nil, from: nil, value: nil)
|
|
96
250
|
Utils.hex_to_int(request("eth_estimateGas", call_object(to: to, data: data, from: from, value: value)))
|
|
97
251
|
end
|
|
98
252
|
|
|
99
253
|
# --- Transactions -------------------------------------------------------
|
|
100
254
|
|
|
255
|
+
# Broadcasts a signed transaction (`eth_sendRawTransaction`).
|
|
256
|
+
#
|
|
257
|
+
# @param raw [String] RLP-encoded signed transaction as a hex string (`0x` prefix optional)
|
|
258
|
+
# @return [Transaction] handle on the broadcast transaction, bound to this client so it can `wait`
|
|
259
|
+
# @raise [ContractRevertError] when the node rejects the transaction because it would revert
|
|
260
|
+
# @raise [RpcError] for any other JSON-RPC error (nonce too low, underpriced, ...)
|
|
101
261
|
def send_raw_transaction(raw)
|
|
102
262
|
Transaction.new(request("eth_sendRawTransaction", Utils.prefix_hex(raw)), client: self)
|
|
103
263
|
end
|
|
104
264
|
|
|
265
|
+
# Wraps an already known transaction hash in a {Transaction} handle. No RPC call is made.
|
|
266
|
+
#
|
|
267
|
+
# @param hash [String] `0x` transaction hash
|
|
268
|
+
# @return [Transaction] handle bound to this client
|
|
105
269
|
def transaction(hash) = Transaction.new(hash, client: self)
|
|
106
270
|
|
|
271
|
+
# Fetches a transaction object by hash (`eth_getTransactionByHash`).
|
|
272
|
+
#
|
|
273
|
+
# @param hash [String] `0x` transaction hash
|
|
274
|
+
# @return [Hash{Symbol => Object}, nil] the transaction with snake_case Symbol keys and Integer quantities
|
|
275
|
+
# (`:nonce`, `:value`, `:gas`, `:block_number`, `:max_fee_per_gas`, ...; `:block_number` is nil while
|
|
276
|
+
# pending), or nil when the node does not know the hash
|
|
107
277
|
def get_transaction(hash)
|
|
108
278
|
raw = request("eth_getTransactionByHash", hash)
|
|
109
279
|
raw && Normalizer.normalize(raw)
|
|
110
280
|
end
|
|
111
281
|
|
|
282
|
+
# Fetches the receipt of a mined transaction (`eth_getTransactionReceipt`).
|
|
283
|
+
#
|
|
284
|
+
# @param hash [String] `0x` transaction hash
|
|
285
|
+
# @return [Receipt, nil] the receipt, or nil while the transaction is not mined (or unknown)
|
|
112
286
|
def get_transaction_receipt(hash)
|
|
113
287
|
raw = request("eth_getTransactionReceipt", hash)
|
|
114
288
|
raw && Receipt.new(raw)
|
|
115
289
|
end
|
|
116
290
|
|
|
117
|
-
#
|
|
291
|
+
# Blocks until the transaction is mined and confirmed by `confirmations` blocks, then returns its receipt.
|
|
292
|
+
#
|
|
293
|
+
# Polls `eth_getTransactionReceipt` (and `eth_blockNumber` when more than one confirmation is required)
|
|
294
|
+
# through {Poller.poll}. A transaction mined in the current head block has exactly 1 confirmation.
|
|
295
|
+
# The receipt is returned whatever its status: check {Receipt#success?} or use {Transaction#wait!}.
|
|
296
|
+
#
|
|
297
|
+
# @example
|
|
298
|
+
# receipt = client.wait_for_transaction_receipt(tx.hash, confirmations: 3, timeout: 300)
|
|
299
|
+
# receipt.status # => :success
|
|
300
|
+
# @param hash [String] `0x` transaction hash
|
|
301
|
+
# @param confirmations [Integer, nil] blocks that must include or follow the transaction (1 = mined).
|
|
302
|
+
# Defaults to `BlockGiven.config.confirmations`.
|
|
303
|
+
# @param timeout [Numeric, nil] seconds before giving up. Defaults to {#timeout}.
|
|
304
|
+
# @param polling_interval [Numeric, nil] seconds between two polls. Defaults to {#polling_interval}.
|
|
305
|
+
# @return [Receipt] the mined (and confirmed) receipt
|
|
306
|
+
# @raise [TimeoutError] when the receipt is still missing or unconfirmed after `timeout` seconds
|
|
118
307
|
def wait_for_transaction_receipt(hash, confirmations: nil, timeout: nil, polling_interval: nil)
|
|
119
308
|
confirmations ||= BlockGiven.config.confirmations
|
|
120
309
|
interval = polling_interval || self.polling_interval
|
|
@@ -129,6 +318,29 @@ module BlockGiven
|
|
|
129
318
|
|
|
130
319
|
# --- Logs ---------------------------------------------------------------
|
|
131
320
|
|
|
321
|
+
# Fetches event logs matching a filter (`eth_getLogs`).
|
|
322
|
+
#
|
|
323
|
+
# Providers cap the block range a single `eth_getLogs` may span; use {#get_logs_in_chunks} for large
|
|
324
|
+
# ranges and {Contract#get_events} to decode the logs against an ABI.
|
|
325
|
+
#
|
|
326
|
+
# @example Transfer logs of one contract over 100 blocks
|
|
327
|
+
# transfer = BlockGiven::Utils.keccak256("Transfer(address,address,uint256)")
|
|
328
|
+
# client.get_logs(address: usdc, topics: [transfer], from_block: 20_000_000, to_block: 20_000_099)
|
|
329
|
+
# # => [{ address: "0x...", topics: [...], data: "0x...", block_number: 20_000_001, log_index: 3, ... }]
|
|
330
|
+
# @param address [String, #address, Array<String, #address>, nil] one or several contract addresses
|
|
331
|
+
# (each checksummed before being sent); nil matches every address
|
|
332
|
+
# @param topics [Array<String, Array<String>, nil>, nil] positional topic filter as expected by the node:
|
|
333
|
+
# a `0x` 32-byte topic, an Array of alternatives (OR), or nil as a wildcard at that position
|
|
334
|
+
# @param from_block [Integer, Symbol, String, nil] start of the range: block number, hex QUANTITY or tag
|
|
335
|
+
# (`:latest`, `:earliest`, `:pending`, `:safe`, `:finalized`); ignored when `block_hash` is given
|
|
336
|
+
# @param to_block [Integer, Symbol, String, nil] end of the range (inclusive), same forms as `from_block`
|
|
337
|
+
# @param block_hash [String, nil] restrict the query to a single block by hash instead of a block range
|
|
338
|
+
# @return [Array<Hash{Symbol => Object}>] raw logs with snake_case Symbol keys: `:address`, `:topics`
|
|
339
|
+
# (Array of `0x` strings), `:data`, `:block_number`, `:transaction_index` and `:log_index` (Integer),
|
|
340
|
+
# `:block_hash`, `:transaction_hash` and `:removed`
|
|
341
|
+
# @raise [InvalidAddressError] when an address is not a valid 20-byte hex address
|
|
342
|
+
# @raise [InvalidArgumentError] when a block argument is neither a number, a known tag nor a hex string
|
|
343
|
+
# @raise [RpcError] when the node rejects the filter (range too large, too many results, ...)
|
|
132
344
|
def get_logs(address: nil, topics: nil, from_block: :latest, to_block: :latest, block_hash: nil)
|
|
133
345
|
filter = {}
|
|
134
346
|
filter[:address] = Array(address).map { |a| Utils.checksum_address(a) } if address
|
|
@@ -145,8 +357,20 @@ module BlockGiven
|
|
|
145
357
|
|
|
146
358
|
# --- Watchers (background polling) --------------------------------------
|
|
147
359
|
|
|
148
|
-
#
|
|
149
|
-
#
|
|
360
|
+
# Starts a background {Watcher} yielding the head block number as it advances (polls `eth_blockNumber`).
|
|
361
|
+
#
|
|
362
|
+
# The first tick yields the current head. Later ticks yield only when the head moved forward: by default
|
|
363
|
+
# just the new head, with `emit_missed: true` every block number between the previous head (excluded) and
|
|
364
|
+
# the new one (included), in order.
|
|
365
|
+
#
|
|
366
|
+
# @param polling_interval [Numeric, nil] seconds between two polls. Defaults to {#polling_interval}.
|
|
367
|
+
# @param emit_missed [Boolean] yield every block skipped between two polls instead of only the latest one
|
|
368
|
+
# @param id [String, nil] stable identifier for {Watcher.find} / {Watcher.stop}. Defaults to a generated one.
|
|
369
|
+
# @yield [number] for each new block number, from the watcher thread
|
|
370
|
+
# @yieldparam number [Integer] block number
|
|
371
|
+
# @yieldreturn [void]
|
|
372
|
+
# @return [Watcher] the started watcher; call {Watcher#stop} to end it
|
|
373
|
+
# @raise [InvalidArgumentError] when a watcher with the same `id` is already running
|
|
150
374
|
def watch_block_number(polling_interval: nil, emit_missed: false, id: nil, &block)
|
|
151
375
|
last = nil
|
|
152
376
|
watcher("block_number", polling_interval, id: id) do
|
|
@@ -162,14 +386,44 @@ module BlockGiven
|
|
|
162
386
|
end
|
|
163
387
|
end
|
|
164
388
|
|
|
389
|
+
# Starts a background {Watcher} yielding every new block object (`eth_getBlockByNumber` for each block
|
|
390
|
+
# number reported by {#watch_block_number} with `emit_missed: true`).
|
|
391
|
+
#
|
|
392
|
+
# @param polling_interval [Numeric, nil] seconds between two polls. Defaults to {#polling_interval}.
|
|
393
|
+
# @param include_transactions [Boolean] when true blocks carry full transaction objects instead of hashes
|
|
394
|
+
# @param id [String, nil] stable identifier for {Watcher.find} / {Watcher.stop}. Defaults to a generated one.
|
|
395
|
+
# @yield [block] for each new block, in order, from the watcher thread
|
|
396
|
+
# @yieldparam block [Hash{Symbol => Object}] normalized block as returned by {#get_block}
|
|
397
|
+
# @yieldreturn [void]
|
|
398
|
+
# @return [Watcher] the started watcher; call {Watcher#stop} to end it
|
|
399
|
+
# @raise [InvalidArgumentError] when a watcher with the same `id` is already running
|
|
165
400
|
def watch_blocks(polling_interval: nil, include_transactions: false, id: nil, &block)
|
|
166
401
|
watch_block_number(polling_interval: polling_interval, emit_missed: true, id: id) do |number|
|
|
167
402
|
block.call(get_block(number, include_transactions: include_transactions))
|
|
168
403
|
end
|
|
169
404
|
end
|
|
170
405
|
|
|
171
|
-
# get_logs over a large range, split in chunks of
|
|
172
|
-
#
|
|
406
|
+
# Runs {#get_logs} over a large block range, split in consecutive chunks of at most `max_block_range`
|
|
407
|
+
# blocks so that provider limits are respected.
|
|
408
|
+
#
|
|
409
|
+
# With a block, each chunk's logs are yielded as soon as they are fetched (and the return value is an
|
|
410
|
+
# empty Array); without a block every log is collected and returned at once.
|
|
411
|
+
#
|
|
412
|
+
# @param from_block [Integer] first block of the range (inclusive)
|
|
413
|
+
# @param address [String, #address, Array<String, #address>, nil] address filter, see {#get_logs}
|
|
414
|
+
# @param topics [Array<String, Array<String>, nil>, nil] topic filter, see {#get_logs}
|
|
415
|
+
# @param to_block [Integer, Symbol, String, nil] last block of the range (inclusive). A block tag
|
|
416
|
+
# (`:latest`, `:earliest`, `:pending`, `:safe`, `:finalized`) or nil is replaced by the current head
|
|
417
|
+
# (`eth_blockNumber`) before chunking.
|
|
418
|
+
# @param max_block_range [Integer, nil] chunk size in blocks. Defaults to `BlockGiven.config.max_block_range`
|
|
419
|
+
# (2000).
|
|
420
|
+
# @yield [logs, from, to] once per chunk, in ascending block order
|
|
421
|
+
# @yieldparam logs [Array<Hash{Symbol => Object}>] logs of the chunk (possibly empty), see {#get_logs}
|
|
422
|
+
# @yieldparam from [Integer] first block of the chunk
|
|
423
|
+
# @yieldparam to [Integer] last block of the chunk (inclusive)
|
|
424
|
+
# @yieldreturn [void]
|
|
425
|
+
# @return [Array<Hash{Symbol => Object}>] every log of the range when no block is given, `[]` otherwise
|
|
426
|
+
# @raise [RpcError] when a chunk is rejected by the node
|
|
173
427
|
def get_logs_in_chunks(from_block:, address: nil, topics: nil, to_block: :latest, max_block_range: nil)
|
|
174
428
|
size = max_block_range || BlockGiven.config.max_block_range
|
|
175
429
|
to_block = block_number if to_block.nil? || Utils::BLOCK_TAGS.include?(to_block.to_s)
|
|
@@ -184,13 +438,46 @@ module BlockGiven
|
|
|
184
438
|
collected
|
|
185
439
|
end
|
|
186
440
|
|
|
187
|
-
#
|
|
441
|
+
# Starts a background {Watcher} yielding new logs matching a filter, one Array per processed block range.
|
|
442
|
+
#
|
|
443
|
+
# On every tick the watcher reads the head (`eth_blockNumber`), subtracts `confirmations`, and walks from
|
|
444
|
+
# the last processed block (excluded) up to that point in ranges of at most `max_block_range` blocks. For
|
|
445
|
+
# each range it calls `eth_getLogs`, yields the logs (only when there are some), calls `on_progress`, then
|
|
446
|
+
# advances {Watcher#cursor} to the range's last block. The cursor is therefore always the last block whose
|
|
447
|
+
# logs were handed to the block: if the block raises, the cursor is not advanced, the error goes through
|
|
448
|
+
# the watcher's error handling and the same range is retried on the next tick.
|
|
449
|
+
#
|
|
450
|
+
# Without `from_block` the watcher starts at the current head (minus `confirmations`) and only reports
|
|
451
|
+
# logs emitted afterwards. With `from_block` it first catches up from that block, chunk by chunk, before
|
|
452
|
+
# following the head; persist the `to` value received by `on_progress` and pass it back as `from_block + 1`
|
|
453
|
+
# to resume after a restart. See the README section "How watchers behave".
|
|
188
454
|
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
#
|
|
455
|
+
# @example Following Transfer events and persisting a cursor
|
|
456
|
+
# transfer = BlockGiven::Utils.keccak256("Transfer(address,address,uint256)")
|
|
457
|
+
# watcher = client.watch_logs(
|
|
458
|
+
# address: usdc, topics: [transfer], from_block: Cursor.last + 1, confirmations: 2, id: "usdc-transfers",
|
|
459
|
+
# on_progress: ->(_from, to) { Cursor.save(to) }
|
|
460
|
+
# ) { |logs| logs.each { |log| Transfers.ingest(log) } }
|
|
461
|
+
# watcher.cursor # => last processed block number
|
|
462
|
+
# BlockGiven::Watcher.stop("usdc-transfers", join: 5)
|
|
463
|
+
# @param address [String, #address, Array<String, #address>, nil] address filter, see {#get_logs}
|
|
464
|
+
# @param topics [Array<String, Array<String>, nil>, nil] topic filter, see {#get_logs}
|
|
465
|
+
# @param from_block [Integer, nil] first block to process (inclusive); nil starts at the current head
|
|
466
|
+
# @param polling_interval [Numeric, nil] seconds between two ticks. Defaults to {#polling_interval}.
|
|
467
|
+
# @param max_block_range [Integer, nil] maximum blocks per `eth_getLogs` call. Defaults to
|
|
468
|
+
# `BlockGiven.config.max_block_range` (2000).
|
|
469
|
+
# @param confirmations [Integer] number of blocks to stay behind the head, to avoid processing logs that a
|
|
470
|
+
# reorg could remove (0 processes up to the head)
|
|
471
|
+
# @param on_progress [#call, nil] callable invoked with `(from, to)` after each range was yielded and
|
|
472
|
+
# before the cursor moves; `to` is the last processed block, suited for persistence
|
|
473
|
+
# @param id [String, nil] stable identifier for {Watcher.find} / {Watcher.stop}. Defaults to a generated one.
|
|
474
|
+
# @param name [String, nil] human-readable name shown by {Watcher#inspect}. Defaults to
|
|
475
|
+
# `"logs@<first address>"` (or `"logs@*"` without address filter).
|
|
476
|
+
# @yield [logs] for each processed range that contains at least one matching log, from the watcher thread
|
|
477
|
+
# @yieldparam logs [Array<Hash{Symbol => Object}>] non-empty normalized logs, see {#get_logs}
|
|
478
|
+
# @yieldreturn [void]
|
|
479
|
+
# @return [Watcher] the started watcher; {Watcher#cursor} holds the last processed block number
|
|
480
|
+
# @raise [InvalidArgumentError] when a watcher with the same `id` is already running
|
|
194
481
|
def watch_logs(address: nil, topics: nil, from_block: nil, polling_interval: nil, max_block_range: nil,
|
|
195
482
|
confirmations: 0, on_progress: nil, id: nil, name: nil, &block)
|
|
196
483
|
last = from_block ? from_block - 1 : block_number - confirmations
|
|
@@ -207,11 +494,26 @@ module BlockGiven
|
|
|
207
494
|
end
|
|
208
495
|
end
|
|
209
496
|
|
|
210
|
-
#
|
|
497
|
+
# Builds and starts a generic background {Watcher} bound to this client's polling interval and logger.
|
|
498
|
+
#
|
|
499
|
+
# Every `watch_*` helper goes through this method so that watchers share the registry, the thread naming
|
|
500
|
+
# and the error handling of {Watcher}.
|
|
501
|
+
#
|
|
502
|
+
# @param name [String] human-readable name, also used to generate the id when none is given
|
|
503
|
+
# @param polling_interval [Numeric, nil] seconds between two ticks. Defaults to {#polling_interval}.
|
|
504
|
+
# @param id [String, nil] stable identifier for {Watcher.find} / {Watcher.stop}. Defaults to a generated one.
|
|
505
|
+
# @yield [watcher] on every tick, from the watcher thread
|
|
506
|
+
# @yieldparam watcher [Watcher] the watcher itself (to read {Watcher#stopped?} or set {Watcher#cursor})
|
|
507
|
+
# @yieldreturn [void]
|
|
508
|
+
# @return [Watcher] the started watcher
|
|
509
|
+
# @raise [InvalidArgumentError] when a watcher with the same `id` is already running
|
|
211
510
|
def watcher(name, polling_interval = nil, id: nil, &tick)
|
|
212
511
|
Watcher.new(interval: polling_interval || self.polling_interval, name: name, id: id, logger: logger, &tick).start
|
|
213
512
|
end
|
|
214
513
|
|
|
514
|
+
# Short description of the client; the connector's `inspect` never exposes credentials.
|
|
515
|
+
#
|
|
516
|
+
# @return [String]
|
|
215
517
|
def inspect = "#<BlockGiven::Client chain=#{chain} connector=#{connector.inspect}>"
|
|
216
518
|
|
|
217
519
|
private
|
|
@@ -3,12 +3,74 @@
|
|
|
3
3
|
require "logger"
|
|
4
4
|
|
|
5
5
|
module BlockGiven
|
|
6
|
-
# Global configuration set through
|
|
6
|
+
# Global configuration set through {BlockGiven.configure}.
|
|
7
|
+
#
|
|
8
|
+
# Every option has a default, so an application only has to provide a {#connector} and a {#chain}. Values
|
|
9
|
+
# are read lazily by {Client} (polling interval, timeout, confirmations, fee multipliers, block range),
|
|
10
|
+
# {Wallet} ({#gas_multiplier}), {Contract.abi_file} ({#abi_path}) and the Rails railtie ({#logger}), so
|
|
11
|
+
# changing them affects clients that were already built.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# BlockGiven.configure do |c|
|
|
15
|
+
# c.connector = BlockGiven::Connectors::Alchemy.new(api_key: ENV["ALCHEMY_API_KEY"])
|
|
16
|
+
# c.chain = :base
|
|
17
|
+
# c.abi_path = Rails.root.join("abis")
|
|
18
|
+
# c.confirmations = 2
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @!attribute [rw] connector
|
|
22
|
+
# JSON-RPC transport used by the default client and by contracts without an explicit connector
|
|
23
|
+
# (default `nil`, required). Any {Connectors::Base} implementation: {Connectors::Alchemy},
|
|
24
|
+
# {Connectors::Http}, or {Connectors::Stub} in tests.
|
|
25
|
+
# @return [BlockGiven::Connectors::Base, nil]
|
|
26
|
+
# @!attribute [rw] polling_interval
|
|
27
|
+
# Seconds between two polls when waiting for a receipt, watching blocks or watching events
|
|
28
|
+
# (default `2.0`).
|
|
29
|
+
# @return [Float, Integer]
|
|
30
|
+
# @!attribute [rw] timeout
|
|
31
|
+
# Seconds before {Client#wait_for_transaction_receipt} gives up with a {TimeoutError} (default `180`).
|
|
32
|
+
# `nil` waits forever.
|
|
33
|
+
# @return [Float, Integer, nil]
|
|
34
|
+
# @!attribute [rw] gas_multiplier
|
|
35
|
+
# Safety margin applied on top of `eth_estimateGas` when a wallet fills in the gas limit of a
|
|
36
|
+
# transaction: `gas = ceil(estimate * gas_multiplier)` (default `1.2`).
|
|
37
|
+
# @return [Float]
|
|
38
|
+
# @!attribute [rw] base_fee_multiplier
|
|
39
|
+
# Multiplier applied to the latest block base fee when {Client#estimate_fees_per_gas} computes
|
|
40
|
+
# `maxFeePerGas = baseFee * base_fee_multiplier + maxPriorityFeePerGas` (default `1.2`, viem's default).
|
|
41
|
+
# @return [Float]
|
|
42
|
+
# @!attribute [rw] confirmations
|
|
43
|
+
# Number of blocks, including the mining block, a receipt must have before
|
|
44
|
+
# {Client#wait_for_transaction_receipt} and {Transaction#confirmed?} consider it final (default `1`, i.e. as
|
|
45
|
+
# soon as the transaction is mined).
|
|
46
|
+
# @return [Integer]
|
|
47
|
+
# @!attribute [rw] abi_path
|
|
48
|
+
# Directory {Contract.abi_file} resolves relative paths against, e.g. `Rails.root.join("abis")`
|
|
49
|
+
# (default `nil`: relative paths are resolved from the current working directory). ABIs belong to the
|
|
50
|
+
# application, the gem ships none.
|
|
51
|
+
# @return [String, Pathname, nil]
|
|
52
|
+
# @!attribute [rw] max_block_range
|
|
53
|
+
# Maximum number of blocks per `eth_getLogs` request; {Client#get_logs_in_chunks} and
|
|
54
|
+
# {Client#watch_logs} split wider ranges into chunks of this size to respect provider caps
|
|
55
|
+
# (default `2_000`).
|
|
56
|
+
# @return [Integer]
|
|
57
|
+
# @!attribute [r] chain
|
|
58
|
+
# Network the default client talks to (default `nil`, required). Assigned through {#chain=}, which accepts
|
|
59
|
+
# anything {Chains.resolve} understands.
|
|
60
|
+
# @return [BlockGiven::Chain, nil]
|
|
61
|
+
# @!attribute [r] logger
|
|
62
|
+
# Logger used by clients, connectors and watchers (default: a `Logger` writing to `$stderr` at `WARN`
|
|
63
|
+
# level with progname `"block_given"`). Assigning one through {#logger=} marks it as application-provided,
|
|
64
|
+
# which stops the Rails railtie from installing `Rails.logger`.
|
|
65
|
+
# @return [Logger]
|
|
7
66
|
class Configuration
|
|
8
67
|
attr_accessor :connector, :polling_interval, :timeout, :gas_multiplier, :base_fee_multiplier,
|
|
9
68
|
:confirmations, :abi_path, :max_block_range
|
|
10
69
|
attr_reader :chain, :logger
|
|
11
70
|
|
|
71
|
+
# Build a configuration holding the defaults documented on each attribute.
|
|
72
|
+
#
|
|
73
|
+
# @return [Configuration]
|
|
12
74
|
def initialize
|
|
13
75
|
@connector = nil
|
|
14
76
|
@chain = nil
|
|
@@ -23,24 +85,44 @@ module BlockGiven
|
|
|
23
85
|
@logger_configured = false
|
|
24
86
|
end
|
|
25
87
|
|
|
88
|
+
# Set the logger and remember that the application provided it (see {#logger_configured?}).
|
|
89
|
+
#
|
|
90
|
+
# @param logger [Logger] any object responding to `debug`, `info`, `warn` and `error`
|
|
91
|
+
# @return [Logger] the assigned logger
|
|
26
92
|
def logger=(logger)
|
|
27
93
|
@logger = logger
|
|
28
94
|
@logger_configured = true
|
|
29
95
|
end
|
|
30
96
|
|
|
31
|
-
#
|
|
97
|
+
# Whether an application set its own logger through {#logger=}.
|
|
98
|
+
#
|
|
99
|
+
# The Rails railtie only installs `Rails.logger` when this is false, so an explicit logger always wins.
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean]
|
|
32
102
|
def logger_configured? = @logger_configured
|
|
33
103
|
|
|
34
|
-
#
|
|
104
|
+
# Set the chain from a {Chain}, a symbol (`:base`), a network name (`"base-sepolia"`) or a chain id (`8453`).
|
|
105
|
+
#
|
|
106
|
+
# @param value [BlockGiven::Chain, Symbol, String, Integer, nil] nil clears the chain
|
|
107
|
+
# @return [BlockGiven::Chain, nil] the resolved chain
|
|
108
|
+
# @raise [BlockGiven::ConfigurationError] when the value does not match a known chain (see {Chains.resolve})
|
|
35
109
|
def chain=(value)
|
|
36
110
|
@chain = value.nil? ? nil : Chains.resolve(value)
|
|
37
111
|
end
|
|
38
112
|
|
|
113
|
+
# Return the connector or raise when none was configured.
|
|
114
|
+
#
|
|
115
|
+
# @return [BlockGiven::Connectors::Base]
|
|
116
|
+
# @raise [BlockGiven::ConfigurationError] when {#connector} is nil
|
|
39
117
|
def connector!
|
|
40
118
|
connector || raise(ConfigurationError, "no connector configured: set BlockGiven.config.connector " \
|
|
41
119
|
"(e.g. BlockGiven::Connectors::Alchemy.new(api_key: ...))")
|
|
42
120
|
end
|
|
43
121
|
|
|
122
|
+
# Return the chain or raise when none was configured.
|
|
123
|
+
#
|
|
124
|
+
# @return [BlockGiven::Chain]
|
|
125
|
+
# @raise [BlockGiven::ConfigurationError] when {#chain} is nil
|
|
44
126
|
def chain!
|
|
45
127
|
chain || raise(ConfigurationError, "no chain configured: set BlockGiven.config.chain (e.g. :base)")
|
|
46
128
|
end
|