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
|
@@ -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
|
data/lib/block_given/utils.rb
CHANGED
|
@@ -4,26 +4,55 @@ require "bigdecimal"
|
|
|
4
4
|
require "bigdecimal/util"
|
|
5
5
|
|
|
6
6
|
module BlockGiven
|
|
7
|
-
# Stateless helpers
|
|
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"
|
|
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-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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_+/, "")
|