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,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockGiven
4
+ # Serialization, signing and decoding of the two transaction types the gem sends: EIP-1559 (type 2) and
5
+ # legacy (type 0) with EIP-155 replay protection.
6
+ #
7
+ # Parameters use the shape of {Wallet#prepare_transaction} (`:to`, `:value`, `:data`, `:chain_id`, `:nonce`,
8
+ # `:gas`, `:access_list`, plus `:gas_price` for legacy or the two EIP-1559 fee fields).
9
+ #
10
+ # Access lists are given and returned as `[{ address: "0x...", storage_keys: ["0x..."] }]`; the RLP form
11
+ # `[["0x...", ["0x..."]]]` and camelCase or String keys (`storageKeys`) are accepted as input too.
12
+ #
13
+ # @api private
14
+ module TransactionEnvelope
15
+ # EIP-2718 type byte of EIP-1559 transactions.
16
+ TYPE_EIP1559 = 2
17
+
18
+ module_function
19
+
20
+ # Signs a transaction.
21
+ #
22
+ # @param params [Hash{Symbol => Object}] resolved parameters ({Wallet#prepare_transaction}); `:gas_price`
23
+ # selects a legacy transaction
24
+ # @param private_key [Integer] the signing key
25
+ # @return [String] the signed transaction bytes as `0x` hex
26
+ # @raise [BlockGiven::InvalidArgumentError] when a field is negative or the gas limit is below the
27
+ # intrinsic gas of the transaction
28
+ def sign(params, private_key)
29
+ Fields.validate!(params)
30
+ if params[:gas_price]
31
+ sign_legacy(params, private_key)
32
+ else
33
+ sign_eip1559(params, private_key)
34
+ end
35
+ end
36
+
37
+ # Decodes signed transaction bytes and recovers the sender.
38
+ #
39
+ # @param raw [String] the signed transaction as hex, with or without `0x`
40
+ # @return [Hash{Symbol => Object}] the {Wallet#prepare_transaction} shape (`:from` checksummed, `:to` nil
41
+ # for a contract creation, `:data` `""` when empty, `:access_list` nil when empty)
42
+ # @raise [BlockGiven::InvalidArgumentError] for malformed bytes, an unsupported transaction type or a
43
+ # signature that recovers no key
44
+ def decode(raw)
45
+ hex = Utils.strip_hex(raw.to_s)
46
+ raise InvalidArgumentError, "not an even-length hex string" unless hex.match?(/\A(?:[0-9a-fA-F]{2})*\z/)
47
+
48
+ bytes = Utils.hex_to_bin(hex)
49
+ first = bytes.getbyte(0)
50
+ raise InvalidArgumentError, "empty transaction" if first.nil?
51
+ return decode_eip1559(bytes.byteslice(1..)) if first == TYPE_EIP1559
52
+ return decode_legacy(bytes) if first >= 0xc0
53
+
54
+ raise InvalidArgumentError, "unsupported transaction type #{first}"
55
+ end
56
+
57
+ # Signs an EIP-1559 transaction.
58
+ #
59
+ # @param params [Hash{Symbol => Object}]
60
+ # @param private_key [Integer]
61
+ # @return [String] `0x02...` hex
62
+ def sign_eip1559(params, private_key)
63
+ fields = [
64
+ params[:chain_id], params[:nonce], params[:max_priority_fee_per_gas], params[:max_fee_per_gas],
65
+ params[:gas], address_bytes(params[:to]), params[:value], Utils.hex_to_bin(params[:data].to_s),
66
+ Fields.rlp_access_list(params[:access_list])
67
+ ]
68
+ r, s, recovery_id = Crypto::Secp256k1.sign(Crypto::Keccak.digest(typed(fields)), private_key)
69
+ Utils.bin_to_hex(typed(fields + [recovery_id, r, s]))
70
+ end
71
+
72
+ # Signs a legacy transaction with EIP-155 replay protection (`v = recovery_id + 35 + 2 * chain_id`).
73
+ #
74
+ # @param params [Hash{Symbol => Object}]
75
+ # @param private_key [Integer]
76
+ # @return [String] RLP list hex
77
+ def sign_legacy(params, private_key)
78
+ fields = [
79
+ params[:nonce], params[:gas_price], params[:gas], address_bytes(params[:to]), params[:value],
80
+ Utils.hex_to_bin(params[:data].to_s)
81
+ ]
82
+ chain_id = params[:chain_id]
83
+ r, s, recovery_id = Crypto::Secp256k1.sign(Crypto::Keccak.digest(Rlp.encode(fields + [chain_id, 0, 0])),
84
+ private_key)
85
+ Utils.bin_to_hex(Rlp.encode(fields + [recovery_id + 35 + (2 * chain_id), r, s]))
86
+ end
87
+
88
+ # Decodes the RLP payload of an EIP-1559 transaction (type byte removed).
89
+ #
90
+ # @param payload [String] binary
91
+ # @return [Hash{Symbol => Object}]
92
+ # @raise [BlockGiven::InvalidArgumentError] when the payload is not a 12-field list
93
+ def decode_eip1559(payload)
94
+ fields = Rlp.decode(payload)
95
+ unless fields.is_a?(Array) && fields.size == 12
96
+ raise InvalidArgumentError,
97
+ "EIP-1559 transaction must have 12 fields"
98
+ end
99
+
100
+ chain_id, nonce, priority, max_fee, gas, = fields.first(5).map { |f| Rlp.to_int(f) }
101
+ y_parity, r, s = fields.last(3).map { |f| Rlp.to_int(f) }
102
+ raise InvalidArgumentError, "invalid y parity #{y_parity}" if y_parity > 1
103
+
104
+ sender = recover_address(typed(fields.first(9)), r, s, y_parity)
105
+ access_list = Fields.parse_access_list(fields[8])
106
+ common_params(sender, fields[5], fields[6], fields[7]).merge(
107
+ chain_id: chain_id, nonce: nonce, gas: gas, max_fee_per_gas: max_fee, max_priority_fee_per_gas: priority,
108
+ access_list: access_list.empty? ? nil : access_list
109
+ )
110
+ end
111
+
112
+ # Decodes a legacy transaction, with (EIP-155) or without replay protection.
113
+ #
114
+ # @param bytes [String] binary RLP list
115
+ # @return [Hash{Symbol => Object}] `:chain_id` is nil for a transaction signed without replay protection
116
+ # @raise [BlockGiven::InvalidArgumentError] when the list does not have 9 fields or `v` is invalid
117
+ def decode_legacy(bytes)
118
+ fields = Rlp.decode(bytes)
119
+ raise InvalidArgumentError, "legacy transaction must have 9 fields" unless fields.is_a?(Array) && fields.size == 9
120
+
121
+ nonce, gas_price, gas = fields.first(3).map { |f| Rlp.to_int(f) }
122
+ v, r, s = fields.last(3).map { |f| Rlp.to_int(f) }
123
+ chain_id, recovery_id, unsigned = legacy_signing_payload(fields.first(6), v)
124
+ sender = recover_address(Rlp.encode(unsigned), r, s, recovery_id)
125
+ common_params(sender, fields[3], fields[4], fields[5]).merge(
126
+ chain_id: chain_id, nonce: nonce, gas: gas, gas_price: gas_price, access_list: nil
127
+ )
128
+ end
129
+
130
+ # The chain id, recovery id and unsigned field list of a legacy transaction, from its `v`.
131
+ #
132
+ # @param fields [Array<String>] the six unsigned fields
133
+ # @param v [Integer]
134
+ # @return [Array(Integer, Integer, Array)] chain id (nil for `v` 27 or 28), recovery id, fields to hash
135
+ # @raise [BlockGiven::InvalidArgumentError] when `v` is neither 27, 28 nor an EIP-155 value
136
+ def legacy_signing_payload(fields, v)
137
+ return [nil, v - 27, fields] if [27, 28].include?(v)
138
+ raise InvalidArgumentError, "invalid legacy signature v #{v}" if v < 37
139
+
140
+ chain_id = (v - 35) / 2
141
+ [chain_id, (v - 35) % 2, fields + [chain_id, 0, 0]]
142
+ end
143
+
144
+ # Fields both transaction types share, formatted.
145
+ #
146
+ # @param sender [String] checksummed sender
147
+ # @param to [String] binary destination (empty for a creation)
148
+ # @param value [String] binary amount
149
+ # @param data [String] binary calldata
150
+ # @return [Hash{Symbol => Object}]
151
+ # @raise [BlockGiven::InvalidArgumentError] when `to` is neither empty nor 20 bytes
152
+ def common_params(sender, to, value, data)
153
+ raise InvalidArgumentError, "invalid destination" unless to.is_a?(String) && [0, 20].include?(to.bytesize)
154
+ raise InvalidArgumentError, "invalid data field" unless data.is_a?(String)
155
+
156
+ {
157
+ from: sender, to: to.empty? ? nil : Utils.checksum_address(Utils.bin_to_hex(to)),
158
+ value: Rlp.to_int(value), data: data.empty? ? "" : Utils.bin_to_hex(data)
159
+ }
160
+ end
161
+
162
+ # Recovers the checksummed address that signed a payload.
163
+ #
164
+ # @param payload [String] the unsigned serialization, binary
165
+ # @param r [Integer]
166
+ # @param s [Integer]
167
+ # @param recovery_id [Integer]
168
+ # @return [String] the checksummed address
169
+ # @raise [BlockGiven::InvalidArgumentError] when no key can be recovered
170
+ def recover_address(payload, r, s, recovery_id)
171
+ public_key = Crypto::Secp256k1.recover(Crypto::Keccak.digest(payload), r, s, recovery_id)
172
+ Crypto.address(public_key)
173
+ end
174
+
175
+ # @param fields [Array] RLP items
176
+ # @return [String] the type-2 envelope: `0x02` followed by the RLP list, binary
177
+ def typed(fields) = TYPE_EIP1559.chr.b + Rlp.encode(fields)
178
+
179
+ # @param address [String, nil] hex address or nil for a contract creation
180
+ # @return [String] 20 binary bytes, or an empty String
181
+ def address_bytes(address) = address.nil? ? "".b : Utils.hex_to_bin(address)
182
+ end
183
+ end
@@ -4,26 +4,55 @@ require "bigdecimal"
4
4
  require "bigdecimal/util"
5
5
 
6
6
  module BlockGiven
7
- # Stateless helpers, mirroring viem's `utils` (parseUnits, formatUnits, keccak256, ...).
7
+ # Stateless helpers mirroring viem's `utils`: hex and byte conversion, keccak256, addresses, unit parsing and
8
+ # formatting, block tags and name casing.
9
+ #
10
+ # Every method is a module function (`BlockGiven::Utils.parse_units(...)`). Hex values are `0x`-prefixed
11
+ # Strings, amounts are Integers in the smallest unit (wei for ETH), and addresses come back EIP-55
12
+ # checksummed.
13
+ #
14
+ # @example
15
+ # BlockGiven::Utils.parse_units("1.5", 6) # => 1_500_000
16
+ # BlockGiven::Utils.format_ether(10**18) # => "1"
17
+ # BlockGiven::Utils.checksum_address("0xd8da6bf26964af9d7eed9e03e53415d37aa96045")
18
+ # # => "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
8
19
  module Utils
9
20
  module_function
10
21
 
22
+ # The zero address (`address(0)`): 20 zero bytes, used as the `from` of mints and the `to` of burns.
11
23
  ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
24
+ # Block tags JSON-RPC accepts in place of a block number (see {.block_tag}).
12
25
  BLOCK_TAGS = %w[latest earliest pending safe finalized].freeze
13
26
 
27
+ # Whether a value is a `0x`-prefixed hexadecimal String (the empty `"0x"` counts).
28
+ #
29
+ # @param value [Object]
30
+ # @return [Boolean]
14
31
  def hex?(value)
15
32
  value.is_a?(String) && value.match?(/\A0x[0-9a-fA-F]*\z/)
16
33
  end
17
34
 
35
+ # Add the `0x` prefix to a hex string when it is missing.
36
+ #
37
+ # @param hex [String]
38
+ # @return [String]
18
39
  def prefix_hex(hex)
19
40
  hex.start_with?("0x") ? hex : "0x#{hex}"
20
41
  end
21
42
 
43
+ # Remove the `0x` prefix from a hex string when it is present.
44
+ #
45
+ # @param hex [String]
46
+ # @return [String]
22
47
  def strip_hex(hex)
23
48
  hex.start_with?("0x") ? hex[2..] : hex
24
49
  end
25
50
 
26
- # Integer -> "0x1a" (no leading zeros, as JSON-RPC QUANTITY expects).
51
+ # Convert an Integer to a JSON-RPC QUANTITY (`26` -> `"0x1a"`, no leading zeros); Strings only get prefixed.
52
+ #
53
+ # @param value [Integer, String] Integer to encode, or hex String to `0x`-prefix
54
+ # @return [String]
55
+ # @raise [BlockGiven::InvalidArgumentError] for any other type
27
56
  def to_hex(value)
28
57
  case value
29
58
  when Integer then "0x#{value.to_s(16)}"
@@ -32,6 +61,10 @@ module BlockGiven
32
61
  end
33
62
  end
34
63
 
64
+ # Convert a hex QUANTITY to an Integer.
65
+ #
66
+ # @param hex [String, Integer, nil] hex String with or without `0x`; Integers pass through
67
+ # @return [Integer, nil] nil when `hex` is nil
35
68
  def hex_to_int(hex)
36
69
  return nil if hex.nil?
37
70
  return hex if hex.is_a?(Integer)
@@ -39,41 +72,87 @@ module BlockGiven
39
72
  Integer(strip_hex(hex), 16)
40
73
  end
41
74
 
75
+ # Decode a hex string into binary bytes.
76
+ #
77
+ # @param hex [String] hex with or without `0x`
78
+ # @return [String] binary (ASCII-8BIT) String
42
79
  def hex_to_bin(hex)
43
80
  [strip_hex(hex)].pack("H*")
44
81
  end
45
82
 
83
+ # Encode binary bytes as a `0x` hex string.
84
+ #
85
+ # @param bin [String] binary String
86
+ # @return [String] lowercase `0x` hex
46
87
  def bin_to_hex(bin)
47
88
  "0x#{bin.unpack1('H*')}"
48
89
  end
49
90
 
50
- # Left-pads a hex value to 32 bytes (used for event topics).
91
+ # Left-pad a hex value with zeros to a fixed byte length, 32 bytes by default as event topics require.
92
+ #
93
+ # @param hex [String] hex with or without `0x`
94
+ # @param bytes [Integer] target length in bytes
95
+ # @return [String] `0x` followed by `bytes * 2` hex chars (longer inputs are returned with only the prefix
96
+ # normalised)
51
97
  def pad_hex(hex, bytes: 32)
52
98
  "0x#{strip_hex(hex).rjust(bytes * 2, '0')}"
53
99
  end
54
100
 
55
- # keccak256 of raw bytes (or of the bytes represented by a 0x hex string).
101
+ # keccak256 of raw bytes, or of the bytes a `0x` hex string represents.
102
+ #
103
+ # @param data [String] `0x` hex is decoded first; any other String is hashed as-is (e.g. an event signature)
104
+ # @return [String] 32-byte digest as a `0x` hex string
105
+ # @example
106
+ # BlockGiven::Utils.keccak256("Transfer(address,address,uint256)")
107
+ # # => "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
56
108
  def keccak256(data)
57
109
  bytes = hex?(data) ? hex_to_bin(data) : data.to_s
58
- bin_to_hex(Eth::Util.keccak256(bytes))
110
+ bin_to_hex(Crypto::Keccak.digest(bytes))
59
111
  end
60
112
 
113
+ # Whether a value is a 20-byte hex address (`0x` + 40 hex chars); the EIP-55 checksum is not verified.
114
+ #
115
+ # @param value [Object]
116
+ # @return [Boolean]
61
117
  def address?(value)
62
118
  value.is_a?(String) && value.match?(/\A0x[0-9a-fA-F]{40}\z/)
63
119
  end
64
120
 
121
+ # Return the EIP-55 checksummed form of an address.
122
+ #
123
+ # @param value [String, #address] hex address in any casing, or an object responding to `address` such as a
124
+ # {Wallet} or a {Contract}
125
+ # @return [String] checksummed `0x` address
126
+ # @raise [BlockGiven::InvalidAddressError] when the value is not a 20-byte hex address
65
127
  def checksum_address(value)
66
128
  value = value.address if value.respond_to?(:address) && !value.is_a?(String)
67
129
  raise InvalidAddressError, "invalid address: #{value.inspect}" unless address?(value)
68
130
 
69
- Eth::Address.new(value).checksummed
131
+ hex = strip_hex(value).downcase
132
+ nibbles = Crypto::Keccak.digest(hex).unpack1("H*")
133
+ "0x#{hex.chars.each_with_index.map { |char, i| nibbles[i].to_i(16) >= 8 ? char.upcase : char }.join}"
70
134
  end
71
135
 
136
+ # Compare two addresses ignoring checksum casing.
137
+ #
138
+ # @param a [String, #to_s]
139
+ # @param b [String, #to_s]
140
+ # @return [Boolean]
72
141
  def same_address?(a, b)
73
142
  a.to_s.downcase == b.to_s.downcase
74
143
  end
75
144
 
76
- # "1.5", 6 -> 1_500_000. Accepts String, Integer, Float, Rational, BigDecimal.
145
+ # Convert a human readable amount to an Integer in the smallest unit (viem's `parseUnits`).
146
+ #
147
+ # @param value [String, Integer, Float, Rational, BigDecimal] decimal amount; Strings avoid float rounding
148
+ # @param decimals [Integer] decimals of the token (18 for ETH, 6 for USDC)
149
+ # @return [Integer] amount in the smallest unit
150
+ # @raise [BlockGiven::InvalidArgumentError] when the value is not a decimal or has more than `decimals`
151
+ # fractional digits
152
+ # @example
153
+ # BlockGiven::Utils.parse_units("1.5", 6) # => 1_500_000
154
+ # BlockGiven::Utils.parse_units("0.000001", 18) # => 1_000_000_000_000
155
+ # BlockGiven::Utils.parse_units("1.0000001", 6) # raises BlockGiven::InvalidArgumentError
77
156
  def parse_units(value, decimals)
78
157
  decimal = to_decimal(value)
79
158
  scaled = decimal * (BigDecimal(10)**decimals)
@@ -82,7 +161,15 @@ module BlockGiven
82
161
  scaled.to_i
83
162
  end
84
163
 
85
- # 1_500_000, 6 -> "1.5"
164
+ # Format an Integer amount in the smallest unit as a decimal String (viem's `formatUnits`).
165
+ #
166
+ # Trailing zeros and a trailing dot are removed, so `1_000_000` with 6 decimals gives `"1"`, not `"1.0"`.
167
+ #
168
+ # @param value [Integer, #to_i] amount in the smallest unit
169
+ # @param decimals [Integer] decimals of the token
170
+ # @return [String] plain decimal notation, never scientific
171
+ # @example
172
+ # BlockGiven::Utils.format_units(1_500_000, 6) # => "1.5"
86
173
  def format_units(value, decimals)
87
174
  decimal = BigDecimal(value.to_i) / (BigDecimal(10)**decimals)
88
175
  str = decimal.to_s("F")
@@ -90,11 +177,40 @@ module BlockGiven
90
177
  str
91
178
  end
92
179
 
180
+ # {.parse_units} with 18 decimals: ether to wei.
181
+ #
182
+ # @param value [String, Integer, Float, Rational, BigDecimal] amount in ether
183
+ # @return [Integer] wei
184
+ # @raise [BlockGiven::InvalidArgumentError] see {.parse_units}
93
185
  def parse_ether(value) = parse_units(value, 18)
186
+
187
+ # {.format_units} with 18 decimals: wei to ether.
188
+ #
189
+ # @param value [Integer, #to_i] wei
190
+ # @return [String] ether
94
191
  def format_ether(value) = format_units(value, 18)
192
+
193
+ # {.parse_units} with 9 decimals: gwei to wei (handy for gas prices).
194
+ #
195
+ # @param value [String, Integer, Float, Rational, BigDecimal] amount in gwei
196
+ # @return [Integer] wei
197
+ # @raise [BlockGiven::InvalidArgumentError] see {.parse_units}
95
198
  def parse_gwei(value) = parse_units(value, 9)
199
+
200
+ # {.format_units} with 9 decimals: wei to gwei.
201
+ #
202
+ # @param value [Integer, #to_i] wei
203
+ # @return [String] gwei
96
204
  def format_gwei(value) = format_units(value, 9)
97
205
 
206
+ # Convert a numeric value to a BigDecimal without floating point surprises.
207
+ #
208
+ # Floats go through their String form, Rationals get 40 significant digits, Strings are stripped first.
209
+ #
210
+ # @api private
211
+ # @param value [BigDecimal, Integer, Float, Rational, String]
212
+ # @return [BigDecimal]
213
+ # @raise [BlockGiven::InvalidArgumentError] for other types or Strings BigDecimal cannot parse
98
214
  def to_decimal(value)
99
215
  case value
100
216
  when BigDecimal then value
@@ -108,7 +224,16 @@ module BlockGiven
108
224
  raise InvalidArgumentError, "cannot convert #{value.inspect} to a decimal"
109
225
  end
110
226
 
111
- # Accepts an Integer, a hex QUANTITY or a block tag (:latest, "pending", ...).
227
+ # Build the block parameter of a JSON-RPC call from an Integer, a hex QUANTITY or a block tag.
228
+ #
229
+ # @param value [Integer, String, Symbol, nil] block number, `0x` hex QUANTITY, or one of {BLOCK_TAGS} as
230
+ # String or Symbol; nil means `"latest"`
231
+ # @return [String] hex QUANTITY or tag, ready to be sent as RPC parameter
232
+ # @raise [BlockGiven::InvalidArgumentError] for an unknown tag or an unsupported type
233
+ # @example
234
+ # BlockGiven::Utils.block_tag(nil) # => "latest"
235
+ # BlockGiven::Utils.block_tag(18_000_000) # => "0x112a880"
236
+ # BlockGiven::Utils.block_tag(:finalized) # => "finalized"
112
237
  def block_tag(value)
113
238
  case value
114
239
  when nil then "latest"
@@ -122,6 +247,15 @@ module BlockGiven
122
247
  end
123
248
  end
124
249
 
250
+ # Convert a camelCase or PascalCase name to snake_case, as used for ABI names and RPC keys.
251
+ #
252
+ # Leading underscores are dropped (`"_owner"` -> `"owner"`), acronyms are split (`"ERC20Token"` ->
253
+ # `"erc20_token"`) and dashes become underscores.
254
+ #
255
+ # @param name [String, Symbol]
256
+ # @return [String]
257
+ # @example
258
+ # BlockGiven::Utils.snake_case("maxFeePerGas") # => "max_fee_per_gas"
125
259
  def snake_case(name)
126
260
  name.to_s
127
261
  .sub(/\A_+/, "")
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BlockGiven
4
- VERSION = "0.1.0"
4
+ # Version of the gem, following semantic versioning.
5
+ VERSION = "0.2.0"
5
6
  end