block_given 0.1.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 +7 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +463 -0
- data/lib/block_given/abi/coder.rb +134 -0
- data/lib/block_given/abi/custom_error.rb +29 -0
- data/lib/block_given/abi/event.rb +87 -0
- data/lib/block_given/abi/function.rb +72 -0
- data/lib/block_given/abi/interface.rb +109 -0
- data/lib/block_given/abi/parameter.rb +57 -0
- data/lib/block_given/chain.rb +112 -0
- data/lib/block_given/client.rb +234 -0
- data/lib/block_given/configuration.rb +48 -0
- data/lib/block_given/connectors/alchemy.rb +36 -0
- data/lib/block_given/connectors/base.rb +26 -0
- data/lib/block_given/connectors/http.rb +150 -0
- data/lib/block_given/connectors/stub.rb +66 -0
- data/lib/block_given/contract.rb +287 -0
- data/lib/block_given/errors.rb +157 -0
- data/lib/block_given/event.rb +36 -0
- data/lib/block_given/normalizer.rb +37 -0
- data/lib/block_given/poller.rb +226 -0
- data/lib/block_given/railtie.rb +17 -0
- data/lib/block_given/receipt.rb +44 -0
- data/lib/block_given/signed_transaction.rb +145 -0
- data/lib/block_given/transaction.rb +85 -0
- data/lib/block_given/utils.rb +134 -0
- data/lib/block_given/version.rb +5 -0
- data/lib/block_given/wallet.rb +137 -0
- data/lib/block_given.rb +68 -0
- metadata +130 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
module Abi
|
|
5
|
+
# Solidity custom error (`error InsufficientBalance(uint256 available, uint256 required)`).
|
|
6
|
+
class CustomError
|
|
7
|
+
attr_reader :name, :inputs
|
|
8
|
+
|
|
9
|
+
def initialize(definition)
|
|
10
|
+
definition = definition.transform_keys(&:to_s)
|
|
11
|
+
@name = definition["name"].to_s
|
|
12
|
+
@inputs = Array(definition["inputs"]).each_with_index.map { |i, idx| Parameter.new(i, index: idx) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def signature = "#{name}(#{inputs.map(&:type).join(',')})"
|
|
16
|
+
def selector = @selector ||= Utils.keccak256(signature)[0, 10]
|
|
17
|
+
|
|
18
|
+
# Returns a Hash of decoded arguments keyed by snake_case names.
|
|
19
|
+
def decode(revert_data)
|
|
20
|
+
payload = "0x#{Utils.strip_hex(revert_data)[8..]}"
|
|
21
|
+
return {} if inputs.empty?
|
|
22
|
+
|
|
23
|
+
inputs.map(&:ruby_name).zip(Coder.decode(inputs, payload)).to_h
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def inspect = "#<BlockGiven::Abi::CustomError #{signature}>"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
module Abi
|
|
5
|
+
class Event
|
|
6
|
+
attr_reader :name, :inputs, :anonymous
|
|
7
|
+
|
|
8
|
+
def initialize(definition)
|
|
9
|
+
definition = definition.transform_keys(&:to_s)
|
|
10
|
+
@name = definition["name"].to_s
|
|
11
|
+
@inputs = Array(definition["inputs"]).each_with_index.map { |i, idx| Parameter.new(i, index: idx) }
|
|
12
|
+
@anonymous = !!definition["anonymous"]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ruby_name = Utils.snake_case(name).to_sym
|
|
16
|
+
def signature = "#{name}(#{inputs.map(&:type).join(',')})"
|
|
17
|
+
def topic = @topic ||= Utils.keccak256(signature)
|
|
18
|
+
def anonymous? = anonymous
|
|
19
|
+
|
|
20
|
+
def indexed_inputs = inputs.select(&:indexed?)
|
|
21
|
+
def data_inputs = inputs.reject(&:indexed?)
|
|
22
|
+
|
|
23
|
+
def matches?(log)
|
|
24
|
+
topic0 = Array(log[:topics] || log["topics"]).first
|
|
25
|
+
!anonymous? && topic0&.downcase == topic
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Decodes a normalized log (Hash with :topics and :data) into a BlockGiven::Event.
|
|
29
|
+
def decode(log)
|
|
30
|
+
topics = Array(log[:topics] || log["topics"])
|
|
31
|
+
data = log[:data] || log["data"] || "0x"
|
|
32
|
+
indexed_topics = anonymous? ? topics : topics[1..] || []
|
|
33
|
+
|
|
34
|
+
args = {}
|
|
35
|
+
indexed_inputs.each_with_index do |param, i|
|
|
36
|
+
args[param.ruby_name] = decode_topic(indexed_topics[i], param)
|
|
37
|
+
end
|
|
38
|
+
unless data_inputs.empty?
|
|
39
|
+
Coder.decode(data_inputs, data).each_with_index do |value, i|
|
|
40
|
+
args[data_inputs[i].ruby_name] = value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
# keep declaration order
|
|
44
|
+
ordered = inputs.to_h { |p| [p.ruby_name, args[p.ruby_name]] }
|
|
45
|
+
BlockGiven::Event.new(name: name, signature: signature, args: ordered, log: log)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Builds the topics filter array for eth_getLogs from indexed argument values.
|
|
49
|
+
# Values can be nil (wildcard), a single value or an Array (OR).
|
|
50
|
+
def encode_topics(filters = {})
|
|
51
|
+
normalized = filters.transform_keys { |k| Utils.snake_case(k).to_sym }
|
|
52
|
+
unknown = normalized.keys - indexed_inputs.map(&:ruby_name)
|
|
53
|
+
unless unknown.empty?
|
|
54
|
+
indexed = indexed_inputs.map(&:ruby_name).join(", ")
|
|
55
|
+
raise InvalidArgumentError, "#{name}: #{unknown.join(', ')} is not an indexed parameter (indexed: #{indexed})"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
topics = indexed_inputs.map do |param|
|
|
59
|
+
value = normalized[param.ruby_name]
|
|
60
|
+
next nil if value.nil?
|
|
61
|
+
|
|
62
|
+
value.is_a?(Array) ? value.map { |v| encode_topic(v, param) } : encode_topic(value, param)
|
|
63
|
+
end
|
|
64
|
+
topics = topics.reverse.drop_while(&:nil?).reverse # trailing wildcards are implicit
|
|
65
|
+
anonymous? ? topics : [topic, *topics]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_s = signature
|
|
69
|
+
def inspect = "#<BlockGiven::Abi::Event #{signature}>"
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def decode_topic(topic, param)
|
|
74
|
+
return nil if topic.nil?
|
|
75
|
+
return topic if param.dynamic? # only the keccak hash of the value is available
|
|
76
|
+
|
|
77
|
+
Coder.decode([param], topic).first
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def encode_topic(value, param)
|
|
81
|
+
return Utils.keccak256(param.raw_type == "string" ? value.to_s : value) if param.dynamic?
|
|
82
|
+
|
|
83
|
+
Utils.prefix_hex(Utils.strip_hex(Coder.encode([param], [value])))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
module Abi
|
|
5
|
+
class Function
|
|
6
|
+
attr_reader :name, :inputs, :outputs, :state_mutability
|
|
7
|
+
|
|
8
|
+
def initialize(definition)
|
|
9
|
+
definition = definition.transform_keys(&:to_s)
|
|
10
|
+
@name = definition["name"].to_s
|
|
11
|
+
@inputs = Array(definition["inputs"]).each_with_index.map { |i, idx| Parameter.new(i, index: idx) }
|
|
12
|
+
@outputs = Array(definition["outputs"]).each_with_index.map { |o, idx| Parameter.new(o, index: idx) }
|
|
13
|
+
@state_mutability = (definition["stateMutability"] || legacy_mutability(definition)).to_s
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ruby_name = Utils.snake_case(name).to_sym
|
|
17
|
+
def signature = "#{name}(#{inputs.map(&:type).join(',')})"
|
|
18
|
+
def selector = @selector ||= Utils.keccak256(signature)[0, 10]
|
|
19
|
+
|
|
20
|
+
def read? = %w[view pure].include?(state_mutability)
|
|
21
|
+
def write? = !read?
|
|
22
|
+
def payable? = state_mutability == "payable"
|
|
23
|
+
|
|
24
|
+
def input_names = inputs.map(&:ruby_name)
|
|
25
|
+
|
|
26
|
+
# Positional args or keyword args (matched on snake_cased input names).
|
|
27
|
+
def encode(args = [], kwargs = {})
|
|
28
|
+
values = resolve_args(args, kwargs)
|
|
29
|
+
selector + Utils.strip_hex(Coder.encode(inputs, values))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Single output -> value; several -> Array.
|
|
33
|
+
def decode_output(hex)
|
|
34
|
+
values = Coder.decode(outputs, hex)
|
|
35
|
+
outputs.size == 1 ? values.first : values
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def resolve_args(args, kwargs)
|
|
39
|
+
raise InvalidArgumentError, "#{name}: mix of positional and keyword arguments" if !args.empty? && !kwargs.empty?
|
|
40
|
+
return args if kwargs.empty?
|
|
41
|
+
|
|
42
|
+
if inputs.any?(&:unnamed?)
|
|
43
|
+
raise InvalidArgumentError, "#{name}: inputs are unnamed in the ABI, use positional arguments"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
normalized = kwargs.transform_keys { |k| Utils.snake_case(k).to_sym }
|
|
47
|
+
unknown = normalized.keys - input_names
|
|
48
|
+
unless unknown.empty?
|
|
49
|
+
raise InvalidArgumentError,
|
|
50
|
+
"#{name}: unknown argument(s) #{unknown.join(', ')} (expected #{input_names.join(', ')})"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
missing = input_names - normalized.keys
|
|
54
|
+
raise InvalidArgumentError, "#{name}: missing argument(s) #{missing.join(', ')}" unless missing.empty?
|
|
55
|
+
|
|
56
|
+
input_names.map { |n| normalized[n] }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_s = signature
|
|
60
|
+
def inspect = "#<BlockGiven::Abi::Function #{signature} #{state_mutability}>"
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def legacy_mutability(definition)
|
|
65
|
+
return "payable" if definition["payable"]
|
|
66
|
+
return "view" if definition["constant"]
|
|
67
|
+
|
|
68
|
+
"nonpayable"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module BlockGiven
|
|
6
|
+
module Abi
|
|
7
|
+
# Parsed ABI: functions (with overload resolution), events and custom errors.
|
|
8
|
+
class Interface
|
|
9
|
+
attr_reader :functions, :events, :errors, :constructor, :raw
|
|
10
|
+
|
|
11
|
+
# Accepts an ABI Array, a Hardhat/Foundry artifact Hash (with "abi"), a JSON String or a Pathname.
|
|
12
|
+
def self.parse(source)
|
|
13
|
+
return source if source.is_a?(Interface)
|
|
14
|
+
|
|
15
|
+
new(load_definitions(source))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.load_definitions(source)
|
|
19
|
+
case source
|
|
20
|
+
when Array then source
|
|
21
|
+
when Hash then source["abi"] || source[:abi] || raise(AbiError, "Hash has no 'abi' key")
|
|
22
|
+
when Pathname, File then load_definitions(JSON.parse(File.read(source)))
|
|
23
|
+
when String then load_definitions(JSON.parse(source))
|
|
24
|
+
else raise AbiError, "cannot parse ABI from #{source.class}"
|
|
25
|
+
end
|
|
26
|
+
rescue JSON::ParserError => e
|
|
27
|
+
raise AbiError, "invalid ABI JSON: #{e.message}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(definitions)
|
|
31
|
+
@raw = definitions.map { |d| d.transform_keys(&:to_s) }
|
|
32
|
+
@functions = []
|
|
33
|
+
@events = []
|
|
34
|
+
@errors = []
|
|
35
|
+
@constructor = nil
|
|
36
|
+
@raw.each do |definition|
|
|
37
|
+
case definition["type"]
|
|
38
|
+
when "function", nil then @functions << Function.new(definition)
|
|
39
|
+
when "event" then @events << Event.new(definition)
|
|
40
|
+
when "error" then @errors << CustomError.new(definition)
|
|
41
|
+
when "constructor" then @constructor = definition
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
@functions_by_name = @functions.group_by(&:ruby_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def function_names = @functions_by_name.keys
|
|
48
|
+
|
|
49
|
+
# Finds a function by name (snake_case or camelCase) or by full signature
|
|
50
|
+
# ("transfer(address,uint256)"). Overloads are disambiguated by argument
|
|
51
|
+
# count or keyword names.
|
|
52
|
+
def function(name, args: [], kwargs: {})
|
|
53
|
+
name = name.to_s
|
|
54
|
+
return function_by_signature(name) if name.include?("(")
|
|
55
|
+
|
|
56
|
+
candidates = @functions_by_name[Utils.snake_case(name).to_sym]
|
|
57
|
+
if candidates.nil?
|
|
58
|
+
raise FunctionNotFoundError,
|
|
59
|
+
"no function #{name.inspect} in ABI (known: #{function_names.join(', ')})"
|
|
60
|
+
end
|
|
61
|
+
return candidates.first if candidates.size == 1
|
|
62
|
+
|
|
63
|
+
matching =
|
|
64
|
+
if kwargs.empty?
|
|
65
|
+
candidates.select { |f| f.inputs.size == args.size }
|
|
66
|
+
else
|
|
67
|
+
keys = kwargs.keys.map { |k| Utils.snake_case(k).to_sym }.sort
|
|
68
|
+
candidates.select { |f| f.input_names.sort == keys }
|
|
69
|
+
end
|
|
70
|
+
return matching.first if matching.size == 1
|
|
71
|
+
|
|
72
|
+
if matching.size > 1
|
|
73
|
+
raise AmbiguousFunctionError,
|
|
74
|
+
"#{name} is overloaded, use the full signature: #{candidates.map(&:signature).join(' | ')}"
|
|
75
|
+
end
|
|
76
|
+
raise FunctionNotFoundError,
|
|
77
|
+
"no overload of #{name} matches the given arguments (#{candidates.map(&:signature).join(' | ')})"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def function_by_signature(signature)
|
|
81
|
+
@functions.find { |f| f.signature == signature.delete(" ") } ||
|
|
82
|
+
raise(FunctionNotFoundError, "no function with signature #{signature.inspect}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def function_by_selector(selector)
|
|
86
|
+
@functions.find { |f| f.selector == selector.downcase }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def event(name)
|
|
90
|
+
name = name.to_s
|
|
91
|
+
key = Utils.snake_case(name).to_sym
|
|
92
|
+
@events.find { |e| e.ruby_name == key || e.signature == name } ||
|
|
93
|
+
raise(EventNotFoundError, "no event #{name.inspect} in ABI (known: #{@events.map(&:name).join(', ')})")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def event_by_topic(topic)
|
|
97
|
+
@events.find { |e| e.topic == topic.to_s.downcase }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def error_by_selector(selector)
|
|
101
|
+
@errors.find { |e| e.selector == selector.to_s.downcase }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def inspect
|
|
105
|
+
"#<BlockGiven::Abi::Interface functions=#{@functions.size} events=#{@events.size} errors=#{@errors.size}>"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
module Abi
|
|
5
|
+
# One ABI input/output. Knows its canonical Solidity type ("(uint256,address)[]").
|
|
6
|
+
class Parameter
|
|
7
|
+
attr_reader :name, :raw_type, :components, :indexed, :internal_type
|
|
8
|
+
|
|
9
|
+
def initialize(definition, index: 0)
|
|
10
|
+
definition = definition.transform_keys(&:to_s)
|
|
11
|
+
@name = definition["name"].to_s
|
|
12
|
+
@name = "arg#{index}" if @name.empty?
|
|
13
|
+
@unnamed = definition["name"].to_s.empty?
|
|
14
|
+
@raw_type = definition["type"].to_s
|
|
15
|
+
@indexed = definition["indexed"] ? true : false
|
|
16
|
+
@internal_type = definition["internalType"]
|
|
17
|
+
@components = Array(definition["components"]).each_with_index.map { |c, i| Parameter.new(c, index: i) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def unnamed? = @unnamed
|
|
21
|
+
def indexed? = indexed
|
|
22
|
+
def ruby_name = Utils.snake_case(name).to_sym
|
|
23
|
+
|
|
24
|
+
def tuple? = raw_type.start_with?("tuple")
|
|
25
|
+
def array? = raw_type.end_with?("]")
|
|
26
|
+
|
|
27
|
+
# "tuple[]" with components -> "(uint256,address)[]"
|
|
28
|
+
def type
|
|
29
|
+
return raw_type unless tuple?
|
|
30
|
+
|
|
31
|
+
raw_type.sub("tuple", "(#{components.map(&:type).join(',')})")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Base type without the outermost array dimension.
|
|
35
|
+
def element
|
|
36
|
+
return nil unless array?
|
|
37
|
+
|
|
38
|
+
Parameter.new(
|
|
39
|
+
{ "name" => name, "type" => raw_type.sub(/\[\d*\]\z/, ""),
|
|
40
|
+
"components" => components.map(&:to_h) }
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def dynamic?
|
|
45
|
+
raw_type == "string" || raw_type == "bytes" || raw_type.end_with?("[]") ||
|
|
46
|
+
(tuple? && components.any?(&:dynamic?)) || (array? && element.dynamic?)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_h
|
|
50
|
+
h = { "name" => unnamed? ? "" : name, "type" => raw_type }
|
|
51
|
+
h["components"] = components.map(&:to_h) if tuple?
|
|
52
|
+
h["indexed"] = indexed if indexed
|
|
53
|
+
h
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BlockGiven
|
|
4
|
+
# Static description of an EVM network, similar to viem/chains.
|
|
5
|
+
Chain = Struct.new(
|
|
6
|
+
:id, :name, :network, :native_currency, :rpc_urls, :block_explorer_url, :alchemy_network, :testnet,
|
|
7
|
+
keyword_init: true
|
|
8
|
+
) do
|
|
9
|
+
def initialize(id:, name:, network: nil, native_currency: nil, rpc_urls: [], block_explorer_url: nil,
|
|
10
|
+
alchemy_network: nil, testnet: false)
|
|
11
|
+
super(
|
|
12
|
+
id: id, name: name, network: network || name.to_s.downcase.gsub(/[^a-z0-9]+/, "-"),
|
|
13
|
+
native_currency: native_currency || { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
14
|
+
rpc_urls: Array(rpc_urls), block_explorer_url: block_explorer_url,
|
|
15
|
+
alchemy_network: alchemy_network, testnet: testnet
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def testnet? = !!testnet
|
|
20
|
+
|
|
21
|
+
def explorer_tx_url(hash)
|
|
22
|
+
block_explorer_url && "#{block_explorer_url}/tx/#{hash}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def explorer_address_url(address)
|
|
26
|
+
block_explorer_url && "#{block_explorer_url}/address/#{address}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s = "#{name} (#{id})"
|
|
30
|
+
def inspect = "#<BlockGiven::Chain #{self}>"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module Chains
|
|
34
|
+
MAINNET = Chain.new(
|
|
35
|
+
id: 1, name: "Ethereum", network: "mainnet",
|
|
36
|
+
rpc_urls: ["https://eth.merkle.io"], block_explorer_url: "https://etherscan.io",
|
|
37
|
+
alchemy_network: "eth-mainnet"
|
|
38
|
+
)
|
|
39
|
+
SEPOLIA = Chain.new(
|
|
40
|
+
id: 11_155_111, name: "Sepolia", network: "sepolia",
|
|
41
|
+
rpc_urls: ["https://sepolia.drpc.org"], block_explorer_url: "https://sepolia.etherscan.io",
|
|
42
|
+
alchemy_network: "eth-sepolia", testnet: true
|
|
43
|
+
)
|
|
44
|
+
BASE = Chain.new(
|
|
45
|
+
id: 8453, name: "Base", network: "base",
|
|
46
|
+
rpc_urls: ["https://mainnet.base.org"], block_explorer_url: "https://basescan.org",
|
|
47
|
+
alchemy_network: "base-mainnet"
|
|
48
|
+
)
|
|
49
|
+
BASE_SEPOLIA = Chain.new(
|
|
50
|
+
id: 84_532, name: "Base Sepolia", network: "base-sepolia",
|
|
51
|
+
rpc_urls: ["https://sepolia.base.org"], block_explorer_url: "https://sepolia.basescan.org",
|
|
52
|
+
alchemy_network: "base-sepolia", testnet: true
|
|
53
|
+
)
|
|
54
|
+
POLYGON = Chain.new(
|
|
55
|
+
id: 137, name: "Polygon", network: "polygon",
|
|
56
|
+
native_currency: { name: "POL", symbol: "POL", decimals: 18 },
|
|
57
|
+
rpc_urls: ["https://polygon-rpc.com"], block_explorer_url: "https://polygonscan.com",
|
|
58
|
+
alchemy_network: "polygon-mainnet"
|
|
59
|
+
)
|
|
60
|
+
POLYGON_AMOY = Chain.new(
|
|
61
|
+
id: 80_002, name: "Polygon Amoy", network: "polygon-amoy",
|
|
62
|
+
native_currency: { name: "POL", symbol: "POL", decimals: 18 },
|
|
63
|
+
rpc_urls: ["https://rpc-amoy.polygon.technology"], block_explorer_url: "https://amoy.polygonscan.com",
|
|
64
|
+
alchemy_network: "polygon-amoy", testnet: true
|
|
65
|
+
)
|
|
66
|
+
ARBITRUM = Chain.new(
|
|
67
|
+
id: 42_161, name: "Arbitrum One", network: "arbitrum",
|
|
68
|
+
rpc_urls: ["https://arb1.arbitrum.io/rpc"], block_explorer_url: "https://arbiscan.io",
|
|
69
|
+
alchemy_network: "arb-mainnet"
|
|
70
|
+
)
|
|
71
|
+
ARBITRUM_SEPOLIA = Chain.new(
|
|
72
|
+
id: 421_614, name: "Arbitrum Sepolia", network: "arbitrum-sepolia",
|
|
73
|
+
rpc_urls: ["https://sepolia-rollup.arbitrum.io/rpc"], block_explorer_url: "https://sepolia.arbiscan.io",
|
|
74
|
+
alchemy_network: "arb-sepolia", testnet: true
|
|
75
|
+
)
|
|
76
|
+
OPTIMISM = Chain.new(
|
|
77
|
+
id: 10, name: "OP Mainnet", network: "optimism",
|
|
78
|
+
rpc_urls: ["https://mainnet.optimism.io"], block_explorer_url: "https://optimistic.etherscan.io",
|
|
79
|
+
alchemy_network: "opt-mainnet"
|
|
80
|
+
)
|
|
81
|
+
OPTIMISM_SEPOLIA = Chain.new(
|
|
82
|
+
id: 11_155_420, name: "OP Sepolia", network: "optimism-sepolia",
|
|
83
|
+
rpc_urls: ["https://sepolia.optimism.io"], block_explorer_url: "https://sepolia-optimism.etherscan.io",
|
|
84
|
+
alchemy_network: "opt-sepolia", testnet: true
|
|
85
|
+
)
|
|
86
|
+
# Hardhat / Anvil / Ganache default chain, handy with a local fork.
|
|
87
|
+
LOCALHOST = Chain.new(
|
|
88
|
+
id: 31_337, name: "Localhost", network: "localhost",
|
|
89
|
+
rpc_urls: ["http://127.0.0.1:8545"], testnet: true
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
ALL = [MAINNET, SEPOLIA, BASE, BASE_SEPOLIA, POLYGON, POLYGON_AMOY, ARBITRUM, ARBITRUM_SEPOLIA,
|
|
93
|
+
OPTIMISM, OPTIMISM_SEPOLIA, LOCALHOST].freeze
|
|
94
|
+
|
|
95
|
+
module_function
|
|
96
|
+
|
|
97
|
+
# Chains.resolve(:base) / Chains.resolve("base-sepolia") / Chains.resolve(8453) / Chains.resolve(chain)
|
|
98
|
+
def resolve(value)
|
|
99
|
+
case value
|
|
100
|
+
when Chain then value
|
|
101
|
+
when Integer then find_by_id(value) || raise(ConfigurationError, "unknown chain id #{value}")
|
|
102
|
+
when Symbol, String
|
|
103
|
+
key = value.to_s.downcase.tr("_", "-")
|
|
104
|
+
ALL.find { |c| c.network == key } || raise(ConfigurationError, "unknown chain #{value.inspect}")
|
|
105
|
+
else raise ConfigurationError, "cannot resolve chain from #{value.inspect}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def find_by_id(id) = ALL.find { |c| c.id == id }
|
|
110
|
+
def [](value) = resolve(value)
|
|
111
|
+
end
|
|
112
|
+
end
|