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
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockGiven
4
+ module Abi
5
+ # A parsed Solidity ABI type (`uint256`, `bytes32[2][]`, `(uint256,(bool,bytes))[]`), as the codec needs it:
6
+ # its base type, its size, its array dimensions and its tuple components.
7
+ #
8
+ # @api private
9
+ class Type
10
+ # @return [String] `uint`, `int`, `address`, `bool`, `bytes`, `string` or `tuple`
11
+ attr_reader :base
12
+ # @return [Integer, nil] bits of an integer, bytes of a `bytesN`, nil otherwise
13
+ attr_reader :size
14
+ # @return [Array<Integer, nil>] array dimensions, innermost first; nil marks a dynamic `[]`
15
+ attr_reader :dimensions
16
+ # @return [Array<Type>] tuple components, empty for other types
17
+ attr_reader :components
18
+
19
+ # Parses a canonical type string, with tuples written `(a,b)` or `tuple(a,b)`.
20
+ #
21
+ # @param type [String]
22
+ # @return [Type]
23
+ # @raise [BlockGiven::AbiError] when the type is malformed or not supported (`fixed`, `function`)
24
+ def self.parse(type)
25
+ type = type.to_s.strip
26
+ type = type.delete_prefix("tuple") if type.start_with?("tuple(")
27
+ return parse_tuple(type) if type.start_with?("(")
28
+
29
+ match = type.match(/\A([a-z]+)(\d*)((?:\[\d*\])*)\z/)
30
+ raise AbiError, "unsupported ABI type #{type.inspect}" unless match
31
+
32
+ new(*elementary(match[1], match[2], type), dimensions(match[3]))
33
+ end
34
+
35
+ # Parses a tuple type and its trailing dimensions.
36
+ #
37
+ # @param type [String] starting with `(`
38
+ # @return [Type]
39
+ # @raise [BlockGiven::AbiError] when the parentheses do not balance
40
+ def self.parse_tuple(type)
41
+ depth = 0
42
+ parts = [+""]
43
+ type.each_char.with_index do |char, index|
44
+ depth += { "(" => 1, ")" => -1 }.fetch(char, 0)
45
+ if depth.zero?
46
+ raise AbiError, "unsupported ABI type #{type.inspect}" unless type[(index + 1)..].match?(/\A(\[\d*\])*\z/)
47
+
48
+ components = parts.first.empty? && parts.size == 1 ? [] : parts.map { |part| parse(part) }
49
+ return new("tuple", nil, dimensions(type[(index + 1)..]), components)
50
+ end
51
+ next if depth == 1 && char == "("
52
+
53
+ depth == 1 && char == "," ? parts << +"" : parts.last << char
54
+ end
55
+ raise AbiError, "unsupported ABI type #{type.inspect}"
56
+ end
57
+
58
+ # Validates an elementary base type and its size suffix.
59
+ #
60
+ # @param base [String]
61
+ # @param suffix [String] digits, possibly empty
62
+ # @param type [String] the full type, for error messages
63
+ # @return [Array(String, Integer)] base and size (nil when the type takes none)
64
+ # @raise [BlockGiven::AbiError] for an unknown base or an invalid size
65
+ def self.elementary(base, suffix, type)
66
+ size = suffix.empty? ? nil : suffix.to_i
67
+ valid =
68
+ case base
69
+ when "uint", "int"
70
+ size ||= 256
71
+ size.between?(8, 256) && (size % 8).zero?
72
+ when "bytes" then size.nil? || size.between?(1, 32)
73
+ when "address", "bool", "string" then size.nil?
74
+ else false
75
+ end
76
+ raise AbiError, "unsupported ABI type #{type.inspect}" unless valid
77
+
78
+ [base, size]
79
+ end
80
+
81
+ # @param suffix [String] e.g. `[2][]`
82
+ # @return [Array<Integer, nil>]
83
+ def self.dimensions(suffix) = suffix.scan(/\[(\d*)\]/).map { |(digits)| digits.empty? ? nil : digits.to_i }
84
+
85
+ private_class_method :parse_tuple, :elementary, :dimensions
86
+
87
+ # @param base [String]
88
+ # @param size [Integer, nil]
89
+ # @param dimensions [Array<Integer, nil>]
90
+ # @param components [Array<Type>]
91
+ def initialize(base, size, dimensions, components = [])
92
+ @base = base
93
+ @size = size
94
+ @dimensions = dimensions
95
+ @components = components
96
+ end
97
+
98
+ # @return [Boolean] whether the type is an array
99
+ def array? = !dimensions.empty?
100
+
101
+ # @return [Type] the element type of an array (outermost dimension removed)
102
+ def element = Type.new(base, size, dimensions[0...-1], components)
103
+
104
+ # @return [Integer, nil] the outermost array length, nil for a dynamic array
105
+ def length = dimensions.last
106
+
107
+ # Whether the encoding is dynamic (stored behind an offset).
108
+ #
109
+ # @return [Boolean]
110
+ def dynamic?
111
+ return @dynamic unless @dynamic.nil?
112
+
113
+ @dynamic =
114
+ if array? then length.nil? || element.dynamic?
115
+ elsif base == "tuple" then components.any?(&:dynamic?)
116
+ else base == "string" || (base == "bytes" && size.nil?)
117
+ end
118
+ end
119
+
120
+ # Bytes taken by a static value in the head of its enclosing sequence (32 for dynamic types: the offset).
121
+ #
122
+ # @return [Integer]
123
+ def head_size
124
+ return 32 if dynamic?
125
+ return length * element.head_size if array?
126
+ return components.sum(&:head_size) if base == "tuple"
127
+
128
+ 32
129
+ end
130
+
131
+ # @return [String] the canonical type, e.g. `(uint256,bytes)[]`
132
+ def to_s
133
+ core = base == "tuple" ? "(#{components.join(',')})" : "#{base}#{size}"
134
+ core + dimensions.map { |d| "[#{d}]" }.join
135
+ end
136
+ end
137
+ end
138
+ end
@@ -2,10 +2,60 @@
2
2
 
3
3
  module BlockGiven
4
4
  # Static description of an EVM network, similar to viem/chains.
5
+ #
6
+ # A `Struct` with keyword initialisation; the constants in {Chains} are the instances applications use, but
7
+ # any network can be described by building one directly (e.g. a private fork with its own id).
8
+ #
9
+ # @example
10
+ # chain = BlockGiven::Chains::BASE
11
+ # chain.id # => 8453
12
+ # chain.testnet? # => false
13
+ # chain.explorer_tx_url("0xabc...") # => "https://basescan.org/tx/0xabc..."
14
+ #
15
+ # BlockGiven::Chain.new(id: 1337, name: "Fork", rpc_urls: "http://localhost:8545", testnet: true)
16
+ #
17
+ # @!attribute [rw] id
18
+ # EIP-155 chain id (`8453` for Base), also used in transaction signatures.
19
+ # @return [Integer]
20
+ # @!attribute [rw] name
21
+ # Human readable name (`"Base Sepolia"`).
22
+ # @return [String]
23
+ # @!attribute [rw] network
24
+ # URL-safe slug matched by {Chains.resolve} (`"base-sepolia"`); derived from `name` when not given.
25
+ # @return [String]
26
+ # @!attribute [rw] native_currency
27
+ # Native coin as `{ name:, symbol:, decimals: }` (defaults to `{ name: "Ether", symbol: "ETH", decimals: 18 }`).
28
+ # @return [Hash{Symbol => String, Integer}]
29
+ # @!attribute [rw] rpc_urls
30
+ # Public JSON-RPC endpoints; {Connectors::Http} falls back to the first one when built without a URL.
31
+ # @return [Array<String>]
32
+ # @!attribute [rw] block_explorer_url
33
+ # Base URL of the block explorer without trailing slash (`"https://basescan.org"`), or nil when there is none.
34
+ # @return [String, nil]
35
+ # @!attribute [rw] alchemy_network
36
+ # Alchemy subdomain for this network (`"base-mainnet"`), used by {Connectors::Alchemy} to build its
37
+ # endpoint; nil when Alchemy does not serve the network.
38
+ # @return [String, nil]
39
+ # @!attribute [rw] testnet
40
+ # Whether the network is a test network (default `false`); see {#testnet?}.
41
+ # @return [Boolean]
5
42
  Chain = Struct.new(
6
43
  :id, :name, :network, :native_currency, :rpc_urls, :block_explorer_url, :alchemy_network, :testnet,
7
44
  keyword_init: true
8
45
  ) do
46
+ # Build a chain description; only `id` and `name` are required.
47
+ #
48
+ # @param id [Integer] EIP-155 chain id
49
+ # @param name [String] human readable name
50
+ # @param network [String, nil] slug used for lookups; defaults to `name` downcased with runs of
51
+ # non-alphanumeric characters replaced by `-`
52
+ # @param native_currency [Hash{Symbol => String, Integer}, nil] `{ name:, symbol:, decimals: }`, defaults to
53
+ # Ether with 18 decimals
54
+ # @param rpc_urls [Array<String>, String] one or more public JSON-RPC endpoints (a single String is wrapped)
55
+ # @param block_explorer_url [String, nil] explorer base URL without trailing slash
56
+ # @param alchemy_network [String, nil] Alchemy subdomain (`"base-mainnet"`)
57
+ # @param testnet [Boolean] whether this is a test network
58
+ # @return [Chain]
9
59
  def initialize(id:, name:, network: nil, native_currency: nil, rpc_urls: [], block_explorer_url: nil,
10
60
  alchemy_network: nil, testnet: false)
11
61
  super(
@@ -16,85 +66,137 @@ module BlockGiven
16
66
  )
17
67
  end
18
68
 
69
+ # Whether this is a test network.
70
+ #
71
+ # @return [Boolean] `testnet` coerced to a strict boolean
19
72
  def testnet? = !!testnet
20
73
 
74
+ # Block explorer page of a transaction.
75
+ #
76
+ # @param hash [String] transaction hash as a `0x` hex string
77
+ # @return [String, nil] `"#{block_explorer_url}/tx/#{hash}"`, or nil when the chain has no explorer
21
78
  def explorer_tx_url(hash)
22
79
  block_explorer_url && "#{block_explorer_url}/tx/#{hash}"
23
80
  end
24
81
 
82
+ # Block explorer page of an address (account or contract).
83
+ #
84
+ # @param address [String] `0x` hex address
85
+ # @return [String, nil] `"#{block_explorer_url}/address/#{address}"`, or nil when the chain has no explorer
25
86
  def explorer_address_url(address)
26
87
  block_explorer_url && "#{block_explorer_url}/address/#{address}"
27
88
  end
28
89
 
90
+ # Short human readable form.
91
+ #
92
+ # @return [String] `"Base (8453)"`
29
93
  def to_s = "#{name} (#{id})"
94
+
95
+ # Compact inspection string, keeping RPC URLs and currency details out of logs.
96
+ #
97
+ # @return [String] `"#<BlockGiven::Chain Base (8453)>"`
30
98
  def inspect = "#<BlockGiven::Chain #{self}>"
31
99
  end
32
100
 
101
+ # Catalogue of known networks and the lookup used by {Configuration#chain=} and {Client#initialize}.
102
+ #
103
+ # Each constant is a {Chain}. {Chains.resolve} accepts a chain, a chain id, or a network slug as a Symbol or
104
+ # String (`:base_sepolia`, `"base-sepolia"`), so applications can keep the chain in an environment variable.
105
+ #
106
+ # @example
107
+ # BlockGiven::Chains.resolve(:base_sepolia) # => #<BlockGiven::Chain Base Sepolia (84532)>
108
+ # BlockGiven::Chains.resolve(8453) # => #<BlockGiven::Chain Base (8453)>
109
+ # BlockGiven::Chains[ENV.fetch("CHAIN")] # same as resolve
33
110
  module Chains
111
+ # Ethereum mainnet: id 1, explorer https://etherscan.io.
34
112
  MAINNET = Chain.new(
35
113
  id: 1, name: "Ethereum", network: "mainnet",
36
114
  rpc_urls: ["https://eth.merkle.io"], block_explorer_url: "https://etherscan.io",
37
115
  alchemy_network: "eth-mainnet"
38
116
  )
117
+ # Sepolia, the Ethereum testnet: id 11155111, explorer https://sepolia.etherscan.io.
39
118
  SEPOLIA = Chain.new(
40
119
  id: 11_155_111, name: "Sepolia", network: "sepolia",
41
120
  rpc_urls: ["https://sepolia.drpc.org"], block_explorer_url: "https://sepolia.etherscan.io",
42
121
  alchemy_network: "eth-sepolia", testnet: true
43
122
  )
123
+ # Base mainnet (Coinbase L2, where Bolero contracts are deployed): id 8453, explorer https://basescan.org.
44
124
  BASE = Chain.new(
45
125
  id: 8453, name: "Base", network: "base",
46
126
  rpc_urls: ["https://mainnet.base.org"], block_explorer_url: "https://basescan.org",
47
127
  alchemy_network: "base-mainnet"
48
128
  )
129
+ # Base Sepolia testnet: id 84532, explorer https://sepolia.basescan.org.
49
130
  BASE_SEPOLIA = Chain.new(
50
131
  id: 84_532, name: "Base Sepolia", network: "base-sepolia",
51
132
  rpc_urls: ["https://sepolia.base.org"], block_explorer_url: "https://sepolia.basescan.org",
52
133
  alchemy_network: "base-sepolia", testnet: true
53
134
  )
135
+ # Polygon PoS mainnet (native currency POL): id 137, explorer https://polygonscan.com.
54
136
  POLYGON = Chain.new(
55
137
  id: 137, name: "Polygon", network: "polygon",
56
138
  native_currency: { name: "POL", symbol: "POL", decimals: 18 },
57
139
  rpc_urls: ["https://polygon-rpc.com"], block_explorer_url: "https://polygonscan.com",
58
140
  alchemy_network: "polygon-mainnet"
59
141
  )
142
+ # Polygon Amoy testnet (native currency POL): id 80002, explorer https://amoy.polygonscan.com.
60
143
  POLYGON_AMOY = Chain.new(
61
144
  id: 80_002, name: "Polygon Amoy", network: "polygon-amoy",
62
145
  native_currency: { name: "POL", symbol: "POL", decimals: 18 },
63
146
  rpc_urls: ["https://rpc-amoy.polygon.technology"], block_explorer_url: "https://amoy.polygonscan.com",
64
147
  alchemy_network: "polygon-amoy", testnet: true
65
148
  )
149
+ # Arbitrum One mainnet: id 42161, explorer https://arbiscan.io.
66
150
  ARBITRUM = Chain.new(
67
151
  id: 42_161, name: "Arbitrum One", network: "arbitrum",
68
152
  rpc_urls: ["https://arb1.arbitrum.io/rpc"], block_explorer_url: "https://arbiscan.io",
69
153
  alchemy_network: "arb-mainnet"
70
154
  )
155
+ # Arbitrum Sepolia testnet: id 421614, explorer https://sepolia.arbiscan.io.
71
156
  ARBITRUM_SEPOLIA = Chain.new(
72
157
  id: 421_614, name: "Arbitrum Sepolia", network: "arbitrum-sepolia",
73
158
  rpc_urls: ["https://sepolia-rollup.arbitrum.io/rpc"], block_explorer_url: "https://sepolia.arbiscan.io",
74
159
  alchemy_network: "arb-sepolia", testnet: true
75
160
  )
161
+ # OP Mainnet (Optimism): id 10, explorer https://optimistic.etherscan.io.
76
162
  OPTIMISM = Chain.new(
77
163
  id: 10, name: "OP Mainnet", network: "optimism",
78
164
  rpc_urls: ["https://mainnet.optimism.io"], block_explorer_url: "https://optimistic.etherscan.io",
79
165
  alchemy_network: "opt-mainnet"
80
166
  )
167
+ # OP Sepolia testnet: id 11155420, explorer https://sepolia-optimism.etherscan.io.
81
168
  OPTIMISM_SEPOLIA = Chain.new(
82
169
  id: 11_155_420, name: "OP Sepolia", network: "optimism-sepolia",
83
170
  rpc_urls: ["https://sepolia.optimism.io"], block_explorer_url: "https://sepolia-optimism.etherscan.io",
84
171
  alchemy_network: "opt-sepolia", testnet: true
85
172
  )
86
- # Hardhat / Anvil / Ganache default chain, handy with a local fork.
173
+ # Hardhat / Anvil / Ganache default chain (id 31337, RPC at http://127.0.0.1:8545, no explorer), handy with
174
+ # a local fork.
87
175
  LOCALHOST = Chain.new(
88
176
  id: 31_337, name: "Localhost", network: "localhost",
89
177
  rpc_urls: ["http://127.0.0.1:8545"], testnet: true
90
178
  )
91
179
 
180
+ # Every chain known to the gem, in declaration order; {.resolve} and {.find_by_id} search this list.
92
181
  ALL = [MAINNET, SEPOLIA, BASE, BASE_SEPOLIA, POLYGON, POLYGON_AMOY, ARBITRUM, ARBITRUM_SEPOLIA,
93
182
  OPTIMISM, OPTIMISM_SEPOLIA, LOCALHOST].freeze
94
183
 
95
184
  module_function
96
185
 
97
- # Chains.resolve(:base) / Chains.resolve("base-sepolia") / Chains.resolve(8453) / Chains.resolve(chain)
186
+ # Resolve a chain from a {Chain}, a chain id, or a network slug given as Symbol or String.
187
+ #
188
+ # Slugs are compared case-insensitively against {Chain#network} after mapping underscores to dashes, so
189
+ # `:base_sepolia`, `"BASE_SEPOLIA"` and `"base-sepolia"` all resolve to {BASE_SEPOLIA}. A {Chain} is
190
+ # returned as-is, which lets custom chains flow through the same code path.
191
+ #
192
+ # @param value [BlockGiven::Chain, Integer, Symbol, String]
193
+ # @return [BlockGiven::Chain]
194
+ # @raise [BlockGiven::ConfigurationError] when the id or slug is unknown, or the value has another type
195
+ # @example
196
+ # BlockGiven::Chains.resolve(:base) # => #<BlockGiven::Chain Base (8453)>
197
+ # BlockGiven::Chains.resolve("base-sepolia") # => #<BlockGiven::Chain Base Sepolia (84532)>
198
+ # BlockGiven::Chains.resolve(8453) # => #<BlockGiven::Chain Base (8453)>
199
+ # BlockGiven::Chains.resolve(:mumbai) # raises BlockGiven::ConfigurationError
98
200
  def resolve(value)
99
201
  case value
100
202
  when Chain then value
@@ -106,7 +208,17 @@ module BlockGiven
106
208
  end
107
209
  end
108
210
 
211
+ # Look a known chain up by its chain id.
212
+ #
213
+ # @param id [Integer] EIP-155 chain id
214
+ # @return [BlockGiven::Chain, nil] nil when no chain in {ALL} has this id
109
215
  def find_by_id(id) = ALL.find { |c| c.id == id }
216
+
217
+ # Shorthand for {.resolve}, so `Chains[:base]` reads like a lookup table.
218
+ #
219
+ # @param value [BlockGiven::Chain, Integer, Symbol, String]
220
+ # @return [BlockGiven::Chain]
221
+ # @raise [BlockGiven::ConfigurationError] when the value cannot be resolved
110
222
  def [](value) = resolve(value)
111
223
  end
112
224
  end