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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -1
- data/README.md +92 -55
- data/lib/block_given/abi/codec/decoder.rb +111 -0
- data/lib/block_given/abi/codec.rb +170 -0
- data/lib/block_given/abi/coder.rb +122 -11
- data/lib/block_given/abi/custom_error.rb +33 -1
- data/lib/block_given/abi/event.rb +85 -3
- data/lib/block_given/abi/function.rb +94 -2
- data/lib/block_given/abi/interface.rb +99 -4
- data/lib/block_given/abi/parameter.rb +60 -3
- data/lib/block_given/abi/standards/erc1155.rb +41 -0
- data/lib/block_given/abi/standards/erc20.rb +33 -0
- data/lib/block_given/abi/standards/erc4626.rb +41 -0
- data/lib/block_given/abi/standards/erc721.rb +47 -0
- data/lib/block_given/abi/standards.rb +124 -0
- data/lib/block_given/abi/type.rb +138 -0
- data/lib/block_given/chain.rb +114 -2
- data/lib/block_given/client.rb +322 -20
- data/lib/block_given/configuration.rb +85 -3
- data/lib/block_given/connectors/alchemy.rb +38 -4
- data/lib/block_given/connectors/base.rb +32 -5
- data/lib/block_given/connectors/http.rb +91 -5
- data/lib/block_given/connectors/stub.rb +70 -4
- data/lib/block_given/contract.rb +422 -26
- data/lib/block_given/crypto/keccak.rb +152 -0
- data/lib/block_given/crypto/secp256k1.rb +168 -0
- data/lib/block_given/crypto.rb +22 -0
- data/lib/block_given/eip712.rb +199 -0
- data/lib/block_given/errors.rb +134 -14
- data/lib/block_given/event.rb +51 -1
- data/lib/block_given/normalizer.rb +27 -2
- data/lib/block_given/poller.rb +177 -15
- data/lib/block_given/receipt.rb +63 -3
- data/lib/block_given/rlp.rb +146 -0
- data/lib/block_given/signed_transaction.rb +148 -47
- data/lib/block_given/transaction.rb +103 -12
- data/lib/block_given/transaction_envelope/fields.rb +104 -0
- data/lib/block_given/transaction_envelope.rb +183 -0
- data/lib/block_given/utils.rb +143 -9
- data/lib/block_given/version.rb +2 -1
- data/lib/block_given/wallet.rb +216 -35
- data/lib/block_given.rb +67 -4
- metadata +19 -23
|
@@ -2,11 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
module BlockGiven
|
|
4
4
|
module Abi
|
|
5
|
-
# Turns Ruby values into what the ABI encoder expects, and decoded values
|
|
6
|
-
#
|
|
5
|
+
# Turns Ruby values into what the ABI encoder expects, and decoded values into idiomatic Ruby.
|
|
6
|
+
#
|
|
7
|
+
# ## Input coercion (Ruby to ABI), per Solidity type
|
|
8
|
+
#
|
|
9
|
+
# - `intN` / `uintN`: `Integer`; a whole `Float`, `BigDecimal` or `Rational` (`1e6` becomes
|
|
10
|
+
# `1000000`); a decimal String (`"1000000"`) or a `0x` hex String. A fractional or non-finite number
|
|
11
|
+
# raises `InvalidArgumentError` pointing at `BlockGiven::Utils.parse_units`.
|
|
12
|
+
# - `address`: a `0x` 40-hex-digit String, or any non-String object responding to `#address` (a
|
|
13
|
+
# {BlockGiven::Wallet}, a {BlockGiven::Contract}). Malformed values raise `InvalidAddressError`.
|
|
14
|
+
# - `bool`: `true` or `false` only (no truthiness).
|
|
15
|
+
# - `string`: anything, converted with `#to_s`.
|
|
16
|
+
# - `bytes` / `bytesN`: a `0x` hex String, or a binary String which is hex-encoded.
|
|
17
|
+
# - tuples: a Hash keyed by component name (Symbol or String, snake_case or camelCase, matched after
|
|
18
|
+
# `Utils.snake_case`) or an Array in declaration order; components are coerced recursively.
|
|
19
|
+
# - arrays: an Array whose elements are coerced recursively with the element type.
|
|
20
|
+
#
|
|
21
|
+
# ## Output formatting (ABI to Ruby)
|
|
22
|
+
#
|
|
23
|
+
# - `address`: EIP-55 checksummed String.
|
|
24
|
+
# - `bytes` / `bytesN`: `0x` hex String.
|
|
25
|
+
# - `string`: UTF-8 String.
|
|
26
|
+
# - integers and booleans: unchanged (`Integer`, `true` / `false`).
|
|
27
|
+
# - tuples: a Hash with snake_case Symbol keys when every component is named, an Array otherwise.
|
|
28
|
+
# - arrays: an Array of formatted elements.
|
|
29
|
+
#
|
|
30
|
+
# {Function#decode_output} additionally unwraps a single output and returns several as an Array.
|
|
7
31
|
module Coder
|
|
8
32
|
module_function
|
|
9
33
|
|
|
34
|
+
# ABI-encodes values for the given parameters after coercing them (see the module documentation).
|
|
35
|
+
#
|
|
36
|
+
# @param params [Array<Parameter>] the parameter types, in order
|
|
37
|
+
# @param values [Array] one Ruby value per parameter
|
|
38
|
+
# @return [String] `0x`-prefixed encoded data (no selector)
|
|
39
|
+
# @raise [BlockGiven::InvalidArgumentError] when the count differs or a value cannot be coerced
|
|
40
|
+
# @raise [BlockGiven::InvalidAddressError] when an address value is malformed
|
|
41
|
+
# @raise [BlockGiven::AbiEncodingError] when the encoder rejects a coerced value (out of bounds, ...)
|
|
42
|
+
# @raise [BlockGiven::AbiError] when a type is not supported
|
|
10
43
|
def encode(params, values)
|
|
11
44
|
if params.size != values.size
|
|
12
45
|
raise InvalidArgumentError,
|
|
@@ -14,24 +47,42 @@ module BlockGiven
|
|
|
14
47
|
end
|
|
15
48
|
|
|
16
49
|
coerced = params.zip(values).map { |param, value| coerce(value, param) }
|
|
17
|
-
Utils.bin_to_hex(
|
|
18
|
-
rescue
|
|
19
|
-
raise
|
|
50
|
+
Utils.bin_to_hex(Codec.encode(params.map(&:type), coerced))
|
|
51
|
+
rescue AbiEncodingError => e
|
|
52
|
+
raise AbiEncodingError, "ABI encoding failed: #{e.message}"
|
|
20
53
|
end
|
|
21
54
|
|
|
55
|
+
# ABI-decodes data for the given parameters and formats the values (see the module documentation).
|
|
56
|
+
#
|
|
57
|
+
# @param params [Array<Parameter>] the parameter types, in order
|
|
58
|
+
# @param hex [String] `0x`-prefixed encoded data
|
|
59
|
+
# @return [Array] one formatted Ruby value per parameter (empty when `params` is empty, whatever
|
|
60
|
+
# the data)
|
|
61
|
+
# @raise [BlockGiven::AbiError] when the data is empty (typically no contract at the address)
|
|
62
|
+
# @raise [BlockGiven::AbiDecodingError] when the data is not hex or too short for the types
|
|
22
63
|
def decode(params, hex)
|
|
23
64
|
return [] if params.empty?
|
|
24
65
|
|
|
25
66
|
data = Utils.strip_hex(hex.to_s)
|
|
26
67
|
raise AbiError, "cannot decode empty data (does the contract exist at this address?)" if data.empty?
|
|
68
|
+
raise AbiDecodingError, "data is not hex" unless data.match?(/\A(\h\h)+\z/)
|
|
27
69
|
|
|
28
|
-
values =
|
|
70
|
+
values = Codec.decode(params.map(&:type), Utils.hex_to_bin(data))
|
|
29
71
|
params.zip(values).map { |param, value| format(value, param) }
|
|
30
|
-
rescue
|
|
31
|
-
raise
|
|
72
|
+
rescue AbiDecodingError => e
|
|
73
|
+
raise AbiDecodingError, "ABI decoding failed: #{e.message}"
|
|
32
74
|
end
|
|
33
75
|
|
|
34
|
-
# Ruby
|
|
76
|
+
# Coerces one Ruby value into the encoder input for a parameter (recursing into arrays and tuples).
|
|
77
|
+
#
|
|
78
|
+
# Types without a dedicated rule (`fixed`, `function`...) are passed through unchanged.
|
|
79
|
+
#
|
|
80
|
+
# @api private
|
|
81
|
+
# @param value [Object] the Ruby value
|
|
82
|
+
# @param param [Parameter] the target parameter
|
|
83
|
+
# @return [Object] the encoder-ready value
|
|
84
|
+
# @raise [BlockGiven::InvalidArgumentError] when the value does not fit the type
|
|
85
|
+
# @raise [BlockGiven::InvalidAddressError] when an address is malformed
|
|
35
86
|
def coerce(value, param)
|
|
36
87
|
if param.array?
|
|
37
88
|
raise InvalidArgumentError, "#{param.name} expects an Array, got #{value.inspect}" unless value.is_a?(Array)
|
|
@@ -50,19 +101,34 @@ module BlockGiven
|
|
|
50
101
|
end
|
|
51
102
|
end
|
|
52
103
|
|
|
53
|
-
#
|
|
104
|
+
# Formats one decoded value into idiomatic Ruby for a parameter (recursing into arrays and tuples).
|
|
105
|
+
#
|
|
106
|
+
# @api private
|
|
107
|
+
# @param value [Object] the value returned by {Codec.decode}
|
|
108
|
+
# @param param [Parameter] the decoded parameter
|
|
109
|
+
# @return [Object] checksummed address, `0x` hex bytes, UTF-8 string, Hash or Array for tuples, or
|
|
110
|
+
# the value unchanged
|
|
54
111
|
def format(value, param)
|
|
55
112
|
return value.map { |v| format(v, param.element) } if param.array?
|
|
56
113
|
return format_tuple(value, param) if param.tuple?
|
|
57
114
|
|
|
58
115
|
case param.raw_type
|
|
59
116
|
when "address" then Utils.checksum_address(value)
|
|
60
|
-
when /\Abytes\d*\z/ then
|
|
117
|
+
when /\Abytes\d*\z/ then Utils.bin_to_hex(value)
|
|
61
118
|
when "string" then value.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
62
119
|
else value
|
|
63
120
|
end
|
|
64
121
|
end
|
|
65
122
|
|
|
123
|
+
# Coerces a value for an `intN` / `uintN` parameter.
|
|
124
|
+
#
|
|
125
|
+
# @api private
|
|
126
|
+
# @param value [Integer, Float, BigDecimal, Rational, String] an Integer, a whole number, a decimal
|
|
127
|
+
# String or a `0x` hex String
|
|
128
|
+
# @param param [Parameter]
|
|
129
|
+
# @return [Integer]
|
|
130
|
+
# @raise [BlockGiven::InvalidArgumentError] for fractional or non-finite numbers (use
|
|
131
|
+
# `Utils.parse_units`), unparsable Strings or any other type
|
|
66
132
|
def coerce_integer(value, param)
|
|
67
133
|
case value
|
|
68
134
|
when Integer then value
|
|
@@ -80,6 +146,13 @@ module BlockGiven
|
|
|
80
146
|
raise InvalidArgumentError, "#{param.name} expects an integer, got #{value.inspect}"
|
|
81
147
|
end
|
|
82
148
|
|
|
149
|
+
# Coerces a value for an `address` parameter.
|
|
150
|
+
#
|
|
151
|
+
# @api private
|
|
152
|
+
# @param value [String, #address] a `0x` hex address or a non-String object responding to `#address`
|
|
153
|
+
# @param param [Parameter]
|
|
154
|
+
# @return [String] the address as given (not checksummed)
|
|
155
|
+
# @raise [BlockGiven::InvalidAddressError] when the resulting value is not a 20-byte hex address
|
|
83
156
|
def coerce_address(value, param)
|
|
84
157
|
value = value.address if value.respond_to?(:address) && !value.is_a?(String)
|
|
85
158
|
raise InvalidAddressError, "#{param.name}: invalid address #{value.inspect}" unless Utils.address?(value)
|
|
@@ -87,18 +160,41 @@ module BlockGiven
|
|
|
87
160
|
value
|
|
88
161
|
end
|
|
89
162
|
|
|
163
|
+
# Coerces a value for a `bool` parameter; only `true` and `false` are accepted.
|
|
164
|
+
#
|
|
165
|
+
# @api private
|
|
166
|
+
# @param value [Boolean]
|
|
167
|
+
# @param param [Parameter]
|
|
168
|
+
# @return [Boolean]
|
|
169
|
+
# @raise [BlockGiven::InvalidArgumentError] for anything else (no truthiness)
|
|
90
170
|
def coerce_bool(value, param)
|
|
91
171
|
return value if [true, false].include?(value)
|
|
92
172
|
|
|
93
173
|
raise InvalidArgumentError, "#{param.name} expects true/false, got #{value.inspect}"
|
|
94
174
|
end
|
|
95
175
|
|
|
176
|
+
# Coerces a value for a `bytes` / `bytesN` parameter.
|
|
177
|
+
#
|
|
178
|
+
# @api private
|
|
179
|
+
# @param value [String] a `0x` hex String (kept) or a binary String (hex-encoded)
|
|
180
|
+
# @param param [Parameter]
|
|
181
|
+
# @return [String] `0x` hex
|
|
182
|
+
# @raise [BlockGiven::InvalidArgumentError] when the value is not a String
|
|
96
183
|
def coerce_bytes(value, param)
|
|
97
184
|
raise InvalidArgumentError, "#{param.name} expects a hex or binary String" unless value.is_a?(String)
|
|
98
185
|
|
|
99
186
|
Utils.hex?(value) ? value : Utils.bin_to_hex(value)
|
|
100
187
|
end
|
|
101
188
|
|
|
189
|
+
# Coerces a value for a tuple parameter into an Array of coerced components.
|
|
190
|
+
#
|
|
191
|
+
# @api private
|
|
192
|
+
# @param value [Hash, Array] components by name (see {.tuple_values_from_hash}) or in declaration
|
|
193
|
+
# order
|
|
194
|
+
# @param param [Parameter] a tuple parameter
|
|
195
|
+
# @return [Array] coerced component values, in declaration order
|
|
196
|
+
# @raise [BlockGiven::InvalidArgumentError] when the value is neither a Hash nor an Array, when the
|
|
197
|
+
# number of values differs from the number of components, or when a field is missing
|
|
102
198
|
def coerce_tuple(value, param)
|
|
103
199
|
values =
|
|
104
200
|
case value
|
|
@@ -113,6 +209,14 @@ module BlockGiven
|
|
|
113
209
|
param.components.zip(values).map { |component, v| coerce(v, component) }
|
|
114
210
|
end
|
|
115
211
|
|
|
212
|
+
# Orders the values of a tuple Hash by component; keys match component names after `Utils.snake_case`
|
|
213
|
+
# (Symbol or String, snake_case or camelCase). Extra keys are ignored.
|
|
214
|
+
#
|
|
215
|
+
# @api private
|
|
216
|
+
# @param hash [Hash] component values by name
|
|
217
|
+
# @param param [Parameter] a tuple parameter
|
|
218
|
+
# @return [Array] values in component declaration order
|
|
219
|
+
# @raise [BlockGiven::InvalidArgumentError] when a component has no matching key
|
|
116
220
|
def tuple_values_from_hash(hash, param)
|
|
117
221
|
lookup = hash.transform_keys { |k| Utils.snake_case(k) }
|
|
118
222
|
param.components.map do |component|
|
|
@@ -123,6 +227,13 @@ module BlockGiven
|
|
|
123
227
|
end
|
|
124
228
|
end
|
|
125
229
|
|
|
230
|
+
# Formats a decoded tuple: a Hash with snake_case Symbol keys when every component is named, an
|
|
231
|
+
# Array otherwise.
|
|
232
|
+
#
|
|
233
|
+
# @api private
|
|
234
|
+
# @param values [Array] decoded component values
|
|
235
|
+
# @param param [Parameter] a tuple parameter
|
|
236
|
+
# @return [Hash{Symbol => Object}, Array]
|
|
126
237
|
def format_tuple(values, param)
|
|
127
238
|
formatted = param.components.zip(values).map { |component, v| format(v, component) }
|
|
128
239
|
return formatted if param.components.any?(&:unnamed?)
|
|
@@ -3,19 +3,48 @@
|
|
|
3
3
|
module BlockGiven
|
|
4
4
|
module Abi
|
|
5
5
|
# Solidity custom error (`error InsufficientBalance(uint256 available, uint256 required)`).
|
|
6
|
+
#
|
|
7
|
+
# Used by `BlockGiven::ContractRevertError#decode_with` to name a revert and decode its arguments.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# error = interface.error_by_selector(revert_data[0, 10])
|
|
11
|
+
# error.signature # => "InsufficientBalance(uint256,uint256)"
|
|
12
|
+
# error.decode(revert_data) # => { available: 5, required: 10 }
|
|
6
13
|
class CustomError
|
|
14
|
+
# @!attribute [r] name
|
|
15
|
+
# @return [String] the Solidity name (`"InsufficientBalance"`)
|
|
16
|
+
# @!attribute [r] inputs
|
|
17
|
+
# @return [Array<Parameter>] the error arguments, in declaration order
|
|
7
18
|
attr_reader :name, :inputs
|
|
8
19
|
|
|
20
|
+
# Builds a custom error from its ABI definition.
|
|
21
|
+
#
|
|
22
|
+
# @param definition [Hash] the ABI entry (`"name"`, `"inputs"`), String or Symbol keys
|
|
9
23
|
def initialize(definition)
|
|
10
24
|
definition = definition.transform_keys(&:to_s)
|
|
11
25
|
@name = definition["name"].to_s
|
|
12
26
|
@inputs = Array(definition["inputs"]).each_with_index.map { |i, idx| Parameter.new(i, index: idx) }
|
|
13
27
|
end
|
|
14
28
|
|
|
29
|
+
# The canonical signature, with tuples expanded (`"InsufficientBalance(uint256,uint256)"`).
|
|
30
|
+
#
|
|
31
|
+
# @return [String]
|
|
15
32
|
def signature = "#{name}(#{inputs.map(&:type).join(',')})"
|
|
33
|
+
|
|
34
|
+
# The 4-byte selector: first 4 bytes of `keccak256(signature)`, `0x`-prefixed lowercase (memoized).
|
|
35
|
+
#
|
|
36
|
+
# @return [String]
|
|
16
37
|
def selector = @selector ||= Utils.keccak256(signature)[0, 10]
|
|
17
38
|
|
|
18
|
-
#
|
|
39
|
+
# Decodes the arguments of revert data produced by this error.
|
|
40
|
+
#
|
|
41
|
+
# The 4-byte selector at the start of the data is skipped, the remainder is decoded against
|
|
42
|
+
# {#inputs} with {Coder.decode}.
|
|
43
|
+
#
|
|
44
|
+
# @param revert_data [String] the full revert data (`0x` + selector + encoded arguments)
|
|
45
|
+
# @return [Hash{Symbol => Object}] decoded arguments keyed by snake_case name, in declaration order;
|
|
46
|
+
# empty for an error without arguments
|
|
47
|
+
# @raise [BlockGiven::AbiError] when the data cannot be decoded against the inputs
|
|
19
48
|
def decode(revert_data)
|
|
20
49
|
payload = "0x#{Utils.strip_hex(revert_data)[8..]}"
|
|
21
50
|
return {} if inputs.empty?
|
|
@@ -23,6 +52,9 @@ module BlockGiven
|
|
|
23
52
|
inputs.map(&:ruby_name).zip(Coder.decode(inputs, payload)).to_h
|
|
24
53
|
end
|
|
25
54
|
|
|
55
|
+
# Compact representation with the signature.
|
|
56
|
+
#
|
|
57
|
+
# @return [String]
|
|
26
58
|
def inspect = "#<BlockGiven::Abi::CustomError #{signature}>"
|
|
27
59
|
end
|
|
28
60
|
end
|
|
@@ -2,9 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
module BlockGiven
|
|
4
4
|
module Abi
|
|
5
|
+
# One ABI event: builds `eth_getLogs` topic filters and decodes logs into {BlockGiven::Event}s.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# transfer = interface.event(:Transfer)
|
|
9
|
+
# transfer.topic # => "0xddf252ad..."
|
|
10
|
+
# transfer.encode_topics(to: wallet.address) # => ["0xddf252ad...", nil, "0x000...wallet"]
|
|
11
|
+
# transfer.decode(log).args # => { from: "0x...", to: "0x...", value: 1000000 }
|
|
5
12
|
class Event
|
|
13
|
+
# @!attribute [r] name
|
|
14
|
+
# @return [String] the Solidity name (`"Transfer"`)
|
|
15
|
+
# @!attribute [r] inputs
|
|
16
|
+
# @return [Array<Parameter>] the parameters, indexed and not, in declaration order
|
|
17
|
+
# @!attribute [r] anonymous
|
|
18
|
+
# @return [Boolean] whether the event is declared `anonymous` (no signature topic)
|
|
6
19
|
attr_reader :name, :inputs, :anonymous
|
|
7
20
|
|
|
21
|
+
# Builds an event from its ABI definition.
|
|
22
|
+
#
|
|
23
|
+
# @param definition [Hash] the ABI entry (`"name"`, `"inputs"`, `"anonymous"`), String or Symbol keys
|
|
8
24
|
def initialize(definition)
|
|
9
25
|
definition = definition.transform_keys(&:to_s)
|
|
10
26
|
@name = definition["name"].to_s
|
|
@@ -12,20 +28,62 @@ module BlockGiven
|
|
|
12
28
|
@anonymous = !!definition["anonymous"]
|
|
13
29
|
end
|
|
14
30
|
|
|
31
|
+
# The snake_case Ruby name (`Transfer` becomes `:transfer`, `OwnershipTransferred` becomes
|
|
32
|
+
# `:ownership_transferred`).
|
|
33
|
+
#
|
|
34
|
+
# @return [Symbol]
|
|
15
35
|
def ruby_name = Utils.snake_case(name).to_sym
|
|
36
|
+
|
|
37
|
+
# The canonical signature, with tuples expanded (`"Transfer(address,address,uint256)"`).
|
|
38
|
+
#
|
|
39
|
+
# @return [String]
|
|
16
40
|
def signature = "#{name}(#{inputs.map(&:type).join(',')})"
|
|
41
|
+
|
|
42
|
+
# The `topics[0]` value of the event: `keccak256(signature)`, `0x`-prefixed lowercase (memoized).
|
|
43
|
+
#
|
|
44
|
+
# @return [String]
|
|
17
45
|
def topic = @topic ||= Utils.keccak256(signature)
|
|
46
|
+
|
|
47
|
+
# Whether the event is anonymous (its logs carry no signature topic).
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean]
|
|
18
50
|
def anonymous? = anonymous
|
|
19
51
|
|
|
52
|
+
# Parameters declared `indexed`, in declaration order (they live in `topics[1..]`).
|
|
53
|
+
#
|
|
54
|
+
# @return [Array<Parameter>]
|
|
20
55
|
def indexed_inputs = inputs.select(&:indexed?)
|
|
56
|
+
|
|
57
|
+
# Parameters not declared `indexed`, in declaration order (they are ABI-encoded in `data`).
|
|
58
|
+
#
|
|
59
|
+
# @return [Array<Parameter>]
|
|
21
60
|
def data_inputs = inputs.reject(&:indexed?)
|
|
22
61
|
|
|
62
|
+
# Whether a log was emitted by this (non-anonymous) event, based on its first topic.
|
|
63
|
+
#
|
|
64
|
+
# @param log [Hash] a log with a `:topics` or `"topics"` key
|
|
65
|
+
# @return [Boolean] always `false` for anonymous events
|
|
23
66
|
def matches?(log)
|
|
24
67
|
topic0 = Array(log[:topics] || log["topics"]).first
|
|
25
68
|
!anonymous? && topic0&.downcase == topic
|
|
26
69
|
end
|
|
27
70
|
|
|
28
|
-
# Decodes a
|
|
71
|
+
# Decodes a log into a {BlockGiven::Event}.
|
|
72
|
+
#
|
|
73
|
+
# Indexed parameters are read from the topics (after `topics[0]`, unless the event is anonymous)
|
|
74
|
+
# and non-indexed ones from `data`. Arguments are keyed by snake_case name in declaration order.
|
|
75
|
+
# Indexed parameters of dynamic type (`string`, `bytes`, arrays, dynamic tuples) cannot be
|
|
76
|
+
# recovered: their keccak hash topic is kept as the value. A missing topic yields `nil`.
|
|
77
|
+
#
|
|
78
|
+
# @param log [Hash] a log Hash with `:topics` / `"topics"` and `:data` / `"data"` keys (a normalized
|
|
79
|
+
# log from {BlockGiven::Client} or a raw JSON-RPC log)
|
|
80
|
+
# @return [BlockGiven::Event] the decoded event, keeping the original log
|
|
81
|
+
# @raise [BlockGiven::AbiError] when `data` cannot be decoded against the non-indexed parameters
|
|
82
|
+
# @example
|
|
83
|
+
# event = interface.event(:Transfer).decode(log)
|
|
84
|
+
# event.args # => { from: "0x...", to: "0x...", value: 1000000 }
|
|
85
|
+
# event[:value] # => 1000000
|
|
86
|
+
# event.block_number # => 18000000
|
|
29
87
|
def decode(log)
|
|
30
88
|
topics = Array(log[:topics] || log["topics"])
|
|
31
89
|
data = log[:data] || log["data"] || "0x"
|
|
@@ -45,8 +103,23 @@ module BlockGiven
|
|
|
45
103
|
BlockGiven::Event.new(name: name, signature: signature, args: ordered, log: log)
|
|
46
104
|
end
|
|
47
105
|
|
|
48
|
-
# Builds the topics filter
|
|
49
|
-
#
|
|
106
|
+
# Builds the `topics` filter of `eth_getLogs` from indexed argument values.
|
|
107
|
+
#
|
|
108
|
+
# The first topic is {#topic} (omitted for anonymous events), followed by one entry per indexed
|
|
109
|
+
# parameter: `nil` is a wildcard, a single value matches that value, an Array matches any of its
|
|
110
|
+
# values (OR). Trailing wildcards are trimmed. Values are coerced with {Coder.encode}; dynamic types
|
|
111
|
+
# (`string`, `bytes`, arrays, dynamic tuples) are hashed with keccak256 as the EVM does.
|
|
112
|
+
#
|
|
113
|
+
# @param filters [Hash{Symbol, String => Object, Array, nil}] values by indexed parameter name
|
|
114
|
+
# (snake_case or camelCase)
|
|
115
|
+
# @return [Array<String, Array<String>, nil>] the topics filter
|
|
116
|
+
# @raise [BlockGiven::InvalidArgumentError] when a key is not an indexed parameter, or when a value
|
|
117
|
+
# cannot be coerced
|
|
118
|
+
# @raise [BlockGiven::InvalidAddressError] when an address value is malformed
|
|
119
|
+
# @example
|
|
120
|
+
# transfer.encode_topics(to: me) # => [topic, nil, "0x000...me"]
|
|
121
|
+
# transfer.encode_topics(from: [a, b]) # => [topic, ["0x000...a", "0x000...b"]]
|
|
122
|
+
# transfer.encode_topics # => [topic]
|
|
50
123
|
def encode_topics(filters = {})
|
|
51
124
|
normalized = filters.transform_keys { |k| Utils.snake_case(k).to_sym }
|
|
52
125
|
unknown = normalized.keys - indexed_inputs.map(&:ruby_name)
|
|
@@ -65,11 +138,19 @@ module BlockGiven
|
|
|
65
138
|
anonymous? ? topics : [topic, *topics]
|
|
66
139
|
end
|
|
67
140
|
|
|
141
|
+
# The canonical signature (same as {#signature}).
|
|
142
|
+
#
|
|
143
|
+
# @return [String]
|
|
68
144
|
def to_s = signature
|
|
145
|
+
|
|
146
|
+
# Compact representation with the signature.
|
|
147
|
+
#
|
|
148
|
+
# @return [String]
|
|
69
149
|
def inspect = "#<BlockGiven::Abi::Event #{signature}>"
|
|
70
150
|
|
|
71
151
|
private
|
|
72
152
|
|
|
153
|
+
# Decodes one indexed topic; dynamic types keep the hash since the value itself is not in the log.
|
|
73
154
|
def decode_topic(topic, param)
|
|
74
155
|
return nil if topic.nil?
|
|
75
156
|
return topic if param.dynamic? # only the keccak hash of the value is available
|
|
@@ -77,6 +158,7 @@ module BlockGiven
|
|
|
77
158
|
Coder.decode([param], topic).first
|
|
78
159
|
end
|
|
79
160
|
|
|
161
|
+
# Encodes one filter value as a 32-byte topic (keccak256 for dynamic types).
|
|
80
162
|
def encode_topic(value, param)
|
|
81
163
|
return Utils.keccak256(param.raw_type == "string" ? value.to_s : value) if param.dynamic?
|
|
82
164
|
|
|
@@ -2,9 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
module BlockGiven
|
|
4
4
|
module Abi
|
|
5
|
+
# One ABI function: encodes calldata from Ruby arguments and decodes return data.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# transfer = interface.function(:transfer)
|
|
9
|
+
# transfer.signature # => "transfer(address,uint256)"
|
|
10
|
+
# transfer.selector # => "0xa9059cbb"
|
|
11
|
+
# transfer.encode([to, 1_000_000]) # => "0xa9059cbb000000..."
|
|
12
|
+
# transfer.encode([], { to: to, value: 1e6 }) # keyword form, same result
|
|
13
|
+
# transfer.decode_output("0x0000...0001") # => true
|
|
5
14
|
class Function
|
|
15
|
+
# @!attribute [r] name
|
|
16
|
+
# @return [String] the Solidity name (`"balanceOf"`)
|
|
17
|
+
# @!attribute [r] inputs
|
|
18
|
+
# @return [Array<Parameter>] the input parameters, in declaration order
|
|
19
|
+
# @!attribute [r] outputs
|
|
20
|
+
# @return [Array<Parameter>] the output parameters, in declaration order
|
|
21
|
+
# @!attribute [r] state_mutability
|
|
22
|
+
# @return [String] `"view"`, `"pure"`, `"nonpayable"` or `"payable"`
|
|
6
23
|
attr_reader :name, :inputs, :outputs, :state_mutability
|
|
7
24
|
|
|
25
|
+
# Builds a function from its ABI definition.
|
|
26
|
+
#
|
|
27
|
+
# Pre-Solidity 0.5 ABIs without `stateMutability` are supported: `payable: true` maps to
|
|
28
|
+
# `"payable"`, `constant: true` to `"view"`, anything else to `"nonpayable"`.
|
|
29
|
+
#
|
|
30
|
+
# @param definition [Hash] the ABI entry (`"name"`, `"inputs"`, `"outputs"`, `"stateMutability"`),
|
|
31
|
+
# String or Symbol keys
|
|
8
32
|
def initialize(definition)
|
|
9
33
|
definition = definition.transform_keys(&:to_s)
|
|
10
34
|
@name = definition["name"].to_s
|
|
@@ -13,28 +37,88 @@ module BlockGiven
|
|
|
13
37
|
@state_mutability = (definition["stateMutability"] || legacy_mutability(definition)).to_s
|
|
14
38
|
end
|
|
15
39
|
|
|
40
|
+
# The snake_case Ruby method name (`balanceOf` becomes `:balance_of`).
|
|
41
|
+
#
|
|
42
|
+
# @return [Symbol]
|
|
16
43
|
def ruby_name = Utils.snake_case(name).to_sym
|
|
44
|
+
|
|
45
|
+
# The canonical signature, with tuples expanded (`"transfer(address,uint256)"`).
|
|
46
|
+
#
|
|
47
|
+
# @return [String]
|
|
17
48
|
def signature = "#{name}(#{inputs.map(&:type).join(',')})"
|
|
49
|
+
|
|
50
|
+
# The 4-byte selector: first 4 bytes of `keccak256(signature)`, `0x`-prefixed lowercase (memoized).
|
|
51
|
+
#
|
|
52
|
+
# @return [String]
|
|
18
53
|
def selector = @selector ||= Utils.keccak256(signature)[0, 10]
|
|
19
54
|
|
|
55
|
+
# Whether the function is `view` or `pure`, i.e. served by `eth_call`.
|
|
56
|
+
#
|
|
57
|
+
# @return [Boolean]
|
|
20
58
|
def read? = %w[view pure].include?(state_mutability)
|
|
59
|
+
|
|
60
|
+
# Whether the function needs a transaction (`nonpayable` or `payable`).
|
|
61
|
+
#
|
|
62
|
+
# @return [Boolean]
|
|
21
63
|
def write? = !read?
|
|
64
|
+
|
|
65
|
+
# Whether the function accepts ether (`payable`).
|
|
66
|
+
#
|
|
67
|
+
# @return [Boolean]
|
|
22
68
|
def payable? = state_mutability == "payable"
|
|
23
69
|
|
|
70
|
+
# The snake_case names of the inputs, in declaration order (unnamed inputs get `:argN`).
|
|
71
|
+
#
|
|
72
|
+
# @return [Array<Symbol>]
|
|
24
73
|
def input_names = inputs.map(&:ruby_name)
|
|
25
74
|
|
|
26
|
-
#
|
|
75
|
+
# ABI-encodes a call: selector followed by the encoded arguments.
|
|
76
|
+
#
|
|
77
|
+
# Arguments are given positionally or by keyword (matched on snake_cased input names, see
|
|
78
|
+
# {#resolve_args}) and coerced with {Coder.encode}.
|
|
79
|
+
#
|
|
80
|
+
# @param args [Array] positional arguments, in declaration order
|
|
81
|
+
# @param kwargs [Hash] keyword arguments by input name (any case); exclusive with `args`
|
|
82
|
+
# @return [String] `0x`-prefixed calldata
|
|
83
|
+
# @raise [BlockGiven::InvalidArgumentError] when positional and keyword arguments are mixed, when
|
|
84
|
+
# keywords are used with unnamed inputs, on unknown or missing keywords, on a wrong argument count
|
|
85
|
+
# or when a value cannot be coerced to its Solidity type
|
|
86
|
+
# @raise [BlockGiven::InvalidAddressError] when an address argument is malformed
|
|
87
|
+
# @raise [BlockGiven::AbiError] when the underlying ABI encoder rejects a value
|
|
88
|
+
# @example
|
|
89
|
+
# transfer.encode([to, 1_000_000])
|
|
90
|
+
# transfer.encode([], { to: to, value: 1_000_000 })
|
|
27
91
|
def encode(args = [], kwargs = {})
|
|
28
92
|
values = resolve_args(args, kwargs)
|
|
29
93
|
selector + Utils.strip_hex(Coder.encode(inputs, values))
|
|
30
94
|
end
|
|
31
95
|
|
|
32
|
-
#
|
|
96
|
+
# Decodes return data into Ruby values (see {Coder.decode} for the formatting rules).
|
|
97
|
+
#
|
|
98
|
+
# @param hex [String] the `0x`-prefixed return data of `eth_call`
|
|
99
|
+
# @return [Object] the single output value when the function has one output
|
|
100
|
+
# @return [Array<Object>] one value per output otherwise (empty Array for no outputs)
|
|
101
|
+
# @raise [BlockGiven::AbiError] when the data is empty (no contract at the address, typically) or
|
|
102
|
+
# cannot be decoded
|
|
103
|
+
# @example
|
|
104
|
+
# balance_of.decode_output("0x00000000000000000000000000000000000000000000000000000000000f4240")
|
|
105
|
+
# # => 1000000
|
|
33
106
|
def decode_output(hex)
|
|
34
107
|
values = Coder.decode(outputs, hex)
|
|
35
108
|
outputs.size == 1 ? values.first : values
|
|
36
109
|
end
|
|
37
110
|
|
|
111
|
+
# Turns positional or keyword arguments into the positional Array expected by the encoder.
|
|
112
|
+
#
|
|
113
|
+
# Keyword names are matched after `Utils.snake_case` (leading underscores stripped, camelCase
|
|
114
|
+
# converted), so `_to`, `to` and `To` all designate the same input.
|
|
115
|
+
#
|
|
116
|
+
# @api private
|
|
117
|
+
# @param args [Array] positional arguments
|
|
118
|
+
# @param kwargs [Hash] keyword arguments
|
|
119
|
+
# @return [Array] values in input declaration order
|
|
120
|
+
# @raise [BlockGiven::InvalidArgumentError] when both forms are mixed, when the ABI inputs are
|
|
121
|
+
# unnamed, or on unknown / missing keywords
|
|
38
122
|
def resolve_args(args, kwargs)
|
|
39
123
|
raise InvalidArgumentError, "#{name}: mix of positional and keyword arguments" if !args.empty? && !kwargs.empty?
|
|
40
124
|
return args if kwargs.empty?
|
|
@@ -56,11 +140,19 @@ module BlockGiven
|
|
|
56
140
|
input_names.map { |n| normalized[n] }
|
|
57
141
|
end
|
|
58
142
|
|
|
143
|
+
# The canonical signature (same as {#signature}).
|
|
144
|
+
#
|
|
145
|
+
# @return [String]
|
|
59
146
|
def to_s = signature
|
|
147
|
+
|
|
148
|
+
# Compact representation with the signature and state mutability.
|
|
149
|
+
#
|
|
150
|
+
# @return [String]
|
|
60
151
|
def inspect = "#<BlockGiven::Abi::Function #{signature} #{state_mutability}>"
|
|
61
152
|
|
|
62
153
|
private
|
|
63
154
|
|
|
155
|
+
# State mutability for pre-0.5 ABIs that only carry `payable` / `constant` flags.
|
|
64
156
|
def legacy_mutability(definition)
|
|
65
157
|
return "payable" if definition["payable"]
|
|
66
158
|
return "view" if definition["constant"]
|