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/contract.rb
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module BlockGiven
|
|
4
|
-
# Base class for typed contracts
|
|
5
|
-
# address / chain) and every ABI function becomes a Ruby method:
|
|
4
|
+
# Base class for typed contracts: declare an ABI and every ABI function becomes a Ruby method.
|
|
6
5
|
#
|
|
6
|
+
# Subclasses describe the contract with the class-level DSL ({Contract.abi} / {Contract.abi_file},
|
|
7
|
+
# optionally {Contract.address} and {Contract.chain}). Instances bind that ABI to an address and,
|
|
8
|
+
# optionally, to a {BlockGiven::Wallet} (needed for writes) or an explicit {BlockGiven::Client}.
|
|
9
|
+
#
|
|
10
|
+
# ## Dynamic methods
|
|
11
|
+
#
|
|
12
|
+
# {Contract.define_abi_methods!} defines one instance method per ABI function name, converted with
|
|
13
|
+
# `Utils.snake_case` (`balanceOf` becomes `#balance_of`). Every generated method has the signature
|
|
14
|
+
# `(*args, tx: {}, **kwargs)`:
|
|
15
|
+
#
|
|
16
|
+
# - ABI inputs are passed either positionally (`transfer(to, amount)`) or as keywords named after the
|
|
17
|
+
# snake_cased input names (`transfer(to: ..., value: ...)`); mixing both raises `InvalidArgumentError`.
|
|
18
|
+
# Inputs without a name in the ABI are positional only.
|
|
19
|
+
# - `tx:` is the only reserved keyword. It takes a Hash whose keys must belong to {TX_OPTIONS}
|
|
20
|
+
# (`value`, `gas`, `nonce`, `max_fee_per_gas`, `max_priority_fee_per_gas`, `gas_price`, `from`,
|
|
21
|
+
# `block`); any other key raises `InvalidArgumentError`. Every other keyword maps to an ABI input, so
|
|
22
|
+
# an ERC20 `value` input never collides with the wei amount (`tx: { value: }`).
|
|
23
|
+
# - `view` / `pure` functions run `eth_call` and return the decoded output (see {#read}); every other
|
|
24
|
+
# function is signed and broadcast and returns a {BlockGiven::Transaction} (see {#write}).
|
|
25
|
+
# - Names clashing with an existing `Contract` method (`send`, `class`, `address`, `read`...) are not
|
|
26
|
+
# defined; call them through {#read} / {#write} with the ABI name instead.
|
|
27
|
+
# - Overloaded functions are resolved by positional arity, by the set of keyword names, or by passing the
|
|
28
|
+
# full signature (`"safeMint(address,bytes)"`) to {#read} / {#write}. Ambiguity raises
|
|
29
|
+
# `AmbiguousFunctionError`. See {Abi::Interface#function}.
|
|
30
|
+
#
|
|
31
|
+
# @example Declaring and using a contract
|
|
7
32
|
# class Usdc < BlockGiven::Contract
|
|
8
|
-
#
|
|
33
|
+
# abi :erc20 # shipped standard (Abi::Standards), or abi_file "abis/my_contract.json"
|
|
9
34
|
# address "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
10
35
|
# end
|
|
11
36
|
#
|
|
@@ -14,16 +39,40 @@ module BlockGiven
|
|
|
14
39
|
# tx = usdc.transfer(to: "0x...", value: 1e6) # signed + broadcast -> BlockGiven::Transaction
|
|
15
40
|
# tx.wait! # polls the receipt
|
|
16
41
|
#
|
|
17
|
-
# Transaction / call overrides
|
|
42
|
+
# @example Transaction / call overrides in the reserved `tx:` keyword
|
|
18
43
|
# vault.deposit(amount, tx: { value: BlockGiven::Utils.parse_ether("0.1"), gas: 200_000 })
|
|
19
44
|
# token.balance_of(addr, tx: { block: 18_000_000 })
|
|
45
|
+
#
|
|
46
|
+
# @example Binding the same ABI to several addresses
|
|
47
|
+
# Erc20.at("0x4200000000000000000000000000000000000006", chain: :base)
|
|
20
48
|
class Contract
|
|
49
|
+
# Keys accepted in the `tx:` Hash of contract methods; any other key raises `InvalidArgumentError`.
|
|
21
50
|
TX_OPTIONS = %i[value gas nonce max_fee_per_gas max_priority_fee_per_gas gas_price from block].freeze
|
|
22
51
|
|
|
23
52
|
class << self
|
|
53
|
+
# The parsed ABI declared with {.abi} / {.abi_file}.
|
|
54
|
+
#
|
|
55
|
+
# @return [Abi::Interface, nil] `nil` until an ABI is declared
|
|
24
56
|
attr_reader :interface
|
|
25
57
|
|
|
26
|
-
#
|
|
58
|
+
# Declares the ABI (and defines the dynamic methods), or returns the raw ABI definitions.
|
|
59
|
+
#
|
|
60
|
+
# @param source [Array<Hash>, Hash, String, Symbol, Pathname, Abi::Interface, nil] an ABI Array, a
|
|
61
|
+
# Hardhat/Foundry artifact Hash with an `"abi"` key, a JSON String, the Symbol name of a shipped standard
|
|
62
|
+
# (`:erc20`, `:erc721`, `:erc1155`, `:erc4626`), a Pathname to a JSON file or an already parsed
|
|
63
|
+
# {Abi::Interface}; `nil` to read the current ABI
|
|
64
|
+
# @return [Abi::Interface] the parsed interface when `source` is given
|
|
65
|
+
# @return [Array<Hash>, nil] the raw ABI definitions when called without argument (`nil` if none)
|
|
66
|
+
# @raise [BlockGiven::AbiError] when the source cannot be parsed as an ABI
|
|
67
|
+
# @example
|
|
68
|
+
# class Erc20 < BlockGiven::Contract
|
|
69
|
+
# abi JSON.parse(File.read("abis/erc20.json"))
|
|
70
|
+
# end
|
|
71
|
+
# Erc20.abi # => [{"type"=>"function", "name"=>"balanceOf", ...}, ...]
|
|
72
|
+
# @example A shipped standard
|
|
73
|
+
# class Usdc < BlockGiven::Contract
|
|
74
|
+
# abi :erc20
|
|
75
|
+
# end
|
|
27
76
|
def abi(source = nil)
|
|
28
77
|
return interface&.raw if source.nil?
|
|
29
78
|
|
|
@@ -32,8 +81,20 @@ module BlockGiven
|
|
|
32
81
|
@interface
|
|
33
82
|
end
|
|
34
83
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
84
|
+
# Declares the ABI from a JSON file (a Hardhat/Foundry artifact or a bare ABI Array).
|
|
85
|
+
#
|
|
86
|
+
# Relative paths are resolved against `BlockGiven.config.abi_path` when it is set: ABIs live in
|
|
87
|
+
# your application, never in the gem.
|
|
88
|
+
#
|
|
89
|
+
# @param path [String, Pathname] absolute path, or path relative to `BlockGiven.config.abi_path`
|
|
90
|
+
# @return [Abi::Interface] the parsed interface
|
|
91
|
+
# @raise [BlockGiven::AbiError] when the file does not exist or is not a valid ABI
|
|
92
|
+
# @example
|
|
93
|
+
# BlockGiven.configure { |c| c.abi_path = Rails.root.join("config/abis") }
|
|
94
|
+
#
|
|
95
|
+
# class CatalogShares < BlockGiven::Contract
|
|
96
|
+
# abi_file "CatalogShares.json"
|
|
97
|
+
# end
|
|
37
98
|
def abi_file(path)
|
|
38
99
|
base = BlockGiven.config.abi_path
|
|
39
100
|
path = File.join(base.to_s, path.to_s) if base && !File.absolute_path?(path.to_s)
|
|
@@ -42,7 +103,19 @@ module BlockGiven
|
|
|
42
103
|
abi(File.read(path))
|
|
43
104
|
end
|
|
44
105
|
|
|
45
|
-
#
|
|
106
|
+
# Sets or returns the default address used by {.new} when no `address:` is given.
|
|
107
|
+
#
|
|
108
|
+
# Instances can still target another deployment with `.new(address: ...)` or {.at}.
|
|
109
|
+
#
|
|
110
|
+
# @param value [String, #address, nil] the contract address (checksummed on storage); `nil` to read
|
|
111
|
+
# @return [String, nil] the checksummed default address, or `nil` when none is declared
|
|
112
|
+
# @raise [BlockGiven::InvalidAddressError] when `value` is not a valid address
|
|
113
|
+
# @example
|
|
114
|
+
# class Usdc < BlockGiven::Contract
|
|
115
|
+
# abi_file "erc20.json"
|
|
116
|
+
# address "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
117
|
+
# end
|
|
118
|
+
# Usdc.address # => "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
46
119
|
def address(value = nil)
|
|
47
120
|
return @default_address if value.nil?
|
|
48
121
|
|
|
@@ -50,18 +123,55 @@ module BlockGiven
|
|
|
50
123
|
end
|
|
51
124
|
alias default_address address
|
|
52
125
|
|
|
126
|
+
# Sets or returns the default chain used to build a {BlockGiven::Client} for instances.
|
|
127
|
+
#
|
|
128
|
+
# When no chain is declared, instances use the wallet's client or the global `BlockGiven.client`
|
|
129
|
+
# (see {#client}).
|
|
130
|
+
#
|
|
131
|
+
# @param value [Chain, Symbol, String, Integer, nil] a {Chain}, a network name (`:base`,
|
|
132
|
+
# `"base-sepolia"`) or a chain id (`8453`); `nil` to read
|
|
133
|
+
# @return [Chain, nil] the resolved default chain, or `nil` when none is declared
|
|
134
|
+
# @raise [BlockGiven::ConfigurationError] when the chain is unknown
|
|
135
|
+
# @example
|
|
136
|
+
# class Usdc < BlockGiven::Contract
|
|
137
|
+
# abi_file "erc20.json"
|
|
138
|
+
# chain :base
|
|
139
|
+
# end
|
|
53
140
|
def chain(value = nil)
|
|
54
141
|
return @chain if value.nil?
|
|
55
142
|
|
|
56
143
|
@chain = Chains.resolve(value)
|
|
57
144
|
end
|
|
58
145
|
|
|
146
|
+
# Builds an instance bound to a specific address; shorthand for `new(address: address, **options)`.
|
|
147
|
+
#
|
|
148
|
+
# @param address [String, #address] the contract address
|
|
149
|
+
# @param options [Hash] the other {#initialize} keywords (`wallet:`, `client:`, `chain:`)
|
|
150
|
+
# @return [Contract] a new instance of this contract class
|
|
151
|
+
# @example
|
|
152
|
+
# weth = Erc20.at("0x4200000000000000000000000000000000000006", chain: :base)
|
|
59
153
|
def at(address, **options) = new(address: address, **options)
|
|
60
154
|
|
|
155
|
+
# ABI functions of the declared interface.
|
|
156
|
+
#
|
|
157
|
+
# @return [Array<Abi::Function>] empty when no ABI is declared
|
|
61
158
|
def functions = interface&.functions || []
|
|
159
|
+
|
|
160
|
+
# ABI events of the declared interface.
|
|
161
|
+
#
|
|
162
|
+
# @return [Array<Abi::Event>] empty when no ABI is declared
|
|
62
163
|
def events = interface&.events || []
|
|
164
|
+
|
|
165
|
+
# ABI custom errors of the declared interface.
|
|
166
|
+
#
|
|
167
|
+
# @return [Array<Abi::CustomError>] empty when no ABI is declared
|
|
63
168
|
def errors = interface&.errors || []
|
|
64
169
|
|
|
170
|
+
# Propagates the ABI, default address and chain to subclasses and defines their dynamic methods.
|
|
171
|
+
#
|
|
172
|
+
# @api private
|
|
173
|
+
# @param subclass [Class] the inheriting class
|
|
174
|
+
# @return [void]
|
|
65
175
|
def inherited(subclass)
|
|
66
176
|
super
|
|
67
177
|
subclass.instance_variable_set(:@interface, @interface)
|
|
@@ -72,8 +182,16 @@ module BlockGiven
|
|
|
72
182
|
|
|
73
183
|
private
|
|
74
184
|
|
|
75
|
-
#
|
|
76
|
-
#
|
|
185
|
+
# Defines one instance method per ABI function name, converted to snake_case.
|
|
186
|
+
#
|
|
187
|
+
# Each method has the signature `(*args, tx: {}, **kwargs)` and dispatches to {#read} for
|
|
188
|
+
# `view` / `pure` functions and to {#write} otherwise, resolving overloads through
|
|
189
|
+
# {Abi::Interface#function}. Names already defined on `Contract` (`send`, `class`, `address`...) are
|
|
190
|
+
# skipped with a debug log entry; those functions stay reachable through {#read} / {#write}.
|
|
191
|
+
# Called by {.abi} and {.inherited}.
|
|
192
|
+
#
|
|
193
|
+
# @api private
|
|
194
|
+
# @return [void]
|
|
77
195
|
def define_abi_methods!
|
|
78
196
|
interface.function_names.each do |ruby_name|
|
|
79
197
|
if Contract.method_defined?(ruby_name) || Contract.private_method_defined?(ruby_name)
|
|
@@ -95,8 +213,29 @@ module BlockGiven
|
|
|
95
213
|
end
|
|
96
214
|
end
|
|
97
215
|
|
|
216
|
+
# @!attribute [r] address
|
|
217
|
+
# The checksummed address this instance targets.
|
|
218
|
+
# @return [String]
|
|
219
|
+
# @!attribute [r] wallet
|
|
220
|
+
# The wallet used to sign writes and as default `from` for calls.
|
|
221
|
+
# @return [BlockGiven::Wallet, nil] `nil` for read-only instances
|
|
98
222
|
attr_reader :address, :wallet
|
|
99
223
|
|
|
224
|
+
# Binds the class ABI to an address, and optionally to a wallet, client or chain.
|
|
225
|
+
#
|
|
226
|
+
# The client is resolved lazily by {#client}: an explicit `client:` wins, then the wallet's client
|
|
227
|
+
# (when it runs on the requested chain), then a client for `chain:` / {Contract.chain}, then the
|
|
228
|
+
# global `BlockGiven.client`.
|
|
229
|
+
#
|
|
230
|
+
# @param address [String, #address, nil] the contract address; defaults to {Contract.address}
|
|
231
|
+
# @param wallet [BlockGiven::Wallet, nil] wallet used to sign writes (required by {#write} and
|
|
232
|
+
# {#prepare_write})
|
|
233
|
+
# @param client [BlockGiven::Client, nil] explicit JSON-RPC client
|
|
234
|
+
# @param chain [Chain, Symbol, String, Integer, nil] chain to build a client for when no `client:`
|
|
235
|
+
# is given; defaults to {Contract.chain}
|
|
236
|
+
# @raise [BlockGiven::AbiError] when the class has no ABI
|
|
237
|
+
# @raise [BlockGiven::InvalidArgumentError] when neither `address:` nor a default address is available
|
|
238
|
+
# @raise [BlockGiven::InvalidAddressError] when the address is malformed
|
|
100
239
|
def initialize(address: nil, wallet: nil, client: nil, chain: nil)
|
|
101
240
|
raise AbiError, "#{self.class.name} has no ABI: declare it with `abi [...]` or `abi_file`" unless interface
|
|
102
241
|
|
|
@@ -109,8 +248,20 @@ module BlockGiven
|
|
|
109
248
|
@chain = chain
|
|
110
249
|
end
|
|
111
250
|
|
|
251
|
+
# The parsed ABI of this contract class.
|
|
252
|
+
#
|
|
253
|
+
# @return [Abi::Interface]
|
|
112
254
|
def interface = self.class.interface
|
|
113
255
|
|
|
256
|
+
# The JSON-RPC client used for calls, gas estimation and logs (memoized).
|
|
257
|
+
#
|
|
258
|
+
# Resolution order: the `client:` given to {#initialize}; the wallet's client when no chain is
|
|
259
|
+
# requested or when it already runs on that chain; a new {BlockGiven::Client} for the requested
|
|
260
|
+
# chain (`chain:` or {Contract.chain}); otherwise the global `BlockGiven.client`.
|
|
261
|
+
#
|
|
262
|
+
# @return [BlockGiven::Client]
|
|
263
|
+
# @raise [BlockGiven::ConfigurationError] when a chain name cannot be resolved, or when no chain is
|
|
264
|
+
# configured anywhere
|
|
114
265
|
def client
|
|
115
266
|
@client ||= begin
|
|
116
267
|
chain = @chain || self.class.chain
|
|
@@ -124,12 +275,50 @@ module BlockGiven
|
|
|
124
275
|
end
|
|
125
276
|
end
|
|
126
277
|
|
|
278
|
+
# The chain of the resolved {#client}.
|
|
279
|
+
#
|
|
280
|
+
# @return [Chain]
|
|
127
281
|
def chain = client.chain
|
|
282
|
+
|
|
283
|
+
# Returns a copy of this contract bound to another wallet, keeping the address, client and chain.
|
|
284
|
+
#
|
|
285
|
+
# @param wallet [BlockGiven::Wallet, nil] the wallet to sign with
|
|
286
|
+
# @return [Contract] a new instance of the same class
|
|
128
287
|
def with_wallet(wallet) = self.class.new(address: address, wallet: wallet, client: @client, chain: @chain)
|
|
129
288
|
|
|
130
289
|
# --- Reads / writes -----------------------------------------------------
|
|
131
290
|
|
|
132
|
-
# eth_call
|
|
291
|
+
# Runs `eth_call` for a function and returns its decoded output.
|
|
292
|
+
#
|
|
293
|
+
# Only `from:` and `block:` are meaningful here; the other {TX_OPTIONS} keys are validated but ignored.
|
|
294
|
+
# A single ABI output is returned unwrapped, several outputs as an Array (see
|
|
295
|
+
# {Abi::Function#decode_output}).
|
|
296
|
+
#
|
|
297
|
+
# @param name [String, Symbol, Abi::Function] function name (snake_case or camelCase), full signature
|
|
298
|
+
# (`"balanceOf(address)"`) or an already resolved {Abi::Function}
|
|
299
|
+
# @param args [Array] positional ABI inputs (exclusive with `kwargs`)
|
|
300
|
+
# @param tx [Hash] call overrides; keys must belong to {TX_OPTIONS}
|
|
301
|
+
# @option tx [Integer] :from address used as `msg.sender` for the call (default: the wallet address,
|
|
302
|
+
# when any)
|
|
303
|
+
# @option tx [Integer, Symbol, String] :block block number or tag (`:latest`, `:safe`, `:finalized`,
|
|
304
|
+
# `:pending`, `:earliest`) the call is evaluated at (default: `:latest`)
|
|
305
|
+
# @option tx [Integer] :value wei to send; accepted for uniformity, ignored by reads
|
|
306
|
+
# @option tx [Integer] :gas gas limit; accepted for uniformity, ignored by reads
|
|
307
|
+
# @option tx [Integer] :nonce transaction nonce; accepted for uniformity, ignored by reads
|
|
308
|
+
# @option tx [Integer] :max_fee_per_gas EIP-1559 max fee; accepted for uniformity, ignored by reads
|
|
309
|
+
# @option tx [Integer] :max_priority_fee_per_gas EIP-1559 priority fee; accepted for uniformity,
|
|
310
|
+
# ignored by reads
|
|
311
|
+
# @option tx [Integer] :gas_price legacy gas price; accepted for uniformity, ignored by reads
|
|
312
|
+
# @param kwargs [Hash] ABI inputs by snake_cased name (exclusive with `args`)
|
|
313
|
+
# @return [Object, Array<Object>] the decoded output: a single value, or an Array for several outputs
|
|
314
|
+
# @raise [BlockGiven::FunctionNotFoundError] when the name (or overload) is not in the ABI
|
|
315
|
+
# @raise [BlockGiven::AmbiguousFunctionError] when several overloads match
|
|
316
|
+
# @raise [BlockGiven::InvalidArgumentError] on unknown `tx:` keys or malformed arguments
|
|
317
|
+
# @raise [BlockGiven::ContractRevertError] when the call reverts; custom errors are decoded with the ABI
|
|
318
|
+
# @example
|
|
319
|
+
# usdc.read(:balance_of, wallet.address)
|
|
320
|
+
# usdc.read("balanceOf(address)", wallet.address, tx: { block: 18_000_000 })
|
|
321
|
+
# token.read(:send, to, amount) # ABI function whose name clashes with Object#send
|
|
133
322
|
def read(name, *args, tx: {}, **kwargs)
|
|
134
323
|
function = resolve(name, args, kwargs)
|
|
135
324
|
data = function.encode(args, kwargs)
|
|
@@ -140,14 +329,72 @@ module BlockGiven
|
|
|
140
329
|
function.decode_output(raw)
|
|
141
330
|
end
|
|
142
331
|
|
|
143
|
-
# Signs and broadcasts
|
|
332
|
+
# Signs and broadcasts a function call; shorthand for `prepare_write(...).broadcast`.
|
|
333
|
+
#
|
|
334
|
+
# The sender is always the wallet: `from:` and `block:` are validated but ignored.
|
|
335
|
+
#
|
|
336
|
+
# @param name [String, Symbol, Abi::Function] function name (snake_case or camelCase), full signature
|
|
337
|
+
# or an already resolved {Abi::Function}
|
|
338
|
+
# @param args [Array] positional ABI inputs (exclusive with `kwargs`)
|
|
339
|
+
# @param tx [Hash] transaction overrides; keys must belong to {TX_OPTIONS}
|
|
340
|
+
# @option tx [Integer] :value wei to send along, payable functions only (default: 0)
|
|
341
|
+
# @option tx [Integer] :gas gas limit (default: estimated by the wallet)
|
|
342
|
+
# @option tx [Integer] :nonce nonce to use (default: the wallet's pending nonce)
|
|
343
|
+
# @option tx [Integer] :max_fee_per_gas EIP-1559 max fee per gas in wei (default: estimated)
|
|
344
|
+
# @option tx [Integer] :max_priority_fee_per_gas EIP-1559 priority fee per gas in wei (default: estimated)
|
|
345
|
+
# @option tx [Integer] :gas_price gas price in wei; builds a legacy (type 0) transaction instead of
|
|
346
|
+
# EIP-1559
|
|
347
|
+
# @option tx [String] :from ignored, the sender is always the wallet
|
|
348
|
+
# @option tx [Integer, Symbol, String] :block ignored by writes
|
|
349
|
+
# @param kwargs [Hash] ABI inputs by snake_cased name (exclusive with `args`)
|
|
350
|
+
# @return [BlockGiven::Transaction] handle on the broadcast transaction (`#wait!` polls the receipt)
|
|
351
|
+
# @raise [BlockGiven::WalletRequiredError] when the instance has no wallet
|
|
352
|
+
# @raise [BlockGiven::InvalidArgumentError] when `value:` is given for a non-payable function, on
|
|
353
|
+
# unknown `tx:` keys or malformed arguments
|
|
354
|
+
# @raise [BlockGiven::FunctionNotFoundError] when the name (or overload) is not in the ABI
|
|
355
|
+
# @raise [BlockGiven::AmbiguousFunctionError] when several overloads match
|
|
356
|
+
# @raise [BlockGiven::ContractRevertError] when gas estimation or broadcast reverts; custom errors are
|
|
357
|
+
# decoded with the ABI
|
|
358
|
+
# @example
|
|
359
|
+
# tx = usdc.write(:transfer, to: recipient, value: 1_000_000)
|
|
360
|
+
# tx.wait!
|
|
144
361
|
def write(name, *args, tx: {}, **kwargs)
|
|
145
362
|
prepare_write(name, *args, tx: tx, **kwargs).broadcast
|
|
146
363
|
end
|
|
147
364
|
|
|
148
|
-
# Signs
|
|
149
|
-
#
|
|
150
|
-
#
|
|
365
|
+
# Signs a function call without broadcasting it.
|
|
366
|
+
#
|
|
367
|
+
# The returned {BlockGiven::SignedTransaction} knows its `#hash` and `#nonce` before any network call:
|
|
368
|
+
# persist them, then call `#broadcast`. The signed transaction keeps this contract's ABI, so reverts
|
|
369
|
+
# raised by `#broadcast` are decoded too. The sender is always the wallet: `from:` and `block:` are
|
|
370
|
+
# validated but ignored.
|
|
371
|
+
#
|
|
372
|
+
# @param name [String, Symbol, Abi::Function] function name (snake_case or camelCase), full signature
|
|
373
|
+
# or an already resolved {Abi::Function}
|
|
374
|
+
# @param args [Array] positional ABI inputs (exclusive with `kwargs`)
|
|
375
|
+
# @param tx [Hash] transaction overrides; keys must belong to {TX_OPTIONS}
|
|
376
|
+
# @option tx [Integer] :value wei to send along, payable functions only (default: 0)
|
|
377
|
+
# @option tx [Integer] :gas gas limit (default: estimated by the wallet)
|
|
378
|
+
# @option tx [Integer] :nonce nonce to use (default: the wallet's pending nonce)
|
|
379
|
+
# @option tx [Integer] :max_fee_per_gas EIP-1559 max fee per gas in wei (default: estimated)
|
|
380
|
+
# @option tx [Integer] :max_priority_fee_per_gas EIP-1559 priority fee per gas in wei (default: estimated)
|
|
381
|
+
# @option tx [Integer] :gas_price gas price in wei; builds a legacy (type 0) transaction instead of
|
|
382
|
+
# EIP-1559
|
|
383
|
+
# @option tx [String] :from ignored, the sender is always the wallet
|
|
384
|
+
# @option tx [Integer, Symbol, String] :block ignored by writes
|
|
385
|
+
# @param kwargs [Hash] ABI inputs by snake_cased name (exclusive with `args`)
|
|
386
|
+
# @return [BlockGiven::SignedTransaction] the signed, not yet broadcast transaction
|
|
387
|
+
# @raise [BlockGiven::WalletRequiredError] when the instance has no wallet
|
|
388
|
+
# @raise [BlockGiven::InvalidArgumentError] when `value:` is given for a non-payable function, on
|
|
389
|
+
# unknown `tx:` keys or malformed arguments
|
|
390
|
+
# @raise [BlockGiven::FunctionNotFoundError] when the name (or overload) is not in the ABI
|
|
391
|
+
# @raise [BlockGiven::AmbiguousFunctionError] when several overloads match
|
|
392
|
+
# @raise [BlockGiven::ContractRevertError] when gas estimation reverts; custom errors are decoded with
|
|
393
|
+
# the ABI
|
|
394
|
+
# @example Persist the hash before broadcasting
|
|
395
|
+
# signed = registry.prepare_write(:record, movement_id, tx: { nonce: call.nonce })
|
|
396
|
+
# call.update!(tx_hash: signed.hash, nonce: signed.nonce, status: :submitted)
|
|
397
|
+
# signed.broadcast # => BlockGiven::Transaction
|
|
151
398
|
def prepare_write(name, *args, tx: {}, **kwargs)
|
|
152
399
|
function = resolve(name, args, kwargs)
|
|
153
400
|
unless wallet
|
|
@@ -171,8 +418,40 @@ module BlockGiven
|
|
|
171
418
|
end
|
|
172
419
|
end
|
|
173
420
|
|
|
174
|
-
# Dry-runs a
|
|
175
|
-
# result.
|
|
421
|
+
# Dry-runs a function with `eth_call` (from the wallet address by default) and returns the decoded
|
|
422
|
+
# result.
|
|
423
|
+
#
|
|
424
|
+
# Nothing is signed or broadcast. `from:`, `value:`, `gas:` and `block:` are forwarded to the call;
|
|
425
|
+
# the fee and nonce keys are validated but ignored. Unlike {#prepare_write}, `value:` is not checked
|
|
426
|
+
# against the function's mutability.
|
|
427
|
+
#
|
|
428
|
+
# @param name [String, Symbol, Abi::Function] function name (snake_case or camelCase), full signature
|
|
429
|
+
# or an already resolved {Abi::Function}
|
|
430
|
+
# @param args [Array] positional ABI inputs (exclusive with `kwargs`)
|
|
431
|
+
# @param tx [Hash] call overrides; keys must belong to {TX_OPTIONS}
|
|
432
|
+
# @option tx [String] :from address used as `msg.sender` (default: the wallet address, when any)
|
|
433
|
+
# @option tx [Integer] :value wei sent with the simulated call
|
|
434
|
+
# @option tx [Integer] :gas gas limit of the simulated call
|
|
435
|
+
# @option tx [Integer, Symbol, String] :block block number or tag the call is evaluated at
|
|
436
|
+
# (default: `:latest`)
|
|
437
|
+
# @option tx [Integer] :nonce accepted for uniformity, ignored by simulations
|
|
438
|
+
# @option tx [Integer] :max_fee_per_gas accepted for uniformity, ignored by simulations
|
|
439
|
+
# @option tx [Integer] :max_priority_fee_per_gas accepted for uniformity, ignored by simulations
|
|
440
|
+
# @option tx [Integer] :gas_price accepted for uniformity, ignored by simulations
|
|
441
|
+
# @param kwargs [Hash] ABI inputs by snake_cased name (exclusive with `args`)
|
|
442
|
+
# @return [Object, Array<Object>] the decoded output: a single value, or an Array for several outputs
|
|
443
|
+
# @raise [BlockGiven::FunctionNotFoundError] when the name (or overload) is not in the ABI
|
|
444
|
+
# @raise [BlockGiven::AmbiguousFunctionError] when several overloads match
|
|
445
|
+
# @raise [BlockGiven::InvalidArgumentError] on unknown `tx:` keys or malformed arguments
|
|
446
|
+
# @raise [BlockGiven::ContractRevertError] when the call reverts, with the reason decoded
|
|
447
|
+
# (`Error(string)`, `Panic` or an ABI custom error)
|
|
448
|
+
# @example
|
|
449
|
+
# begin
|
|
450
|
+
# usdc.simulate(:transfer, to: recipient, value: 1_000_000)
|
|
451
|
+
# rescue BlockGiven::ContractRevertError => e
|
|
452
|
+
# e.error_name # => "ERC20InsufficientBalance"
|
|
453
|
+
# e.args # => { sender: "0x...", balance: 0, needed: 1000000 }
|
|
454
|
+
# end
|
|
176
455
|
def simulate(name, *args, tx: {}, **kwargs)
|
|
177
456
|
function = resolve(name, args, kwargs)
|
|
178
457
|
options = tx_options(tx)
|
|
@@ -184,6 +463,32 @@ module BlockGiven
|
|
|
184
463
|
function.decode_output(raw)
|
|
185
464
|
end
|
|
186
465
|
|
|
466
|
+
# Estimates the gas needed by a function call with `eth_estimateGas`.
|
|
467
|
+
#
|
|
468
|
+
# `from:` and `value:` are forwarded to the estimation; the other {TX_OPTIONS} keys are validated but
|
|
469
|
+
# ignored.
|
|
470
|
+
#
|
|
471
|
+
# @param name [String, Symbol, Abi::Function] function name (snake_case or camelCase), full signature
|
|
472
|
+
# or an already resolved {Abi::Function}
|
|
473
|
+
# @param args [Array] positional ABI inputs (exclusive with `kwargs`)
|
|
474
|
+
# @param tx [Hash] call overrides; keys must belong to {TX_OPTIONS}
|
|
475
|
+
# @option tx [String] :from address used as `msg.sender` (default: the wallet address, when any)
|
|
476
|
+
# @option tx [Integer] :value wei sent with the estimated call
|
|
477
|
+
# @option tx [Integer] :gas accepted for uniformity, ignored by estimations
|
|
478
|
+
# @option tx [Integer] :nonce accepted for uniformity, ignored by estimations
|
|
479
|
+
# @option tx [Integer] :max_fee_per_gas accepted for uniformity, ignored by estimations
|
|
480
|
+
# @option tx [Integer] :max_priority_fee_per_gas accepted for uniformity, ignored by estimations
|
|
481
|
+
# @option tx [Integer] :gas_price accepted for uniformity, ignored by estimations
|
|
482
|
+
# @option tx [Integer, Symbol, String] :block accepted for uniformity, ignored by estimations
|
|
483
|
+
# @param kwargs [Hash] ABI inputs by snake_cased name (exclusive with `args`)
|
|
484
|
+
# @return [Integer] the estimated gas
|
|
485
|
+
# @raise [BlockGiven::FunctionNotFoundError] when the name (or overload) is not in the ABI
|
|
486
|
+
# @raise [BlockGiven::AmbiguousFunctionError] when several overloads match
|
|
487
|
+
# @raise [BlockGiven::InvalidArgumentError] on unknown `tx:` keys or malformed arguments
|
|
488
|
+
# @raise [BlockGiven::ContractRevertError] when the estimated call reverts; custom errors are decoded
|
|
489
|
+
# with the ABI
|
|
490
|
+
# @example
|
|
491
|
+
# usdc.estimate_gas(:transfer, to: recipient, value: 1_000_000) # => 51_234
|
|
187
492
|
def estimate_gas(name, *args, tx: {}, **kwargs)
|
|
188
493
|
function = resolve(name, args, kwargs)
|
|
189
494
|
options = tx_options(tx)
|
|
@@ -193,19 +498,51 @@ module BlockGiven
|
|
|
193
498
|
end
|
|
194
499
|
end
|
|
195
500
|
|
|
501
|
+
# ABI-encodes a function call (selector + arguments) without sending anything.
|
|
502
|
+
#
|
|
503
|
+
# @param name [String, Symbol, Abi::Function] function name, full signature or {Abi::Function}
|
|
504
|
+
# @param args [Array] positional ABI inputs (exclusive with `kwargs`)
|
|
505
|
+
# @param kwargs [Hash] ABI inputs by snake_cased name (exclusive with `args`)
|
|
506
|
+
# @return [String] `0x`-prefixed calldata
|
|
507
|
+
# @raise [BlockGiven::FunctionNotFoundError] when the name (or overload) is not in the ABI
|
|
508
|
+
# @raise [BlockGiven::AmbiguousFunctionError] when several overloads match
|
|
509
|
+
# @raise [BlockGiven::InvalidArgumentError] when the arguments do not match the ABI inputs
|
|
510
|
+
# @raise [BlockGiven::AbiError] when the ABI encoder rejects a value
|
|
196
511
|
def encode_function_data(name, *args, **kwargs)
|
|
197
512
|
resolve(name, args, kwargs).encode(args, kwargs)
|
|
198
513
|
end
|
|
199
514
|
|
|
515
|
+
# Decodes the raw return data of a function call.
|
|
516
|
+
#
|
|
517
|
+
# @param name [String, Symbol] function name or full signature; overloads must be given as a signature
|
|
518
|
+
# @param hex [String] the `0x`-prefixed return data
|
|
519
|
+
# @return [Object, Array<Object>] a single decoded value, or an Array for several outputs
|
|
520
|
+
# @raise [BlockGiven::FunctionNotFoundError] when the name is not in the ABI
|
|
521
|
+
# @raise [BlockGiven::AmbiguousFunctionError] when the name is overloaded
|
|
522
|
+
# @raise [BlockGiven::AbiError] when the data is empty or cannot be decoded
|
|
200
523
|
def decode_function_result(name, hex)
|
|
201
524
|
interface.function(name).decode_output(hex)
|
|
202
525
|
end
|
|
203
526
|
|
|
204
527
|
# --- Events -------------------------------------------------------------
|
|
205
528
|
|
|
206
|
-
# Fetches past events
|
|
529
|
+
# Fetches past events emitted by this contract and decodes them.
|
|
530
|
+
#
|
|
531
|
+
# @param name [String, Symbol, nil] event name (snake_case or camelCase) or signature; `nil` fetches
|
|
532
|
+
# every log of the contract (logs with a topic unknown to the ABI are skipped)
|
|
533
|
+
# @param from_block [Integer, Symbol, String] first block of the range (number or tag)
|
|
534
|
+
# @param to_block [Integer, Symbol, String] last block of the range (default: `:latest`)
|
|
535
|
+
# @param args [Hash] filters on indexed parameters, by snake_cased name: `nil` is a wildcard, an Array
|
|
536
|
+
# matches any of its values (see {Abi::Event#encode_topics})
|
|
537
|
+
# @param max_block_range [Integer, nil] split the range into several `eth_getLogs` calls of at most
|
|
538
|
+
# this many blocks (providers cap the range)
|
|
539
|
+
# @return [Array<BlockGiven::Event>] decoded events, oldest first
|
|
540
|
+
# @raise [BlockGiven::EventNotFoundError] when `name` is not an event of the ABI
|
|
541
|
+
# @raise [BlockGiven::InvalidArgumentError] when `args` names a non-indexed parameter
|
|
542
|
+
# @raise [BlockGiven::RpcError] when the node rejects the request
|
|
543
|
+
# @example
|
|
207
544
|
# usdc.get_events(:Transfer, from_block: 18_000_000, to_block: :latest, args: { to: wallet.address })
|
|
208
|
-
#
|
|
545
|
+
# usdc.get_events(from_block: 18_000_000, max_block_range: 2_000) # every known event, chunked
|
|
209
546
|
def get_events(name = nil, from_block:, to_block: :latest, args: {}, max_block_range: nil)
|
|
210
547
|
topics = name ? interface.event(name).encode_topics(args) : nil
|
|
211
548
|
logs = if max_block_range
|
|
@@ -217,13 +554,38 @@ module BlockGiven
|
|
|
217
554
|
decode_logs(logs)
|
|
218
555
|
end
|
|
219
556
|
|
|
220
|
-
# Polls for new events in a background thread
|
|
557
|
+
# Polls for new events in a background thread and yields each decoded event.
|
|
558
|
+
#
|
|
559
|
+
# Resuming after a restart: pass `from_block:` (your persisted cursor + 1) and persist the `to` block
|
|
560
|
+
# handed to `on_progress` after each processed range. `confirmations:` keeps the watcher N blocks
|
|
561
|
+
# behind the head so reorged logs are never delivered. The watcher is registered by id and can be
|
|
562
|
+
# stopped with `watcher.stop` or `BlockGiven::Watcher.stop(id)`.
|
|
563
|
+
#
|
|
564
|
+
# @param name [String, Symbol, nil] event name or signature; `nil` watches every event known to the ABI
|
|
565
|
+
# @param args [Hash] filters on indexed parameters, by snake_cased name (see {Abi::Event#encode_topics})
|
|
566
|
+
# @param from_block [Integer, nil] first block to process (default: start from the current head)
|
|
567
|
+
# @param polling_interval [Numeric, nil] seconds between ticks (default: the client's interval)
|
|
568
|
+
# @param max_block_range [Integer, nil] maximum blocks per `eth_getLogs` call while catching up
|
|
569
|
+
# (default: `BlockGiven.config.max_block_range`)
|
|
570
|
+
# @param confirmations [Integer] number of blocks to stay behind the head (default: 0)
|
|
571
|
+
# @param on_progress [#call, nil] called with `(from, to)` after each processed block range
|
|
572
|
+
# @param id [String, nil] stable watcher id for `BlockGiven::Watcher.find` / `.stop`
|
|
573
|
+
# (default: generated)
|
|
574
|
+
# @yield [event] once per decoded event, in log order, from the watcher thread
|
|
575
|
+
# @yieldparam event [BlockGiven::Event] the decoded event
|
|
576
|
+
# @return [BlockGiven::Watcher] the started watcher
|
|
577
|
+
# @raise [ArgumentError] when no block is given
|
|
578
|
+
# @raise [BlockGiven::EventNotFoundError] when `name` is not an event of the ABI
|
|
579
|
+
# @raise [BlockGiven::InvalidArgumentError] when `args` names a non-indexed parameter, or when a
|
|
580
|
+
# watcher with the same `id` is already running
|
|
581
|
+
# @example
|
|
221
582
|
# watcher = usdc.watch_event(:Transfer, args: { to: me }) { |event| puts event.args }
|
|
222
583
|
# watcher.stop
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
584
|
+
# @example Resumable watcher
|
|
585
|
+
# usdc.watch_event(:Transfer, from_block: cursor.last_block + 1, confirmations: 3,
|
|
586
|
+
# on_progress: ->(_from, to) { cursor.update!(last_block: to) }) do |event|
|
|
587
|
+
# Deposit.record!(event)
|
|
588
|
+
# end
|
|
227
589
|
def watch_event(name = nil, args: {}, from_block: nil, polling_interval: nil, max_block_range: nil,
|
|
228
590
|
confirmations: 0, on_progress: nil, id: nil, &block)
|
|
229
591
|
raise ::ArgumentError, "a block is required" unless block
|
|
@@ -239,7 +601,17 @@ module BlockGiven
|
|
|
239
601
|
end
|
|
240
602
|
alias watch_events watch_event
|
|
241
603
|
|
|
242
|
-
# Decodes raw logs with this contract's ABI.
|
|
604
|
+
# Decodes raw logs with this contract's ABI.
|
|
605
|
+
#
|
|
606
|
+
# Logs are normalized first when they are not already snake_case Hashes. Logs whose first topic does
|
|
607
|
+
# not match an event of the ABI are skipped. The log address is not checked; use {#events_from} for
|
|
608
|
+
# that.
|
|
609
|
+
#
|
|
610
|
+
# @param logs [Array<Hash>] raw JSON-RPC logs or normalized log Hashes (with `:topics` and `:data`)
|
|
611
|
+
# @return [Array<BlockGiven::Event>] the decoded events, in input order
|
|
612
|
+
# @raise [BlockGiven::AbiError] when a matching log carries data that cannot be decoded
|
|
613
|
+
# @example
|
|
614
|
+
# usdc.decode_logs(receipt.logs)
|
|
243
615
|
def decode_logs(logs)
|
|
244
616
|
logs.filter_map do |log|
|
|
245
617
|
log = Normalizer.normalize(log) unless log.is_a?(Hash) && log.key?(:topics)
|
|
@@ -249,22 +621,45 @@ module BlockGiven
|
|
|
249
621
|
end
|
|
250
622
|
end
|
|
251
623
|
|
|
252
|
-
#
|
|
624
|
+
# Decodes the events emitted by this contract in a transaction receipt.
|
|
625
|
+
#
|
|
626
|
+
# Logs emitted by other addresses are ignored.
|
|
627
|
+
#
|
|
628
|
+
# @param receipt [BlockGiven::Receipt, Hash] a receipt object responding to `#logs`, or a normalized
|
|
629
|
+
# receipt Hash with a `:logs` key
|
|
630
|
+
# @return [Array<BlockGiven::Event>] the decoded events, in log order
|
|
631
|
+
# @example
|
|
632
|
+
# tx = usdc.transfer(to: recipient, value: 1_000_000)
|
|
633
|
+
# usdc.events_from(tx.wait!).map(&:name) # => ["Transfer"]
|
|
253
634
|
def events_from(receipt)
|
|
254
635
|
logs = receipt.respond_to?(:logs) ? receipt.logs : Array(receipt[:logs])
|
|
255
636
|
decode_logs(logs.select { |l| Utils.same_address?(l[:address], address) })
|
|
256
637
|
end
|
|
257
638
|
|
|
639
|
+
# Block explorer URL of this contract on its chain.
|
|
640
|
+
#
|
|
641
|
+
# @return [String, nil] `nil` when the chain has no explorer configured
|
|
258
642
|
def explorer_url = chain.explorer_address_url(address)
|
|
643
|
+
|
|
644
|
+
# Two contracts are equal when they share the same class and address (the wallet is not compared).
|
|
645
|
+
#
|
|
646
|
+
# @param other [Object]
|
|
647
|
+
# @return [Boolean]
|
|
259
648
|
def ==(other) = other.class == self.class && other.address == address
|
|
649
|
+
|
|
650
|
+
# Compact representation showing the class, the address and the wallet address (never its key).
|
|
651
|
+
#
|
|
652
|
+
# @return [String]
|
|
260
653
|
def inspect = "#<#{self.class.name} #{address}#{" wallet=#{wallet.address}" if wallet}>"
|
|
261
654
|
|
|
262
655
|
private
|
|
263
656
|
|
|
657
|
+
# Resolves a name, signature or Function into an Abi::Function using the call arguments for overloads.
|
|
264
658
|
def resolve(name, args, kwargs)
|
|
265
659
|
name.is_a?(Abi::Function) ? name : interface.function(name, args: args, kwargs: kwargs)
|
|
266
660
|
end
|
|
267
661
|
|
|
662
|
+
# Validates the tx: Hash against TX_OPTIONS and returns it with symbol keys.
|
|
268
663
|
def tx_options(tx)
|
|
269
664
|
raise InvalidArgumentError, "tx: must be a Hash" unless tx.is_a?(Hash)
|
|
270
665
|
|
|
@@ -278,6 +673,7 @@ module BlockGiven
|
|
|
278
673
|
options
|
|
279
674
|
end
|
|
280
675
|
|
|
676
|
+
# Re-raises ContractRevertError enriched with custom errors decoded from this contract's ABI.
|
|
281
677
|
def with_decoded_errors
|
|
282
678
|
yield
|
|
283
679
|
rescue ContractRevertError => e
|