Class: BlockGiven::Contract
- Inherits:
-
Object
- Object
- BlockGiven::Contract
- 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
-
.interface ⇒ Object
readonly
Returns the value of attribute interface.
Instance Attribute Summary collapse
-
#address ⇒ Object
readonly
Returns the value of attribute address.
-
#wallet ⇒ Object
readonly
Returns the value of attribute wallet.
Class Method Summary collapse
-
.abi(source = nil) ⇒ Object
Sets (or returns) the ABI.
-
.abi_file(path) ⇒ Object
Loads the ABI from a JSON file.
-
.address(value = nil) ⇒ Object
(also: default_address)
Default address for instances (can be overridden with .new(address: ...) / .at(...)).
- .at(address, **options) ⇒ Object
- .chain(value = nil) ⇒ Object
- .errors ⇒ Object
- .events ⇒ Object
- .functions ⇒ Object
- .inherited(subclass) ⇒ Object
Instance Method Summary collapse
- #==(other) ⇒ Object
- #chain ⇒ Object
- #client ⇒ Object
- #decode_function_result(name, hex) ⇒ Object
-
#decode_logs(logs) ⇒ Object
Decodes raw logs with this contract's ABI.
- #encode_function_data(name, *args, **kwargs) ⇒ Object
- #estimate_gas(name, *args, tx: {}, **kwargs) ⇒ Object
-
#events_from(receipt) ⇒ Object
Events emitted by this contract in a receipt.
- #explorer_url ⇒ Object
-
#get_events(name = nil, from_block:, to_block: :latest, args: {}, max_block_range: nil) ⇒ Object
Fetches past events.
-
#initialize(address: nil, wallet: nil, client: nil, chain: nil) ⇒ Contract
constructor
A new instance of Contract.
- #inspect ⇒ Object
- #interface ⇒ Object
-
#prepare_write(name, *args, tx: {}, **kwargs) ⇒ Object
Signs without broadcasting.
-
#read(name, *args, tx: {}, **kwargs) ⇒ Object
eth_call + decode.
-
#simulate(name, *args, tx: {}, **kwargs) ⇒ Object
Dry-runs a write with eth_call from the wallet address and returns the decoded result.
-
#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: #watch_events)
Polls for new events in a background thread.
- #with_wallet(wallet) ⇒ Object
-
#write(name, *args, tx: {}, **kwargs) ⇒ Object
Signs and broadcasts.
Constructor Details
#initialize(address: nil, wallet: nil, client: nil, chain: nil) ⇒ Contract
Returns a new instance of Contract.
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
.interface ⇒ Object (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
#address ⇒ Object (readonly)
Returns the value of attribute address.
98 99 100 |
# File 'lib/block_given/contract.rb', line 98 def address @address end |
#wallet ⇒ Object (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).
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, **) = new(address: address, **) |
.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 |
.errors ⇒ Object
63 |
# File 'lib/block_given/contract.rb', line 63 def errors = interface&.errors || [] |
.events ⇒ Object
62 |
# File 'lib/block_given/contract.rb', line 62 def events = interface&.events || [] |
.functions ⇒ Object
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 |
#chain ⇒ Object
127 |
# File 'lib/block_given/contract.rb', line 127 def chain = client.chain |
#client ⇒ Object
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) = (tx) data = function.encode(args, kwargs) with_decoded_errors do client.estimate_gas(to: address, data: data, from: [:from] || wallet&.address, value: [: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_url ⇒ Object
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 |
#inspect ⇒ Object
260 |
# File 'lib/block_given/contract.rb', line 260 def inspect = "#<#{self.class.name} #{address}#{" wallet=#{wallet.address}" if wallet}>" |
#interface ⇒ Object
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 = (tx) value = [: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: [:gas], nonce: [:nonce], max_fee_per_gas: [:max_fee_per_gas], max_priority_fee_per_gas: [:max_priority_fee_per_gas], gas_price: [: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) = (tx) raw = with_decoded_errors do client.call(to: address, data: data, from: [:from] || wallet&.address, block: [: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) = (tx) data = function.encode(args, kwargs) raw = with_decoded_errors do client.call(to: address, data: data, from: [:from] || wallet&.address, value: [:value], gas: [:gas], block: [: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.
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 |