Module: Klay::Eip712

Extended by:
Eip712
Included in:
Eip712
Defined in:
lib/klay/eip712.rb

Overview

Defines handy tools for encoding typed structured data as per EIP-712. Ref: https://eips.ethereum.org/EIPS/eip-712

Defined Under Namespace

Classes: TypedDataError

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.encode_data(primary_type, data, types) ⇒ String

Recursively ABI-encodes all data and types according to EIP-712.

Parameters:

  • primary_type (String)

    the primary type which we want to encode.

  • data (Array)

    the data in the data structure we want to encode.

  • types (Array)

    all existing types in the data structure.

Returns:

  • (String)

    an ABI-encoded representation of the data and the types.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/klay/eip712.rb', line 106

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]
    raise NotImplementedError, "Arrays currently unimplemented for EIP-712." if type.end_with? "]"
    if type == "string" or type == "bytes"
      encoded_types.push "bytes32"
      encoded_values.push Util.keccak256 value
    elsif !types[type.to_sym].nil?
      encoded_types.push "bytes32"
      value = encode_data type, value, types
      encoded_values.push Util.keccak256 value
    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).

Parameters:

  • primary_type (String)

    the type which we want to encode.

  • types (Array)

    all existing types in the data structure.

Returns:

  • (String)

    an EIP-712 encoded type-string.

Raises:



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/klay/eip712.rb', line 63

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

.enforce_typed_data(data) ⇒ Array

Enforces basic properties to be represented in the EIP-712 typed data structure: types, domain, message, etc.

Parameters:

  • data (Array)

    the data in the data structure we want to hash.

Returns:

  • (Array)

    the data in the data structure we want to hash.

Raises:



151
152
153
154
155
156
157
158
159
160
# File 'lib/klay/eip712.rb', line 151

def enforce_typed_data(data)
  data = JSON.parse data if Util.is_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.

Parameters:

  • data (Array)

    all the data in the typed data structure.

Returns:

  • (String)

    a Keccak-256 hash of the EIP-712-encoded typed data.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/klay/eip712.rb', line 167

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.

Parameters:

  • primary_type (String)

    the primary type which we want to hash.

  • data (Array)

    the data in the data structure we want to hash.

  • types (Array)

    all existing types in the data structure.

Returns:

  • (String)

    a Keccak-256 hash of the ABI-encoded data and types.



140
141
142
143
# File 'lib/klay/eip712.rb', line 140

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.

Parameters:

  • primary_type (String)

    the type which we want to hash.

  • types (Array)

    all existing types in the data structure.

Returns:

  • (String)

    a Keccak-256 hash of an EIP-712 encoded type-string.



95
96
97
98
# File 'lib/klay/eip712.rb', line 95

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.

Parameters:

  • primary_type (String)

    the primary type which we want to scan.

  • types (Array)

    all existing types in the data structure.

  • result (Array) (defaults to: [])

    found results from previous recursions.

Returns:

  • (Array)

    all dependent types for the given primary type.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/klay/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|
      dependency = type_dependencies t[:type], types, result
    end
    return result
  end
end

Instance Method Details

#encode_data(primary_type, data, types) ⇒ String

Recursively ABI-encodes all data and types according to EIP-712.

Parameters:

  • primary_type (String)

    the primary type which we want to encode.

  • data (Array)

    the data in the data structure we want to encode.

  • types (Array)

    all existing types in the data structure.

Returns:

  • (String)

    an ABI-encoded representation of the data and the types.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/klay/eip712.rb', line 106

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]
    raise NotImplementedError, "Arrays currently unimplemented for EIP-712." if type.end_with? "]"
    if type == "string" or type == "bytes"
      encoded_types.push "bytes32"
      encoded_values.push Util.keccak256 value
    elsif !types[type.to_sym].nil?
      encoded_types.push "bytes32"
      value = encode_data type, value, types
      encoded_values.push Util.keccak256 value
    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).

Parameters:

  • primary_type (String)

    the type which we want to encode.

  • types (Array)

    all existing types in the data structure.

Returns:

  • (String)

    an EIP-712 encoded type-string.

Raises:



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/klay/eip712.rb', line 63

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

#enforce_typed_data(data) ⇒ Array

Enforces basic properties to be represented in the EIP-712 typed data structure: types, domain, message, etc.

Parameters:

  • data (Array)

    the data in the data structure we want to hash.

Returns:

  • (Array)

    the data in the data structure we want to hash.

Raises:



151
152
153
154
155
156
157
158
159
160
# File 'lib/klay/eip712.rb', line 151

def enforce_typed_data(data)
  data = JSON.parse data if Util.is_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.

Parameters:

  • data (Array)

    all the data in the typed data structure.

Returns:

  • (String)

    a Keccak-256 hash of the EIP-712-encoded typed data.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/klay/eip712.rb', line 167

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.

Parameters:

  • primary_type (String)

    the primary type which we want to hash.

  • data (Array)

    the data in the data structure we want to hash.

  • types (Array)

    all existing types in the data structure.

Returns:

  • (String)

    a Keccak-256 hash of the ABI-encoded data and types.



140
141
142
143
# File 'lib/klay/eip712.rb', line 140

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.

Parameters:

  • primary_type (String)

    the type which we want to hash.

  • types (Array)

    all existing types in the data structure.

Returns:

  • (String)

    a Keccak-256 hash of an EIP-712 encoded type-string.



95
96
97
98
# File 'lib/klay/eip712.rb', line 95

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.

Parameters:

  • primary_type (String)

    the primary type which we want to scan.

  • types (Array)

    all existing types in the data structure.

  • result (Array) (defaults to: [])

    found results from previous recursions.

Returns:

  • (Array)

    all dependent types for the given primary type.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/klay/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|
      dependency = type_dependencies t[:type], types, result
    end
    return result
  end
end