Class: BlockGiven::Client

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

Overview

Public JSON-RPC client bound to a chain and a connector (viem's PublicClient).

connector = BlockGiven::Connectors::Alchemy.new(api_key: "...")
client = BlockGiven::Client.new(chain: :base, connector: connector)
client.block_number
client.get_balance("0x...")

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(chain: nil, connector: nil, polling_interval: nil, timeout: nil, logger: nil) ⇒ Client

Returns a new instance of Client.



13
14
15
16
17
18
19
# File 'lib/block_given/client.rb', line 13

def initialize(chain: nil, connector: nil, polling_interval: nil, timeout: nil, logger: nil)
  @chain = chain ? Chains.resolve(chain) : BlockGiven.config.chain!
  @connector = connector || BlockGiven.config.connector!
  @polling_interval = polling_interval
  @timeout = timeout
  @logger = logger
end

Instance Attribute Details

#chainObject (readonly)

Returns the value of attribute chain.



11
12
13
# File 'lib/block_given/client.rb', line 11

def chain
  @chain
end

#connectorObject (readonly)

Returns the value of attribute connector.



11
12
13
# File 'lib/block_given/client.rb', line 11

def connector
  @connector
end

Instance Method Details

#batch(calls) ⇒ Object

Batched JSON-RPC: client.batch([["eth_blockNumber"], ["eth_chainId"]])



34
# File 'lib/block_given/client.rb', line 34

def batch(calls) = connector.batch(calls, chain: chain)

#block_numberObject



39
# File 'lib/block_given/client.rb', line 39

def block_number = Utils.hex_to_int(request("eth_blockNumber"))

#call(to:, data:, from: nil, value: nil, gas: nil, block: :latest) ⇒ Object

Executes a read-only call. Returns the raw hex result.



91
92
93
# File 'lib/block_given/client.rb', line 91

def call(to:, data:, from: nil, value: nil, gas: nil, block: :latest)
  request("eth_call", call_object(to: to, data: data, from: from, value: value, gas: gas), Utils.block_tag(block))
end

#chain_idObject

--- Chain / blocks -----------------------------------------------------



38
# File 'lib/block_given/client.rb', line 38

def chain_id = Utils.hex_to_int(request("eth_chainId"))

#contract?(address) ⇒ Boolean

Returns:

  • (Boolean)


82
# File 'lib/block_given/client.rb', line 82

def contract?(address) = get_code(address) != "0x"

#estimate_fees_per_gas(base_fee_multiplier: nil) ⇒ Object

EIP-1559 fee estimation (viem semantics: baseFee * multiplier + priority fee).



56
57
58
59
60
61
62
63
64
65
66
# File 'lib/block_given/client.rb', line 56

def estimate_fees_per_gas(base_fee_multiplier: nil)
  multiplier = base_fee_multiplier || BlockGiven.config.base_fee_multiplier
  block = get_block(:latest)
  base_fee = block[:base_fee_per_gas] || gas_price
  priority = max_priority_fee_per_gas
  {
    base_fee_per_gas: base_fee,
    max_priority_fee_per_gas: priority,
    max_fee_per_gas: (base_fee * multiplier).ceil + priority
  }
end

#estimate_gas(to:, data: nil, from: nil, value: nil) ⇒ Object



95
96
97
# File 'lib/block_given/client.rb', line 95

def estimate_gas(to:, data: nil, from: nil, value: nil)
  Utils.hex_to_int(request("eth_estimateGas", call_object(to: to, data: data, from: from, value: value)))
end

#gas_priceObject



47
# File 'lib/block_given/client.rb', line 47

def gas_price = Utils.hex_to_int(request("eth_gasPrice"))

#get_balance(address, block: :latest) ⇒ Object

--- Accounts -----------------------------------------------------------



70
71
72
# File 'lib/block_given/client.rb', line 70

def get_balance(address, block: :latest)
  Utils.hex_to_int(request("eth_getBalance", Utils.checksum_address(address), Utils.block_tag(block)))
end

#get_block(block = :latest, include_transactions: false) ⇒ Object



41
42
43
44
45
# File 'lib/block_given/client.rb', line 41

def get_block(block = :latest, include_transactions: false)
  method = Utils.hex?(block.to_s) && block.to_s.length == 66 ? "eth_getBlockByHash" : "eth_getBlockByNumber"
  raw = request(method, block_param(block), include_transactions)
  raw && Normalizer.normalize(raw)
end

#get_code(address, block: :latest) ⇒ Object



78
79
80
# File 'lib/block_given/client.rb', line 78

def get_code(address, block: :latest)
  request("eth_getCode", Utils.checksum_address(address), Utils.block_tag(block))
end

#get_logs(address: nil, topics: nil, from_block: :latest, to_block: :latest, block_hash: nil) ⇒ Object

--- Logs ---------------------------------------------------------------



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/block_given/client.rb', line 132

def get_logs(address: nil, topics: nil, from_block: :latest, to_block: :latest, block_hash: nil)
  filter = {}
  filter[:address] = Array(address).map { |a| Utils.checksum_address(a) } if address
  filter[:address] = filter[:address].first if filter[:address]&.size == 1
  filter[:topics] = topics if topics
  if block_hash
    filter[:blockHash] = block_hash
  else
    filter[:fromBlock] = Utils.block_tag(from_block)
    filter[:toBlock] = Utils.block_tag(to_block)
  end
  Normalizer.normalize(request("eth_getLogs", filter))
end

#get_logs_in_chunks(from_block:, address: nil, topics: nil, to_block: :latest, max_block_range: nil) ⇒ Object

get_logs over a large range, split in chunks of max_block_range blocks so provider limits are respected. Yields each chunk's logs when a block is given, else returns them all.



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

def get_logs_in_chunks(from_block:, address: nil, topics: nil, to_block: :latest, max_block_range: nil)
  size = max_block_range || BlockGiven.config.max_block_range
  to_block = block_number if to_block.nil? || Utils::BLOCK_TAGS.include?(to_block.to_s)
  collected = []
  from = from_block
  while from <= to_block
    to = [from + size - 1, to_block].min
    logs = get_logs(address: address, topics: topics, from_block: from, to_block: to)
    block_given? ? yield(logs, from, to) : collected.concat(logs)
    from = to + 1
  end
  collected
end

#get_storage_at(address, slot, block: :latest) ⇒ Object



84
85
86
# File 'lib/block_given/client.rb', line 84

def get_storage_at(address, slot, block: :latest)
  request("eth_getStorageAt", Utils.checksum_address(address), Utils.to_hex(slot), Utils.block_tag(block))
end

#get_transaction(hash) ⇒ Object



107
108
109
110
# File 'lib/block_given/client.rb', line 107

def get_transaction(hash)
  raw = request("eth_getTransactionByHash", hash)
  raw && Normalizer.normalize(raw)
end

#get_transaction_count(address, block: :pending) ⇒ Object



74
75
76
# File 'lib/block_given/client.rb', line 74

def get_transaction_count(address, block: :pending)
  Utils.hex_to_int(request("eth_getTransactionCount", Utils.checksum_address(address), Utils.block_tag(block)))
end

#get_transaction_receipt(hash) ⇒ Object



112
113
114
115
# File 'lib/block_given/client.rb', line 112

def get_transaction_receipt(hash)
  raw = request("eth_getTransactionReceipt", hash)
  raw && Receipt.new(raw)
end

#inspectObject



215
# File 'lib/block_given/client.rb', line 215

def inspect = "#<BlockGiven::Client chain=#{chain} connector=#{connector.inspect}>"

#loggerObject



23
# File 'lib/block_given/client.rb', line 23

def logger = @logger || BlockGiven.config.logger

#max_priority_fee_per_gasObject



49
50
51
52
53
# File 'lib/block_given/client.rb', line 49

def max_priority_fee_per_gas
  Utils.hex_to_int(request("eth_maxPriorityFeePerGas"))
rescue RpcError
  Utils.parse_gwei("1") # method not supported by every node
end

#polling_intervalObject



21
# File 'lib/block_given/client.rb', line 21

def polling_interval = @polling_interval || BlockGiven.config.polling_interval

#request(method, *params) ⇒ Object

Raw JSON-RPC call: client.request("eth_blockNumber") / client.request("eth_getBalance", addr, "latest")



26
27
28
29
30
31
# File 'lib/block_given/client.rb', line 26

def request(method, *params)
  logger.debug { "[block_given] -> #{method} #{params.inspect}" }
  result = connector.request(method, params, chain: chain)
  logger.debug { "[block_given] <- #{method} #{result.inspect[0, 200]}" }
  result
end

#send_raw_transaction(raw) ⇒ Object

--- Transactions -------------------------------------------------------



101
102
103
# File 'lib/block_given/client.rb', line 101

def send_raw_transaction(raw)
  Transaction.new(request("eth_sendRawTransaction", Utils.prefix_hex(raw)), client: self)
end

#timeoutObject



22
# File 'lib/block_given/client.rb', line 22

def timeout = @timeout || BlockGiven.config.timeout

#transaction(hash) ⇒ Object



105
# File 'lib/block_given/client.rb', line 105

def transaction(hash) = Transaction.new(hash, client: self)

#wait_for_transaction_receipt(hash, confirmations: nil, timeout: nil, polling_interval: nil) ⇒ Object

Polls until the receipt is available (and confirmed by N blocks).



118
119
120
121
122
123
124
125
126
127
128
# File 'lib/block_given/client.rb', line 118

def wait_for_transaction_receipt(hash, confirmations: nil, timeout: nil, polling_interval: nil)
  confirmations ||= BlockGiven.config.confirmations
  interval = polling_interval || self.polling_interval
  Poller.poll(interval: interval, timeout: timeout || self.timeout, description: "receipt of #{hash}") do
    receipt = get_transaction_receipt(hash)
    next nil unless receipt&.block_number
    next receipt if confirmations <= 1

    block_number - receipt.block_number + 1 >= confirmations ? receipt : nil
  end
end

#watch_block_number(polling_interval: nil, emit_missed: false, id: nil, &block) ⇒ Object

Yields each new block number. With emit_missed: true every block between two polls is yielded, otherwise only the latest one.



150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/block_given/client.rb', line 150

def watch_block_number(polling_interval: nil, emit_missed: false, id: nil, &block)
  last = nil
  watcher("block_number", polling_interval, id: id) do
    current = block_number
    next if last && current <= last

    if emit_missed && last
      ((last + 1)..current).each { |n| block.call(n) }
    else
      block.call(current)
    end
    last = current
  end
end

#watch_blocks(polling_interval: nil, include_transactions: false, id: nil, &block) ⇒ Object



165
166
167
168
169
# File 'lib/block_given/client.rb', line 165

def watch_blocks(polling_interval: nil, include_transactions: false, id: nil, &block)
  watch_block_number(polling_interval: polling_interval, emit_missed: true, id: id) do |number|
    block.call(get_block(number, include_transactions: include_transactions))
  end
end

#watch_logs(address: nil, topics: nil, from_block: nil, polling_interval: nil, max_block_range: nil, confirmations: 0, on_progress: nil, id: nil, name: nil, &block) ⇒ Object

Yields new logs matching the filter, one Array per processed block range.

from_block:      resume from this block (catch-up is chunked by max_block_range)
confirmations:   stay this many blocks behind the head to dodge reorgs (default 0)
on_progress:     ->(from, to) called after each range is processed: persist `to` as your cursor
watcher.cursor:  last processed block number
id:              stable identifier for BlockGiven::Watcher.find / stop (default: auto-generated)


194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/block_given/client.rb', line 194

def watch_logs(address: nil, topics: nil, from_block: nil, polling_interval: nil, max_block_range: nil,
               confirmations: 0, on_progress: nil, id: nil, name: nil, &block)
  last = from_block ? from_block - 1 : block_number - confirmations
  watcher(name || "logs@#{Array(address).first || '*'}", polling_interval, id: id) do |watcher|
    watcher.cursor ||= last
    head = block_number - confirmations
    while last < head && !watcher.stopped?
      to = [last + (max_block_range || BlockGiven.config.max_block_range), head].min
      logs = get_logs(address: address, topics: topics, from_block: last + 1, to_block: to)
      block.call(logs) unless logs.empty?
      on_progress&.call(last + 1, to)
      last = watcher.cursor = to
    end
  end
end

#watcher(name, polling_interval = nil, id: nil, &tick) ⇒ Object

Generic background watcher on this client.



211
212
213
# File 'lib/block_given/client.rb', line 211

def watcher(name, polling_interval = nil, id: nil, &tick)
  Watcher.new(interval: polling_interval || self.polling_interval, name: name, id: id, logger: logger, &tick).start
end