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
@@ -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
- # into idiomatic Ruby (checksummed addresses, hex bytes, named tuples as Hash).
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(Eth::Abi.encode(params.map(&:type), coerced))
18
- rescue Eth::Abi::EncodingError, Eth::Abi::ValueOutOfBounds => e
19
- raise AbiError, "ABI encoding failed: #{e.message}"
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 = Eth::Abi.decode(params.map(&:type), "0x#{data}")
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 Eth::Abi::DecodingError => e
31
- raise AbiError, "ABI decoding failed: #{e.message}"
72
+ rescue AbiDecodingError => e
73
+ raise AbiDecodingError, "ABI decoding failed: #{e.message}"
32
74
  end
33
75
 
34
- # Ruby -> encoder input.
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
- # Decoded value -> Ruby.
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 value.is_a?(String) && !Utils.hex?(value) ? Utils.bin_to_hex(value) : value
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
- # Returns a Hash of decoded arguments keyed by snake_case names.
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 normalized log (Hash with :topics and :data) into a BlockGiven::Event.
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 array for eth_getLogs from indexed argument values.
49
- # Values can be nil (wildcard), a single value or an Array (OR).
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
- # Positional args or keyword args (matched on snake_cased input names).
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
- # Single output -> value; several -> Array.
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"]