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,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockGiven
4
+ module Crypto
5
+ # Keccak-256 as used by Ethereum: the original Keccak submission (padding byte `0x01`), not the
6
+ # standardized NIST SHA3-256 (padding byte `0x06`), which produces different digests.
7
+ #
8
+ # Pure Ruby implementation of the Keccak-f[1600] permutation with a 1088-bit rate.
9
+ #
10
+ # @api private
11
+ # @example
12
+ # BlockGiven::Crypto::Keccak.digest("").unpack1("H*")
13
+ # # => "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
14
+ module Keccak # rubocop:disable Metrics/ModuleLength
15
+ # Sponge rate in bytes for a 256-bit output (1600 - 2 * 256 bits).
16
+ RATE = 136
17
+ # Mask keeping lane arithmetic on 64 bits.
18
+ MASK = 0xFFFFFFFFFFFFFFFF
19
+ # Round constants of the iota step, one per round.
20
+ ROUND_CONSTANTS = [
21
+ 0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000,
22
+ 0x000000000000808B, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
23
+ 0x000000000000008A, 0x0000000000000088, 0x0000000080008009, 0x000000008000000A,
24
+ 0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 0x8000000000008003,
25
+ 0x8000000000008002, 0x8000000000000080, 0x000000000000800A, 0x800000008000000A,
26
+ 0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008
27
+ ].freeze
28
+
29
+ module_function
30
+
31
+ # Keccak-256 digest of a byte string.
32
+ #
33
+ # @param data [String] the bytes to hash (any encoding, read as binary)
34
+ # @return [String] the 32-byte digest as a binary String
35
+ def digest(data)
36
+ message = data.b
37
+ padding = RATE - (message.bytesize % RATE)
38
+ message << (padding == 1 ? "\x81".b : "\x01".b + ("\x00".b * (padding - 2)) + "\x80".b)
39
+
40
+ state = Array.new(25, 0)
41
+ (0...message.bytesize).step(RATE) do |offset|
42
+ message.byteslice(offset, RATE).unpack("Q<17").each_with_index { |lane, i| state[i] ^= lane }
43
+ state = permute(state)
44
+ end
45
+ state.first(4).pack("Q<4")
46
+ end
47
+
48
+ # Applies the 24 rounds of Keccak-f[1600] to a state.
49
+ #
50
+ # Unrolled on purpose: lanes live in local variables (about four times faster than indexing an Array in
51
+ # the inner loops). The rho rotation offsets and the pi lane permutation of the specification are baked
52
+ # into the `b*` assignments (lane x + 5y rotated, then moved to y + 5 * ((2x + 3y) % 5)).
53
+ #
54
+ # @param state [Array<Integer>] 25 lanes of 64 bits, indexed by x + 5 * y
55
+ # @return [Array<Integer>] the permuted state, as a new Array
56
+ def permute(state) # rubocop:disable Metrics/MethodLength
57
+ a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12,
58
+ a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24 = state
59
+ ROUND_CONSTANTS.each do |rc| # rubocop:disable Metrics/BlockLength
60
+ c0 = a0 ^ a5 ^ a10 ^ a15 ^ a20
61
+ c1 = a1 ^ a6 ^ a11 ^ a16 ^ a21
62
+ c2 = a2 ^ a7 ^ a12 ^ a17 ^ a22
63
+ c3 = a3 ^ a8 ^ a13 ^ a18 ^ a23
64
+ c4 = a4 ^ a9 ^ a14 ^ a19 ^ a24
65
+ d0 = c4 ^ (((c1 << 1) | (c1 >> 63)) & MASK)
66
+ d1 = c0 ^ (((c2 << 1) | (c2 >> 63)) & MASK)
67
+ d2 = c1 ^ (((c3 << 1) | (c3 >> 63)) & MASK)
68
+ d3 = c2 ^ (((c4 << 1) | (c4 >> 63)) & MASK)
69
+ d4 = c3 ^ (((c0 << 1) | (c0 >> 63)) & MASK)
70
+ a0 ^= d0
71
+ a1 ^= d1
72
+ a2 ^= d2
73
+ a3 ^= d3
74
+ a4 ^= d4
75
+ a5 ^= d0
76
+ a6 ^= d1
77
+ a7 ^= d2
78
+ a8 ^= d3
79
+ a9 ^= d4
80
+ a10 ^= d0
81
+ a11 ^= d1
82
+ a12 ^= d2
83
+ a13 ^= d3
84
+ a14 ^= d4
85
+ a15 ^= d0
86
+ a16 ^= d1
87
+ a17 ^= d2
88
+ a18 ^= d3
89
+ a19 ^= d4
90
+ a20 ^= d0
91
+ a21 ^= d1
92
+ a22 ^= d2
93
+ a23 ^= d3
94
+ a24 ^= d4
95
+ b0 = a0
96
+ b10 = (((a1 << 1) | (a1 >> 63)) & MASK)
97
+ b20 = (((a2 << 62) | (a2 >> 2)) & MASK)
98
+ b5 = (((a3 << 28) | (a3 >> 36)) & MASK)
99
+ b15 = (((a4 << 27) | (a4 >> 37)) & MASK)
100
+ b16 = (((a5 << 36) | (a5 >> 28)) & MASK)
101
+ b1 = (((a6 << 44) | (a6 >> 20)) & MASK)
102
+ b11 = (((a7 << 6) | (a7 >> 58)) & MASK)
103
+ b21 = (((a8 << 55) | (a8 >> 9)) & MASK)
104
+ b6 = (((a9 << 20) | (a9 >> 44)) & MASK)
105
+ b7 = (((a10 << 3) | (a10 >> 61)) & MASK)
106
+ b17 = (((a11 << 10) | (a11 >> 54)) & MASK)
107
+ b2 = (((a12 << 43) | (a12 >> 21)) & MASK)
108
+ b12 = (((a13 << 25) | (a13 >> 39)) & MASK)
109
+ b22 = (((a14 << 39) | (a14 >> 25)) & MASK)
110
+ b23 = (((a15 << 41) | (a15 >> 23)) & MASK)
111
+ b8 = (((a16 << 45) | (a16 >> 19)) & MASK)
112
+ b18 = (((a17 << 15) | (a17 >> 49)) & MASK)
113
+ b3 = (((a18 << 21) | (a18 >> 43)) & MASK)
114
+ b13 = (((a19 << 8) | (a19 >> 56)) & MASK)
115
+ b14 = (((a20 << 18) | (a20 >> 46)) & MASK)
116
+ b24 = (((a21 << 2) | (a21 >> 62)) & MASK)
117
+ b9 = (((a22 << 61) | (a22 >> 3)) & MASK)
118
+ b19 = (((a23 << 56) | (a23 >> 8)) & MASK)
119
+ b4 = (((a24 << 14) | (a24 >> 50)) & MASK)
120
+ a0 = b0 ^ (~b1 & b2)
121
+ a1 = b1 ^ (~b2 & b3)
122
+ a2 = b2 ^ (~b3 & b4)
123
+ a3 = b3 ^ (~b4 & b0)
124
+ a4 = b4 ^ (~b0 & b1)
125
+ a5 = b5 ^ (~b6 & b7)
126
+ a6 = b6 ^ (~b7 & b8)
127
+ a7 = b7 ^ (~b8 & b9)
128
+ a8 = b8 ^ (~b9 & b5)
129
+ a9 = b9 ^ (~b5 & b6)
130
+ a10 = b10 ^ (~b11 & b12)
131
+ a11 = b11 ^ (~b12 & b13)
132
+ a12 = b12 ^ (~b13 & b14)
133
+ a13 = b13 ^ (~b14 & b10)
134
+ a14 = b14 ^ (~b10 & b11)
135
+ a15 = b15 ^ (~b16 & b17)
136
+ a16 = b16 ^ (~b17 & b18)
137
+ a17 = b17 ^ (~b18 & b19)
138
+ a18 = b18 ^ (~b19 & b15)
139
+ a19 = b19 ^ (~b15 & b16)
140
+ a20 = b20 ^ (~b21 & b22)
141
+ a21 = b21 ^ (~b22 & b23)
142
+ a22 = b22 ^ (~b23 & b24)
143
+ a23 = b23 ^ (~b24 & b20)
144
+ a24 = b24 ^ (~b20 & b21)
145
+ a0 ^= rc
146
+ end
147
+ [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12,
148
+ a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24]
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "securerandom"
5
+
6
+ module BlockGiven
7
+ module Crypto
8
+ # ECDSA over secp256k1, the curve Ethereum accounts use: key generation, public key derivation,
9
+ # deterministic signing (RFC 6979, low-s as required by EIP-2, with the recovery id) and public key recovery.
10
+ #
11
+ # Elliptic curve point multiplications go through the OpenSSL standard library (`OpenSSL::PKey::EC::Point`);
12
+ # scalars stay plain Ruby Integers. Signatures are byte-identical to libsecp256k1's and viem's for the same
13
+ # key and hash, since the nonce is derived deterministically from both.
14
+ #
15
+ # @api private
16
+ module Secp256k1
17
+ # Order of the curve's base point.
18
+ N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
19
+ # Field prime.
20
+ P = (2**256) - (2**32) - 977
21
+ # Largest `s` value of a canonical (low-s) signature.
22
+ HALF_N = N >> 1
23
+
24
+ module_function
25
+
26
+ # The OpenSSL group for the curve, built once.
27
+ #
28
+ # @return [OpenSSL::PKey::EC::Group]
29
+ # @raise [BlockGiven::ConfigurationError] when the linked OpenSSL was built without secp256k1
30
+ def group
31
+ @group ||= OpenSSL::PKey::EC::Group.new("secp256k1")
32
+ rescue OpenSSL::PKey::EC::Group::Error => e
33
+ raise ConfigurationError, "the OpenSSL library Ruby is linked against does not support secp256k1 (#{e.message})"
34
+ end
35
+
36
+ # Whether an Integer is a valid private key, i.e. in [1, N - 1].
37
+ #
38
+ # @param key [Integer]
39
+ # @return [Boolean]
40
+ def valid_private_key?(key) = key.is_a?(Integer) && key.positive? && key < N
41
+
42
+ # Draws a uniformly random private key from `SecureRandom`.
43
+ #
44
+ # @return [Integer] a key in [1, N - 1]
45
+ def generate_private_key
46
+ loop do
47
+ key = bytes_to_int(SecureRandom.random_bytes(32))
48
+ return key if valid_private_key?(key)
49
+ end
50
+ end
51
+
52
+ # The uncompressed public key of a private key.
53
+ #
54
+ # @param private_key [Integer] a key in [1, N - 1]
55
+ # @return [String] 65 binary bytes: `0x04`, then X and Y on 32 bytes each
56
+ def public_key(private_key)
57
+ group.generator.mul(OpenSSL::BN.new(private_key)).to_octet_string(:uncompressed)
58
+ end
59
+
60
+ # Signs a 32-byte hash with a nonce derived per RFC 6979 (HMAC-SHA256), normalized to low-s.
61
+ #
62
+ # @param hash [String] the 32-byte message hash, binary
63
+ # @param private_key [Integer] a key in [1, N - 1]
64
+ # @return [Array(Integer, Integer, Integer)] `[r, s, recovery_id]`, `recovery_id` in 0..3 (bit 0: parity of
65
+ # the nonce point's Y, bit 1: its X overflowed N)
66
+ def sign(hash, private_key)
67
+ z = bytes_to_int(hash) % N
68
+ nonces(int_to_bytes(private_key), int_to_bytes(z)).each do |k|
69
+ point = group.generator.mul(OpenSSL::BN.new(k)).to_octet_string(:uncompressed)
70
+ x = bytes_to_int(point.byteslice(1, 32))
71
+ r = x % N
72
+ next if r.zero?
73
+
74
+ s = (blinded_inverse(k) * (z + (r * private_key))) % N
75
+ next if s.zero?
76
+
77
+ recovery_id = (point.getbyte(64) & 1) | (x >= N ? 2 : 0)
78
+ return s > HALF_N ? [r, N - s, recovery_id ^ 1] : [r, s, recovery_id]
79
+ end
80
+ end
81
+
82
+ # Recovers the uncompressed public key that produced a signature over a hash.
83
+ #
84
+ # @param hash [String] the 32-byte message hash, binary
85
+ # @param r [Integer]
86
+ # @param s [Integer]
87
+ # @param recovery_id [Integer] 0..3
88
+ # @return [String] the 65-byte uncompressed public key, binary
89
+ # @raise [BlockGiven::InvalidArgumentError] when the signature is malformed or matches no curve point
90
+ def recover(hash, r, s, recovery_id)
91
+ unless r.positive? && r < N && s.positive? && s < N && (0..3).cover?(recovery_id)
92
+ raise InvalidArgumentError, "invalid signature"
93
+ end
94
+
95
+ x = r + ((recovery_id >> 1) * N)
96
+ raise InvalidArgumentError, "invalid signature" if x >= P
97
+
98
+ point_r = decompress(x, recovery_id & 1)
99
+ r_inverse = r.pow(N - 2, N)
100
+ u1 = (-bytes_to_int(hash) * r_inverse) % N
101
+ u2 = (s * r_inverse) % N
102
+ public_key = point_r.mul(OpenSSL::BN.new(u2), OpenSSL::BN.new(u1))
103
+ raise InvalidArgumentError, "invalid signature" if public_key.infinity?
104
+
105
+ public_key.to_octet_string(:uncompressed)
106
+ end
107
+
108
+ # Candidate nonces of RFC 6979 section 3.2 with HMAC-SHA256, in order (libsecp256k1's nonce function
109
+ # without extra entropy).
110
+ #
111
+ # @param key [String] the private key on 32 bytes
112
+ # @param message [String] the hash reduced modulo N, on 32 bytes
113
+ # @return [Enumerator<Integer>] endless sequence of nonces in [1, N - 1]
114
+ def nonces(key, message)
115
+ Enumerator.new do |yielder|
116
+ v = "\x01".b * 32
117
+ k = "\x00".b * 32
118
+ k = hmac(k, "#{v}\x00".b + key + message)
119
+ v = hmac(k, v)
120
+ k = hmac(k, "#{v}\x01".b + key + message)
121
+ v = hmac(k, v)
122
+ loop do
123
+ v = hmac(k, v)
124
+ candidate = bytes_to_int(v)
125
+ yielder << candidate if valid_private_key?(candidate)
126
+ k = hmac(k, "#{v}\x00".b)
127
+ v = hmac(k, v)
128
+ end
129
+ end
130
+ end
131
+
132
+ # The modular inverse of a nonce, computed on a randomly blinded value so that its timing does not depend
133
+ # on the nonce itself.
134
+ #
135
+ # @param k [Integer] a nonce in [1, N - 1]
136
+ # @return [Integer] k^-1 mod N
137
+ def blinded_inverse(k)
138
+ blind = (bytes_to_int(SecureRandom.random_bytes(32)) % (N - 1)) + 1
139
+ ((k * blind) % N).pow(N - 2, N) * blind % N
140
+ end
141
+
142
+ # The curve point with a given X coordinate and Y parity.
143
+ #
144
+ # @param x [Integer] X coordinate, below P
145
+ # @param y_parity [Integer] 0 for an even Y, 1 for an odd Y
146
+ # @return [OpenSSL::PKey::EC::Point]
147
+ # @raise [BlockGiven::InvalidArgumentError] when X is not on the curve
148
+ def decompress(x, y_parity)
149
+ OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new((2 + y_parity).chr + int_to_bytes(x), 2))
150
+ rescue OpenSSL::PKey::EC::Point::Error
151
+ raise InvalidArgumentError, "invalid signature"
152
+ end
153
+
154
+ # @param key [String] HMAC key
155
+ # @param data [String]
156
+ # @return [String] the 32-byte HMAC-SHA256, binary
157
+ def hmac(key, data) = OpenSSL::HMAC.digest("SHA256", key, data)
158
+
159
+ # @param bytes [String] big-endian binary
160
+ # @return [Integer]
161
+ def bytes_to_int(bytes) = bytes.unpack1("H*").to_i(16)
162
+
163
+ # @param int [Integer] a non-negative Integer below 2**256
164
+ # @return [String] 32 big-endian bytes, binary
165
+ def int_to_bytes(int) = [int.to_s(16).rjust(64, "0")].pack("H*")
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "crypto/keccak"
4
+ require_relative "crypto/secp256k1"
5
+
6
+ module BlockGiven
7
+ # Cryptographic primitives Ethereum needs, in pure Ruby on top of the OpenSSL standard library: Keccak-256
8
+ # ({Keccak}) and ECDSA over secp256k1 ({Secp256k1}). No native extension is involved beyond OpenSSL.
9
+ #
10
+ # @api private
11
+ module Crypto
12
+ module_function
13
+
14
+ # The address controlled by a public key: the last 20 bytes of the keccak-256 of its X and Y coordinates.
15
+ #
16
+ # @param public_key [String] the 65-byte uncompressed public key, binary (`0x04` prefix included)
17
+ # @return [String] the EIP-55 checksummed address
18
+ def address(public_key)
19
+ Utils.checksum_address(Utils.bin_to_hex(Keccak.digest(public_key.byteslice(1, 64)).byteslice(12, 20)))
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BlockGiven
4
+ # EIP-712 typed structured data hashing (`encodeType`, `encodeData`, `hashStruct`, domain separator).
5
+ #
6
+ # The payload is a Hash with `types`, `primaryType`, `domain` and `message`, with Symbol or String keys.
7
+ # When `types` has no `EIP712Domain` entry it is derived from the domain keys present, in the canonical
8
+ # order (`name`, `version`, `chainId`, `verifyingContract`, `salt`), as viem does.
9
+ #
10
+ # Values: integers as Integer, decimal String or `0x` hex String; addresses and `bytes` / `bytesN` as `0x`
11
+ # hex; `string` as text; arrays as Arrays (hashed per the specification, element by element); structs as
12
+ # Hashes keyed by field name.
13
+ #
14
+ # @api private
15
+ module Eip712
16
+ # Domain fields in the order EIP-712 lists them, with their types, used when `EIP712Domain` is omitted.
17
+ DOMAIN_FIELDS = [
18
+ %w[name string], %w[version string], %w[chainId uint256], %w[verifyingContract address], %w[salt bytes32]
19
+ ].freeze
20
+
21
+ module_function
22
+
23
+ # The digest to sign: keccak256("\x19\x01" || domainSeparator || hashStruct(message)).
24
+ #
25
+ # @param typed_data [Hash] `types`, `primaryType`, `domain` and `message`
26
+ # @return [String] the 32-byte digest, binary
27
+ # @raise [BlockGiven::InvalidArgumentError] when a section is missing, a type is unknown or a value does not
28
+ # fit its type
29
+ def hash(typed_data)
30
+ data = typed_data.to_h.transform_keys(&:to_s)
31
+ %w[types primaryType domain message].each do |key|
32
+ missing = data[key].nil? || (key != "domain" && data[key].empty?)
33
+ raise InvalidArgumentError, "typed data #{key} is missing" if missing
34
+ end
35
+ domain = data["domain"].transform_keys(&:to_s)
36
+ types = normalize_types(data["types"], domain)
37
+ Crypto::Keccak.digest("\x19\x01".b + hash_struct("EIP712Domain", domain, types) +
38
+ hash_struct(data["primaryType"].to_s, data["message"], types))
39
+ end
40
+
41
+ # keccak256(typeHash || encodeData(fields)) of a struct value.
42
+ #
43
+ # @param type [String] the struct name
44
+ # @param value [Hash] field values by name (Symbol or String keys)
45
+ # @param types [Hash{String => Array<Array(String, String)>}] normalized types: struct name to [name, type]
46
+ # @return [String] 32 bytes, binary
47
+ # @raise [BlockGiven::InvalidArgumentError] when the value is not a Hash or misses a field
48
+ def hash_struct(type, value, types)
49
+ raise InvalidArgumentError, "#{type} expects a Hash, got #{value.inspect}" unless value.is_a?(Hash)
50
+
51
+ raise InvalidArgumentError, "unknown EIP-712 type #{type}" unless types.key?(type)
52
+
53
+ fields = value.transform_keys(&:to_s)
54
+ encoded = types[type].map do |name, field_type|
55
+ raise InvalidArgumentError, "#{type}.#{name} is missing" unless fields.key?(name)
56
+
57
+ encode_field(field_type, fields[name], types)
58
+ end
59
+ Crypto::Keccak.digest(Crypto::Keccak.digest(encode_type(type, types)) + encoded.join)
60
+ end
61
+
62
+ # `Name(type1 name1,...)` followed by the referenced structs sorted by name.
63
+ #
64
+ # @param type [String] the struct name
65
+ # @param types [Hash{String => Array<Array(String, String)>}]
66
+ # @return [String]
67
+ def encode_type(type, types)
68
+ ([type] + dependencies(type, types).reject { |t| t == type }.sort).map do |name|
69
+ "#{name}(#{types.fetch(name).map { |field, field_type| "#{field_type} #{field}" }.join(',')})"
70
+ end.join
71
+ end
72
+
73
+ # Every struct reachable from a type, the type itself included.
74
+ #
75
+ # @param type [String]
76
+ # @param types [Hash{String => Array<Array(String, String)>}]
77
+ # @param found [Array<String>] accumulator
78
+ # @return [Array<String>]
79
+ def dependencies(type, types, found = [])
80
+ base = type.sub(/(\[\d*\])+\z/, "")
81
+ return found if found.include?(base) || !types.key?(base)
82
+
83
+ found << base
84
+ types[base].each { |field| dependencies(field[1], types, found) }
85
+ found
86
+ end
87
+
88
+ # The 32-byte encoding of one field value.
89
+ #
90
+ # @param type [String] the field type
91
+ # @param value [Object]
92
+ # @param types [Hash{String => Array<Array(String, String)>}]
93
+ # @return [String] 32 bytes, binary
94
+ # @raise [BlockGiven::InvalidArgumentError] when the value does not fit the type
95
+ def encode_field(type, value, types)
96
+ if (array = type.match(/\A(.+)\[(\d*)\]\z/))
97
+ raise InvalidArgumentError, "#{type} expects an Array, got #{value.inspect}" unless value.is_a?(Array)
98
+ if !array[2].empty? && value.size != array[2].to_i
99
+ raise InvalidArgumentError, "#{type} expects #{array[2]} elements, got #{value.size}"
100
+ end
101
+
102
+ return Crypto::Keccak.digest(value.map { |element| encode_field(array[1], element, types) }.join)
103
+ end
104
+ return hash_struct(type, value, types) if types.key?(type)
105
+ return Crypto::Keccak.digest(value.to_s) if type == "string"
106
+ return Crypto::Keccak.digest(hex_bytes(type, value)) if type == "bytes"
107
+
108
+ encode_atomic(type, value)
109
+ end
110
+
111
+ # The 32-byte word of an atomic value.
112
+ #
113
+ # @param type [String] `uintN`, `intN`, `address`, `bool` or `bytesN`
114
+ # @param value [Object]
115
+ # @return [String] 32 bytes, binary
116
+ # @raise [BlockGiven::InvalidArgumentError] for an unknown type or an out-of-range value
117
+ def encode_atomic(type, value)
118
+ case type
119
+ when /\A(u?)int(\d*)\z/
120
+ bits = ::Regexp.last_match(2).empty? ? 256 : ::Regexp.last_match(2).to_i
121
+ word(integer(type, value), bits, signed: ::Regexp.last_match(1).empty?)
122
+ when "address"
123
+ raise InvalidArgumentError, "invalid address #{value.inspect}" unless Utils.address?(value)
124
+
125
+ Utils.hex_to_bin(Utils.pad_hex(value))
126
+ when "bool"
127
+ raise InvalidArgumentError, "bool expects true/false, got #{value.inspect}" unless [true, false].include?(value)
128
+
129
+ word(value ? 1 : 0, 8, signed: false)
130
+ when /\Abytes(\d+)\z/
131
+ bytes = hex_bytes(type, value)
132
+ raise InvalidArgumentError, "#{type} value is too long" if bytes.bytesize > ::Regexp.last_match(1).to_i
133
+
134
+ bytes.ljust(32, "\x00".b)
135
+ else raise InvalidArgumentError, "unknown EIP-712 type #{type}"
136
+ end
137
+ end
138
+
139
+ # @param type [String] for error messages
140
+ # @param value [Integer, String] Integer, decimal String or `0x` hex String
141
+ # @return [Integer]
142
+ # @raise [BlockGiven::InvalidArgumentError] when the value is not an integer
143
+ def integer(type, value)
144
+ return value if value.is_a?(Integer)
145
+ return Utils.hex_to_int(value) if Utils.hex?(value) && value.size > 2
146
+ return Integer(value, 10) if value.is_a?(String)
147
+
148
+ raise InvalidArgumentError, "#{type} expects an integer, got #{value.inspect}"
149
+ rescue ::ArgumentError
150
+ raise InvalidArgumentError, "#{type} expects an integer, got #{value.inspect}"
151
+ end
152
+
153
+ # A two's complement 32-byte word.
154
+ #
155
+ # @param value [Integer]
156
+ # @param bits [Integer] the type width
157
+ # @param signed [Boolean]
158
+ # @return [String] 32 bytes, binary
159
+ # @raise [BlockGiven::InvalidArgumentError] when the value does not fit
160
+ def word(value, bits, signed:)
161
+ range = signed ? (-(2**(bits - 1))...(2**(bits - 1))) : (0...(2**bits))
162
+ raise InvalidArgumentError, "#{value} does not fit in #{'u' unless signed}int#{bits}" unless range.cover?(value)
163
+
164
+ [(value % (2**256)).to_s(16).rjust(64, "0")].pack("H*")
165
+ end
166
+
167
+ # @param type [String] for error messages
168
+ # @param value [String] `0x` hex
169
+ # @return [String] the bytes, binary
170
+ # @raise [BlockGiven::InvalidArgumentError] when the value is not hex
171
+ def hex_bytes(type, value)
172
+ raise InvalidArgumentError, "#{type} expects 0x hex: #{value.inspect}" unless value.to_s.match?(/\A0x(\h\h)*\z/)
173
+
174
+ Utils.hex_to_bin(value)
175
+ end
176
+
177
+ # Struct definitions as `{ "Name" => [[field, type], ...] }`, `EIP712Domain` derived when absent.
178
+ #
179
+ # @param types [Hash] struct name to an Array of `{ name:, type: }`
180
+ # @param domain [Hash{String => Object}] the domain values
181
+ # @return [Hash{String => Array<Array(String, String)>}]
182
+ def normalize_types(types, domain)
183
+ normalized = types.to_h { |name, fields| [name.to_s, fields.map { |f| field_pair(name, f) }] }
184
+ normalized["EIP712Domain"] ||= DOMAIN_FIELDS.select { |name, _type| domain.key?(name) }
185
+ normalized
186
+ end
187
+
188
+ # @param struct [String, Symbol] for error messages
189
+ # @param field [Hash] `{ name:, type: }`, Symbol or String keys
190
+ # @return [Array(String, String)] name and type
191
+ # @raise [BlockGiven::InvalidArgumentError] when the definition is malformed
192
+ def field_pair(struct, field)
193
+ field = field.to_h.transform_keys(&:to_s)
194
+ raise InvalidArgumentError, "malformed field in #{struct}: #{field.inspect}" unless field["name"] && field["type"]
195
+
196
+ [field["name"].to_s, field["type"].to_s]
197
+ end
198
+ end
199
+ end