Class: BlockGiven::Contract

Inherits:
Object
  • Object
show all
Defined in:
lib/block_given/contract.rb

Overview

Base class for typed contracts. Declare the ABI (and optionally a default address / chain) and every ABI function becomes a Ruby method:

class Usdc < BlockGiven::Contract
abi_file "abis/erc20.json"   # your app's ABI file (see BlockGiven.config.abi_path)
address "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
end

usdc = Usdc.new(wallet: BlockGiven::Wallet.new(private_key: "0x..."))
usdc.balance_of(wallet.address)                       # eth_call, decoded
tx = usdc.transfer(to: "0x...", value: 1e6)           # signed + broadcast -> BlockGiven::Transaction
tx.wait!                                              # polls the receipt

Transaction / call overrides go in the reserved tx: keyword:

vault.deposit(amount, tx: { value: BlockGiven::Utils.parse_ether("0.1"), gas: 200_000 })
token.balance_of(addr, tx: { block: 18_000_000 })

Constant Summary collapse

TX_OPTIONS =
%i[value gas nonce max_fee_per_gas max_priority_fee_per_gas gas_price from block].freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(address: nil, wallet: nil, client: nil, chain: nil) ⇒ Contract

Returns a new instance of Contract.

Raises:



100
101
102
103
104
105
106
107
108
109
110
# File 'lib/block_given/contract.rb', line 100

def initialize(address: nil, wallet: nil, client: nil, chain: nil)
  raise AbiError, "#{self.class.name} has no ABI: declare it with `abi [...]` or `abi_file`" unless interface

  resolved = address || self.class.default_address
  raise InvalidArgumentError, "#{self.class.name}: address is required" if resolved.nil?

  @address = Utils.checksum_address(resolved)
  @wallet = wallet
  @client = client
  @chain = chain
end

Class Attribute Details

.interfaceObject (readonly)

Returns the value of attribute interface.



24
25
26
# File 'lib/block_given/contract.rb', line 24

def interface
  @interface
end

Instance Attribute Details

#addressObject (readonly)

Returns the value of attribute address.



98
99
100
# File 'lib/block_given/contract.rb', line 98

def address
  @address
end

#walletObject (readonly)

Returns the value of attribute wallet.



98
99
100
# File 'lib/block_given/contract.rb', line 98

def wallet
  @wallet
end

Class Method Details

.abi(source = nil) ⇒ Object

Sets (or returns) the ABI. Accepts an Array, an artifact Hash, or a JSON String.



27
28
29
30
31
32
33
# File 'lib/block_given/contract.rb', line 27

def abi(source = nil)
  return interface&.raw if source.nil?

  @interface = Abi::Interface.parse(source)
  define_abi_methods!
  @interface
end

.abi_file(path) ⇒ Object

Loads the ABI from a JSON file. Relative paths are resolved against BlockGiven.config.abi_path when set (ABIs live in your app, not in the gem).

Raises:



37
38
39
40
41
42
43
# File 'lib/block_given/contract.rb', line 37

def abi_file(path)
  base = BlockGiven.config.abi_path
  path = File.join(base.to_s, path.to_s) if base && !File.absolute_path?(path.to_s)
  raise AbiError, "ABI file not found: #{path}" unless File.file?(path)

  abi(File.read(path))
end

.address(value = nil) ⇒ Object Also known as: default_address

Default address for instances (can be overridden with .new(address: ...) / .at(...)).



46
47
48
49
50
# File 'lib/block_given/contract.rb', line 46

def address(value = nil)
  return @default_address if value.nil?

  @default_address = Utils.checksum_address(value)
end

.at(address, **options) ⇒ Object



59
# File 'lib/block_given/contract.rb', line 59

def at(address, **options) = new(address: address, **options)

.chain(value = nil) ⇒ Object



53
54
55
56
57
# File 'lib/block_given/contract.rb', line 53

def chain(value = nil)
  return @chain if value.nil?

  @chain = Chains.resolve(value)
end

.errorsObject



63
# File 'lib/block_given/contract.rb', line 63

def errors = interface&.errors || []

.eventsObject



62
# File 'lib/block_given/contract.rb', line 62

def events = interface&.events || []

.functionsObject



61
# File 'lib/block_given/contract.rb', line 61

def functions = interface&.functions || []

.inherited(subclass) ⇒ Object



65
66
67
68
69
70
71
# File 'lib/block_given/contract.rb', line 65

def inherited(subclass)
  super
  subclass.instance_variable_set(:@interface, @interface)
  subclass.instance_variable_set(:@default_address, @default_address)
  subclass.instance_variable_set(:@chain, @chain)
  subclass.send(:define_abi_methods!) if @interface
end

Instance Method Details

#==(other) ⇒ Object



259
# File 'lib/block_given/contract.rb', line 259

def ==(other) = other.class == self.class && other.address == address

#chainObject



127
# File 'lib/block_given/contract.rb', line 127

def chain = client.chain

#clientObject



114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/block_given/contract.rb', line 114

def client
  @client ||= begin
    chain = @chain || self.class.chain
    if wallet && (chain.nil? || wallet.client.chain == Chains.resolve(chain))
      wallet.client
    elsif chain
      Client.new(chain: chain)
    else
      BlockGiven.client
    end
  end
end

#decode_function_result(name, hex) ⇒ Object



200
201
202
# File 'lib/block_given/contract.rb', line 200

def decode_function_result(name, hex)
  interface.function(name).decode_output(hex)
end

#decode_logs(logs) ⇒ Object

Decodes raw logs with this contract's ABI. Unknown topics are skipped.



243
244
245
246
247
248
249
250
# File 'lib/block_given/contract.rb', line 243

def decode_logs(logs)
  logs.filter_map do |log|
    log = Normalizer.normalize(log) unless log.is_a?(Hash) && log.key?(:topics)
    topic = Array(log[:topics]).first
    event = topic && interface.event_by_topic(topic)
    event&.decode(log)
  end
end

#encode_function_data(name, *args, **kwargs) ⇒ Object



196
197
198
# File 'lib/block_given/contract.rb', line 196

def encode_function_data(name, *args, **kwargs)
  resolve(name, args, kwargs).encode(args, kwargs)
end

#estimate_gas(name, *args, tx: {}, **kwargs) ⇒ Object



187
188
189
190
191
192
193
194
# File 'lib/block_given/contract.rb', line 187

def estimate_gas(name, *args, tx: {}, **kwargs)
  function = resolve(name, args, kwargs)
  options = tx_options(tx)
  data = function.encode(args, kwargs)
  with_decoded_errors do
    client.estimate_gas(to: address, data: data, from: options[:from] || wallet&.address, value: options[:value])
  end
end

#events_from(receipt) ⇒ Object

Events emitted by this contract in a receipt.



253
254
255
256
# File 'lib/block_given/contract.rb', line 253

def events_from(receipt)
  logs = receipt.respond_to?(:logs) ? receipt.logs : Array(receipt[:logs])
  decode_logs(logs.select { |l| Utils.same_address?(l[:address], address) })
end

#explorer_urlObject



258
# File 'lib/block_given/contract.rb', line 258

def explorer_url = chain.explorer_address_url(address)

#get_events(name = nil, from_block:, to_block: :latest, args: {}, max_block_range: nil) ⇒ Object

Fetches past events. args filters on indexed parameters. usdc.get_events(:Transfer, from_block: 18_000_000, to_block: :latest, args: { to: wallet.address }) Pass max_block_range: to split a large range into several eth_getLogs calls.



209
210
211
212
213
214
215
216
217
218
# File 'lib/block_given/contract.rb', line 209

def get_events(name = nil, from_block:, to_block: :latest, args: {}, max_block_range: nil)
  topics = name ? interface.event(name).encode_topics(args) : nil
  logs = if max_block_range
           client.get_logs_in_chunks(address: address, topics: topics, from_block: from_block,
                                     to_block: to_block, max_block_range: max_block_range)
         else
           client.get_logs(address: address, topics: topics, from_block: from_block, to_block: to_block)
         end
  decode_logs(logs)
end

#inspectObject



260
# File 'lib/block_given/contract.rb', line 260

def inspect = "#<#{self.class.name} #{address}#{" wallet=#{wallet.address}" if wallet}>"

#interfaceObject



112
# File 'lib/block_given/contract.rb', line 112

def interface = self.class.interface

#prepare_write(name, *args, tx: {}, **kwargs) ⇒ Object

Signs without broadcasting. Returns a BlockGiven::SignedTransaction whose #hash and #nonce are known before any network call: persist them, then call #broadcast. The signed transaction keeps this contract's ABI, so reverts raised by #broadcast are decoded too.



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/block_given/contract.rb', line 151

def prepare_write(name, *args, tx: {}, **kwargs)
  function = resolve(name, args, kwargs)
  unless wallet
    raise WalletRequiredError,
          "#{self.class.name}##{function.ruby_name} needs a wallet (pass wallet: to .new)"
  end

  options = tx_options(tx)
  value = options[:value] || 0
  if value != 0 && !function.payable?
    raise InvalidArgumentError, "#{function.name} is not payable, cannot send value"
  end

  data = function.encode(args, kwargs)
  with_decoded_errors do
    wallet.signed_transaction(
      to: address, data: data, value: value, gas: options[:gas], nonce: options[:nonce],
      max_fee_per_gas: options[:max_fee_per_gas], max_priority_fee_per_gas: options[:max_priority_fee_per_gas],
      gas_price: options[:gas_price]
    ).with_interface(interface)
  end
end

#read(name, *args, tx: {}, **kwargs) ⇒ Object

eth_call + decode. Overrides: tx: { block:, from: }.



133
134
135
136
137
138
139
140
141
# File 'lib/block_given/contract.rb', line 133

def read(name, *args, tx: {}, **kwargs)
  function = resolve(name, args, kwargs)
  data = function.encode(args, kwargs)
  options = tx_options(tx)
  raw = with_decoded_errors do
    client.call(to: address, data: data, from: options[:from] || wallet&.address, block: options[:block] || :latest)
  end
  function.decode_output(raw)
end

#simulate(name, *args, tx: {}, **kwargs) ⇒ Object

Dry-runs a write with eth_call from the wallet address and returns the decoded result. Raises BlockGiven::ContractRevertError with the decoded reason on failure.



176
177
178
179
180
181
182
183
184
185
# File 'lib/block_given/contract.rb', line 176

def simulate(name, *args, tx: {}, **kwargs)
  function = resolve(name, args, kwargs)
  options = tx_options(tx)
  data = function.encode(args, kwargs)
  raw = with_decoded_errors do
    client.call(to: address, data: data, from: options[:from] || wallet&.address, value: options[:value],
                gas: options[:gas], block: options[:block] || :latest)
  end
  function.decode_output(raw)
end

#watch_event(name = nil, args: {}, from_block: nil, polling_interval: nil, max_block_range: nil, confirmations: 0, on_progress: nil, id: nil, &block) ⇒ Object Also known as: watch_events

Polls for new events in a background thread. Returns a BlockGiven::Watcher. watcher = usdc.watch_event(:Transfer, args: { to: me }) { |event| puts event.args } watcher.stop

Resuming after a restart: pass from_block: (your persisted cursor + 1) and persist the to block handed to on_progress after each processed range. confirmations: keeps the watcher N blocks behind the head so reorged logs are never delivered.

Raises:

  • (::ArgumentError)


227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/block_given/contract.rb', line 227

def watch_event(name = nil, args: {}, from_block: nil, polling_interval: nil, max_block_range: nil,
                confirmations: 0, on_progress: nil, id: nil, &block)
  raise ::ArgumentError, "a block is required" unless block

  event = name && interface.event(name)
  topics = event&.encode_topics(args)
  label = "#{event ? event.name : '*'}@#{self.class.name || 'Contract'}(#{address[0, 10]})"
  client.watch_logs(address: address, topics: topics, from_block: from_block,
                    polling_interval: polling_interval, max_block_range: max_block_range,
                    confirmations: confirmations, on_progress: on_progress, id: id, name: label) do |logs|
    decode_logs(logs).each { |event| block.call(event) }
  end
end

#with_wallet(wallet) ⇒ Object



128
# File 'lib/block_given/contract.rb', line 128

def with_wallet(wallet) = self.class.new(address: address, wallet: wallet, client: @client, chain: @chain)

#write(name, *args, tx: {}, **kwargs) ⇒ Object

Signs and broadcasts. Returns a BlockGiven::Transaction. Overrides: tx: { value:, gas:, nonce:, fees... }.



144
145
146
# File 'lib/block_given/contract.rb', line 144

def write(name, *args, tx: {}, **kwargs)
  prepare_write(name, *args, tx: tx, **kwargs).broadcast
end