Module: Eth::Eip712
Overview
Defines handy tools for encoding typed structured data as per EIP-712. Ref: eips.ethereum.org/EIPS/eip-712
Defined Under Namespace
Classes: TypedDataError
Class Method Summary collapse
-
.encode_array(type, value, types) ⇒ Object
Prepares array values by encoding each element according to its base type.
-
.encode_data(primary_type, data, types) ⇒ String
Recursively ABI-encodes all data and types according to EIP-712.
-
.encode_type(primary_type, types) ⇒ String
Encode types as an EIP-712 confrom string, e.g.,
MyType(string attribute)
. -
.encode_value(type, value, types) ⇒ Object
Encodes a single value according to its type following EIP-712 rules.
-
.enforce_typed_data(data) ⇒ Array
Enforces basic properties to be represented in the EIP-712 typed data structure: types, domain, message, etc.
-
.hash(data) ⇒ String
Hashes a typed data structure with Keccak-256 to prepare a signed typed data operation respecting EIP-712.
-
.hash_data(primary_type, data, types) ⇒ String
Recursively ABI-encodes and hashes all data and types.
-
.hash_type(primary_type, types) ⇒ String
Hashes an EIP-712 confrom type-string.
-
.type_dependencies(primary_type, types, result = []) ⇒ Array
Scans all dependencies of a given type recursively and returns either all dependencies or none if not found.
Instance Method Summary collapse
-
#encode_array(type, value, types) ⇒ Object
Prepares array values by encoding each element according to its base type.
-
#encode_data(primary_type, data, types) ⇒ String
Recursively ABI-encodes all data and types according to EIP-712.
-
#encode_type(primary_type, types) ⇒ String
Encode types as an EIP-712 confrom string, e.g.,
MyType(string attribute)
. -
#encode_value(type, value, types) ⇒ Object
Encodes a single value according to its type following EIP-712 rules.
-
#enforce_typed_data(data) ⇒ Array
Enforces basic properties to be represented in the EIP-712 typed data structure: types, domain, message, etc.
-
#hash(data) ⇒ String
Hashes a typed data structure with Keccak-256 to prepare a signed typed data operation respecting EIP-712.
-
#hash_data(primary_type, data, types) ⇒ String
Recursively ABI-encodes and hashes all data and types.
-
#hash_type(primary_type, types) ⇒ String
Hashes an EIP-712 confrom type-string.
-
#type_dependencies(primary_type, types, result = []) ⇒ Array
Scans all dependencies of a given type recursively and returns either all dependencies or none if not found.
Class Method Details
.encode_array(type, value, types) ⇒ Object
Prepares array values by encoding each element according to its base type. Returns an array compatible with Abi.encode.
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/eth/eip712.rb', line 156 def encode_array(type, value, types) inner_type = type.slice(0, type.rindex("[")) return [] if value.nil? value.map do |v| if inner_type.end_with?("]") encode_array inner_type, v, types elsif inner_type == "string" Util.keccak256 v elsif inner_type == "bytes" Util.keccak256 Util.hex_to_bin(v) elsif !types[inner_type.to_sym].nil? Util.keccak256 encode_data(inner_type, v, types) else v end end end |
.encode_data(primary_type, data, types) ⇒ String
Recursively ABI-encodes all data and types according to EIP-712.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/eth/eip712.rb', line 111 def encode_data(primary_type, data, types) # first data field is the type hash encoded_types = ["bytes32"] encoded_values = [hash_type(primary_type, types)] # adds field contents types[primary_type.to_sym].each do |field| value = data[field[:name].to_sym] type = field[:type] if type.end_with?("]") encoded_types.push type encoded_values.push encode_array(type, value, types) elsif type == "string" || type == "bytes" || !types[type.to_sym].nil? encoded_types.push "bytes32" encoded_values.push encode_value(type, value, types) else encoded_types.push type encoded_values.push value end end # all data is abi-encoded return Abi.encode encoded_types, encoded_values end |
.encode_type(primary_type, types) ⇒ String
Encode types as an EIP-712 confrom string, e.g., MyType(string attribute)
.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/eth/eip712.rb', line 68 def encode_type(primary_type, types) # get all used types all_dependencies = type_dependencies primary_type, types # remove primary types and sort the rest alphabetically filtered_dependencies = all_dependencies.delete_if { |type| type.to_s == primary_type } sorted_dependencies = filtered_dependencies.sort dependencies = [primary_type] sorted_dependencies.each do |sorted| dependencies.push sorted end # join them all in a string with types and field names result = "" dependencies.each do |type| # dependencies should not have non-primary types (such as string, address) raise TypedDataError, "Non-primary type found: #{type}!" if types[type.to_sym].nil? result += "#{type}(" result += types[type.to_sym].map { |t| "#{t[:type]} #{t[:name]}" }.join(",") result += ")" end return result end |
.encode_value(type, value, types) ⇒ Object
Encodes a single value according to its type following EIP-712 rules. Returns a 32-byte binary string.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/eth/eip712.rb', line 139 def encode_value(type, value, types) if type == "string" return Util.keccak256 value elsif type == "bytes" value = Util.hex_to_bin value return Util.keccak256 value elsif !types[type.to_sym].nil? nested = encode_data type, value, types return Util.keccak256 nested else # encode basic types via ABI to get 32-byte representation return Abi.encode([type], [value]) end end |
.enforce_typed_data(data) ⇒ Array
Enforces basic properties to be represented in the EIP-712 typed data structure: types, domain, message, etc.
191 192 193 194 195 196 197 198 199 200 |
# File 'lib/eth/eip712.rb', line 191 def enforce_typed_data(data) data = JSON.parse data if Util.hex? data raise TypedDataError, "Data is missing, try again with data." if data.nil? or data.empty? raise TypedDataError, "Data types are missing." if data[:types].nil? or data[:types].empty? raise TypedDataError, "Data primaryType is missing." if data[:primaryType].nil? or data[:primaryType].empty? raise TypedDataError, "Data domain is missing." if data[:domain].nil? raise TypedDataError, "Data message is missing." if data[:message].nil? or data[:message].empty? raise TypedDataError, "Data EIP712Domain is missing." if data[:types][:EIP712Domain].nil? return data end |
.hash(data) ⇒ String
Hashes a typed data structure with Keccak-256 to prepare a signed typed data operation respecting EIP-712.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/eth/eip712.rb', line 207 def hash(data) data = enforce_typed_data data # EIP-191 prefix byte buffer = Signature::EIP191_PREFIX_BYTE # EIP-712 version byte buffer += Signature::EIP712_VERSION_BYTE # hashed domain data buffer += hash_data "EIP712Domain", data[:domain], data[:types] # hashed message data buffer += hash_data data[:primaryType], data[:message], data[:types] return Util.keccak256 buffer end |
.hash_data(primary_type, data, types) ⇒ String
Recursively ABI-encodes and hashes all data and types.
180 181 182 183 |
# File 'lib/eth/eip712.rb', line 180 def hash_data(primary_type, data, types) encoded_data = encode_data primary_type, data, types return Util.keccak256 encoded_data end |
.hash_type(primary_type, types) ⇒ String
Hashes an EIP-712 confrom type-string.
100 101 102 103 |
# File 'lib/eth/eip712.rb', line 100 def hash_type(primary_type, types) encoded_type = encode_type primary_type, types return Util.keccak256 encoded_type end |
.type_dependencies(primary_type, types, result = []) ⇒ Array
Scans all dependencies of a given type recursively and returns either all dependencies or none if not found.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/eth/eip712.rb', line 34 def type_dependencies(primary_type, types, result = []) if result.include? primary_type # ignore if we already have the give type in results return result elsif types[primary_type.to_sym].nil? # ignore if the type is not used, e.g., a string or address. return result else # we found something result.push primary_type # recursively look for further nested dependencies types[primary_type.to_sym].each do |t| nested_type = t[:type] # unpack arrays to their inner types to resolve dependencies if nested_type.end_with?("]") nested_type = nested_type.partition("[").first end dependency = type_dependencies nested_type, types, result end return result end end |
Instance Method Details
#encode_array(type, value, types) ⇒ Object
Prepares array values by encoding each element according to its base type. Returns an array compatible with Abi.encode.
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/eth/eip712.rb', line 156 def encode_array(type, value, types) inner_type = type.slice(0, type.rindex("[")) return [] if value.nil? value.map do |v| if inner_type.end_with?("]") encode_array inner_type, v, types elsif inner_type == "string" Util.keccak256 v elsif inner_type == "bytes" Util.keccak256 Util.hex_to_bin(v) elsif !types[inner_type.to_sym].nil? Util.keccak256 encode_data(inner_type, v, types) else v end end end |
#encode_data(primary_type, data, types) ⇒ String
Recursively ABI-encodes all data and types according to EIP-712.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/eth/eip712.rb', line 111 def encode_data(primary_type, data, types) # first data field is the type hash encoded_types = ["bytes32"] encoded_values = [hash_type(primary_type, types)] # adds field contents types[primary_type.to_sym].each do |field| value = data[field[:name].to_sym] type = field[:type] if type.end_with?("]") encoded_types.push type encoded_values.push encode_array(type, value, types) elsif type == "string" || type == "bytes" || !types[type.to_sym].nil? encoded_types.push "bytes32" encoded_values.push encode_value(type, value, types) else encoded_types.push type encoded_values.push value end end # all data is abi-encoded return Abi.encode encoded_types, encoded_values end |
#encode_type(primary_type, types) ⇒ String
Encode types as an EIP-712 confrom string, e.g., MyType(string attribute)
.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/eth/eip712.rb', line 68 def encode_type(primary_type, types) # get all used types all_dependencies = type_dependencies primary_type, types # remove primary types and sort the rest alphabetically filtered_dependencies = all_dependencies.delete_if { |type| type.to_s == primary_type } sorted_dependencies = filtered_dependencies.sort dependencies = [primary_type] sorted_dependencies.each do |sorted| dependencies.push sorted end # join them all in a string with types and field names result = "" dependencies.each do |type| # dependencies should not have non-primary types (such as string, address) raise TypedDataError, "Non-primary type found: #{type}!" if types[type.to_sym].nil? result += "#{type}(" result += types[type.to_sym].map { |t| "#{t[:type]} #{t[:name]}" }.join(",") result += ")" end return result end |
#encode_value(type, value, types) ⇒ Object
Encodes a single value according to its type following EIP-712 rules. Returns a 32-byte binary string.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/eth/eip712.rb', line 139 def encode_value(type, value, types) if type == "string" return Util.keccak256 value elsif type == "bytes" value = Util.hex_to_bin value return Util.keccak256 value elsif !types[type.to_sym].nil? nested = encode_data type, value, types return Util.keccak256 nested else # encode basic types via ABI to get 32-byte representation return Abi.encode([type], [value]) end end |
#enforce_typed_data(data) ⇒ Array
Enforces basic properties to be represented in the EIP-712 typed data structure: types, domain, message, etc.
191 192 193 194 195 196 197 198 199 200 |
# File 'lib/eth/eip712.rb', line 191 def enforce_typed_data(data) data = JSON.parse data if Util.hex? data raise TypedDataError, "Data is missing, try again with data." if data.nil? or data.empty? raise TypedDataError, "Data types are missing." if data[:types].nil? or data[:types].empty? raise TypedDataError, "Data primaryType is missing." if data[:primaryType].nil? or data[:primaryType].empty? raise TypedDataError, "Data domain is missing." if data[:domain].nil? raise TypedDataError, "Data message is missing." if data[:message].nil? or data[:message].empty? raise TypedDataError, "Data EIP712Domain is missing." if data[:types][:EIP712Domain].nil? return data end |
#hash(data) ⇒ String
Hashes a typed data structure with Keccak-256 to prepare a signed typed data operation respecting EIP-712.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/eth/eip712.rb', line 207 def hash(data) data = enforce_typed_data data # EIP-191 prefix byte buffer = Signature::EIP191_PREFIX_BYTE # EIP-712 version byte buffer += Signature::EIP712_VERSION_BYTE # hashed domain data buffer += hash_data "EIP712Domain", data[:domain], data[:types] # hashed message data buffer += hash_data data[:primaryType], data[:message], data[:types] return Util.keccak256 buffer end |
#hash_data(primary_type, data, types) ⇒ String
Recursively ABI-encodes and hashes all data and types.
180 181 182 183 |
# File 'lib/eth/eip712.rb', line 180 def hash_data(primary_type, data, types) encoded_data = encode_data primary_type, data, types return Util.keccak256 encoded_data end |
#hash_type(primary_type, types) ⇒ String
Hashes an EIP-712 confrom type-string.
100 101 102 103 |
# File 'lib/eth/eip712.rb', line 100 def hash_type(primary_type, types) encoded_type = encode_type primary_type, types return Util.keccak256 encoded_type end |
#type_dependencies(primary_type, types, result = []) ⇒ Array
Scans all dependencies of a given type recursively and returns either all dependencies or none if not found.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/eth/eip712.rb', line 34 def type_dependencies(primary_type, types, result = []) if result.include? primary_type # ignore if we already have the give type in results return result elsif types[primary_type.to_sym].nil? # ignore if the type is not used, e.g., a string or address. return result else # we found something result.push primary_type # recursively look for further nested dependencies types[primary_type.to_sym].each do |t| nested_type = t[:type] # unpack arrays to their inner types to resolve dependencies if nested_type.end_with?("]") nested_type = nested_type.partition("[").first end dependency = type_dependencies nested_type, types, result end return result end end |