block_given 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -1
  3. data/README.md +92 -55
  4. data/lib/block_given/abi/codec/decoder.rb +111 -0
  5. data/lib/block_given/abi/codec.rb +170 -0
  6. data/lib/block_given/abi/coder.rb +122 -11
  7. data/lib/block_given/abi/custom_error.rb +33 -1
  8. data/lib/block_given/abi/event.rb +85 -3
  9. data/lib/block_given/abi/function.rb +94 -2
  10. data/lib/block_given/abi/interface.rb +99 -4
  11. data/lib/block_given/abi/parameter.rb +60 -3
  12. data/lib/block_given/abi/standards/erc1155.rb +41 -0
  13. data/lib/block_given/abi/standards/erc20.rb +33 -0
  14. data/lib/block_given/abi/standards/erc4626.rb +41 -0
  15. data/lib/block_given/abi/standards/erc721.rb +47 -0
  16. data/lib/block_given/abi/standards.rb +124 -0
  17. data/lib/block_given/abi/type.rb +138 -0
  18. data/lib/block_given/chain.rb +114 -2
  19. data/lib/block_given/client.rb +322 -20
  20. data/lib/block_given/configuration.rb +85 -3
  21. data/lib/block_given/connectors/alchemy.rb +38 -4
  22. data/lib/block_given/connectors/base.rb +32 -5
  23. data/lib/block_given/connectors/http.rb +91 -5
  24. data/lib/block_given/connectors/stub.rb +70 -4
  25. data/lib/block_given/contract.rb +422 -26
  26. data/lib/block_given/crypto/keccak.rb +152 -0
  27. data/lib/block_given/crypto/secp256k1.rb +168 -0
  28. data/lib/block_given/crypto.rb +22 -0
  29. data/lib/block_given/eip712.rb +199 -0
  30. data/lib/block_given/errors.rb +134 -14
  31. data/lib/block_given/event.rb +51 -1
  32. data/lib/block_given/normalizer.rb +27 -2
  33. data/lib/block_given/poller.rb +177 -15
  34. data/lib/block_given/receipt.rb +63 -3
  35. data/lib/block_given/rlp.rb +146 -0
  36. data/lib/block_given/signed_transaction.rb +148 -47
  37. data/lib/block_given/transaction.rb +103 -12
  38. data/lib/block_given/transaction_envelope/fields.rb +104 -0
  39. data/lib/block_given/transaction_envelope.rb +183 -0
  40. data/lib/block_given/utils.rb +143 -9
  41. data/lib/block_given/version.rb +2 -1
  42. data/lib/block_given/wallet.rb +216 -35
  43. data/lib/block_given.rb +67 -4
  44. metadata +19 -23
@@ -1,11 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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:
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
- # abi_file "abis/erc20.json" # your app's ABI file (see BlockGiven.config.abi_path)
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 go in the reserved `tx:` keyword:
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
- # Sets (or returns) the ABI. Accepts an Array, an artifact Hash, or a JSON String.
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
- # 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).
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
- # Default address for instances (can be overridden with .new(address: ...) / .at(...)).
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
- # One Ruby method per ABI function name (snake_case). Names clashing with
76
- # existing methods (send, class, address...) are skipped: use #read / #write.
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 + decode. Overrides: tx: { block:, from: }.
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. Returns a BlockGiven::Transaction. Overrides: tx: { value:, gas:, nonce:, fees... }.
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 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.
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 write with eth_call from the wallet address and returns the decoded
175
- # result. Raises BlockGiven::ContractRevertError with the decoded reason on failure.
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. `args` filters on indexed parameters.
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
- # Pass max_block_range: to split a large range into several eth_getLogs calls.
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. Returns a BlockGiven::Watcher.
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
- # 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.
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. Unknown topics are skipped.
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
- # Events emitted by this contract in a receipt.
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