Skip to content

Commit 354589f

Browse files
feat(EIP712): parsing of TypedData payload; encoding + hashing;
1 parent 2d4bffd commit 354589f

File tree

7 files changed

+333
-7
lines changed

7 files changed

+333
-7
lines changed

Sources/Web3Core/Utility/String+Extension.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ extension String {
120120
let to16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location + nsRange.length, limitedBy: utf16.endIndex),
121121
let from = from16.samePosition(in: self),
122122
let to = to16.samePosition(in: self)
123-
else { return nil }
123+
else { return nil }
124124
return from ..< to
125125
}
126126

Sources/web3swift/Utils/EIP/EIP712.swift renamed to Sources/web3swift/Utils/EIP/EIP712/EIP712.swift

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public class EIP712 {
1616
public typealias Bytes = Data
1717
}
1818

19+
// FIXME: this type is wrong - The minimum number of optional fields is 5, and those are
20+
// string name the user readable name of signing domain, i.e. the name of the DApp or the protocol.
21+
// string version the current major version of the signing domain. Signatures from different versions are not compatible.
22+
// uint256 chainId the EIP-155 chain id. The user-agent should refuse signing if it does not match the currently active chain.
23+
// address verifyingContract the address of the contract that will verify the signature. The user-agent may do contract specific phishing prevention.
24+
// bytes32 salt an disambiguating salt for the protocol. This can be used as a domain separator of last resort.
1925
public struct EIP712Domain: EIP712Hashable {
2026
public let chainId: EIP712.UInt256?
2127
public let verifyingContract: EIP712.Address
@@ -54,7 +60,10 @@ public extension EIP712Hashable {
5460
result = ABIEncoder.encodeSingleType(type: .uint(bits: 256), value: field)!
5561
case is EIP712.Address:
5662
result = ABIEncoder.encodeSingleType(type: .address, value: field)!
63+
case let boolean as Bool:
64+
result = ABIEncoder.encodeSingleType(type: .uint(bits: 8), value: boolean ? 1 : 0)!
5765
case let hashable as EIP712Hashable:
66+
// TODO: should it be hashed here?
5867
result = try hashable.hash()
5968
default:
6069
/// Cast to `AnyObject` is required. Otherwise, `nil` value will fail this condition.
@@ -64,16 +73,77 @@ public extension EIP712Hashable {
6473
preconditionFailure("Not solidity type")
6574
}
6675
}
67-
guard result.count == 32 else { preconditionFailure("ABI encode error") }
76+
guard result.count % 32 == 0 else { preconditionFailure("ABI encode error") }
6877
parameters.append(result)
6978
}
7079
return Data(parameters.flatMap { $0.bytes }).sha3(.keccak256)
7180
}
7281
}
7382

74-
public func eip712encode(domainSeparator: EIP712Hashable, message: EIP712Hashable) throws -> Data {
75-
let data = try Data([UInt8(0x19), UInt8(0x01)]) + domainSeparator.hash() + message.hash()
76-
return data.sha3(.keccak256)
83+
public func eip712hash(domainSeparator: EIP712Hashable, message: EIP712Hashable) throws -> Data {
84+
try eip712hash(domainSeparatorHash: domainSeparator.hash(), messageHash: message.hash())
85+
}
86+
87+
public func eip712hash(_ eip712TypedData: EIP712TypedData) throws -> Data {
88+
guard let chainId = eip712TypedData.domain["chainId"] as? Int64,
89+
let verifyingContract = eip712TypedData.domain["verifyingContract"] as? String,
90+
let verifyingContractAddress = EIP712.Address(verifyingContract)
91+
else {
92+
throw Web3Error.inputError(desc: "Failed to parse chainId or verifyingContract address. Domain object is \(eip712TypedData.domain).")
93+
}
94+
95+
let domainHash = try EIP712Domain(chainId: EIP712.UInt256(chainId), verifyingContract: verifyingContractAddress).hash()
96+
guard let primaryTypeData = eip712TypedData.types[eip712TypedData.primaryType] else {
97+
throw Web3Error.inputError(desc: "EIP712 hashing error. Given primary type name is not present amongst types. primaryType - \(eip712TypedData.primaryType); available types - \(eip712TypedData.types.values)")
98+
}
99+
100+
let messageHash = try hashEip712Message(eip712TypedData,
101+
eip712TypedData.message,
102+
messageTypeData: primaryTypeData)
103+
return eip712hash(domainSeparatorHash: domainHash, messageHash: messageHash)
104+
}
105+
106+
func hashEip712Message(_ typedData: EIP712TypedData, _ message: [String: AnyObject], messageTypeData: [EIP712TypeProperty]) throws -> Data {
107+
var messageData: [Data] = []
108+
for field in messageTypeData {
109+
guard let fieldValue = message[field.name] else {
110+
throw Web3Error.inputError(desc: "EIP712 message doesn't have field with name \(field.name).")
111+
}
112+
113+
if let customType = typedData.types[field.type] {
114+
guard let objectAttribute = fieldValue as? [String: AnyObject] else {
115+
throw Web3Error.processingError(desc: "Failed to hash EIP712 message. A property from 'message' field with custom type cannot be represented as object and thus encoded & hashed. Property name \(field.name); value \(String(describing: message[field.name])).")
116+
}
117+
try messageData.append(hashEip712Message(typedData, objectAttribute, messageTypeData: customType))
118+
} else {
119+
let type = try ABITypeParser.parseTypeString(field.type)
120+
var data: Data?
121+
switch type {
122+
case .dynamicBytes, .bytes:
123+
if let bytes = fieldValue as? Data {
124+
data = bytes.sha3(.keccak256)
125+
}
126+
case .string:
127+
if let string = fieldValue as? String {
128+
data = Data(string.bytes).sha3(.keccak256)
129+
}
130+
default:
131+
data = ABIEncoder.encodeSingleType(type: type, value: fieldValue)
132+
}
133+
134+
if let data = data {
135+
messageData.append(data)
136+
} else {
137+
throw Web3Error.processingError(desc: "Failed to encode property of EIP712 message. Property name \(field.name); value \(String(describing: message[field.name]))")
138+
}
139+
}
140+
}
141+
142+
return Data(messageData.flatMap { $0.bytes }).sha3(.keccak256)
143+
}
144+
145+
public func eip712hash(domainSeparatorHash: Data, messageHash: Data) -> Data {
146+
(Data([UInt8(0x19), UInt8(0x01)]) + domainSeparatorHash + messageHash).sha3(.keccak256)
77147
}
78148

79149
// MARK: - Additional private and public extensions with support members
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//
2+
// EIP712Parser.swift
3+
//
4+
// Created by JeneaVranceanu on 17.10.2023.
5+
//
6+
7+
import Foundation
8+
import Web3Core
9+
10+
/// The only purpose of this class is to parse raw JSON and output an EIP712 hash.
11+
/// Example of a payload that is received via `eth_signTypedData` for signing:
12+
/// ```
13+
/// {
14+
/// "types":{
15+
/// "EIP712Domain":[
16+
/// {
17+
/// "name":"name",
18+
/// "type":"string"
19+
/// },
20+
/// {
21+
/// "name":"version",
22+
/// "type":"string"
23+
/// },
24+
/// {
25+
/// "name":"chainId",
26+
/// "type":"uint256"
27+
/// },
28+
/// {
29+
/// "name":"verifyingContract",
30+
/// "type":"address"
31+
/// }
32+
/// ],
33+
/// "Person":[
34+
/// {
35+
/// "name":"name",
36+
/// "type":"string"
37+
/// },
38+
/// {
39+
/// "name":"wallet",
40+
/// "type":"address"
41+
/// }
42+
/// ],
43+
/// "Mail":[
44+
/// {
45+
/// "name":"from",
46+
/// "type":"Person"
47+
/// },
48+
/// {
49+
/// "name":"to",
50+
/// "type":"Person"
51+
/// },
52+
/// {
53+
/// "name":"contents",
54+
/// "type":"string"
55+
/// }
56+
/// ]
57+
/// },
58+
/// "primaryType":"Mail",
59+
/// "domain":{
60+
/// "name":"Ether Mail",
61+
/// "version":"1",
62+
/// "chainId":1,
63+
/// "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
64+
/// },
65+
/// "message":{
66+
/// "from":{
67+
/// "name":"Cow",
68+
/// "wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
69+
/// },
70+
/// "to":{
71+
/// "name":"Bob",
72+
/// "wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
73+
/// },
74+
/// "contents":"Hello, Bob!"
75+
/// }
76+
/// }
77+
/// ```
78+
public class EIP712Parser {
79+
static func toData(_ json: String) throws -> Data {
80+
guard let json = json.data(using: .utf8) else {
81+
throw Web3Error.inputError(desc: "Failed to parse EIP712 payload. Given string is not valid UTF8 string. \(json)")
82+
}
83+
return json
84+
}
85+
86+
public static func parse(_ rawJson: String) throws -> EIP712TypedData {
87+
try parse(try toData(rawJson))
88+
}
89+
90+
public static func parse(_ rawJson: Data) throws -> EIP712TypedData {
91+
let decoder = JSONDecoder()
92+
let types = try decoder.decode(EIP712TypeArray.self, from: rawJson).types
93+
guard let json = try rawJson.asJsonDictionary(),
94+
let primaryType = json["primaryType"] as? String,
95+
let domain = json["domain"] as? [String : AnyObject],
96+
let message = json["message"] as? [String : AnyObject]
97+
else {
98+
throw Web3Error.inputError(desc: "EIP712Parser: cannot decode EIP712TypedData object. Failed to parse one of primaryType, domain or message fields. Is any field missing?")
99+
}
100+
101+
return EIP712TypedData(types: types, primaryType: primaryType, domain: domain, message: message)
102+
}
103+
}
104+
105+
internal struct EIP712TypeArray: Codable {
106+
public let types: [String : [EIP712TypeProperty]]
107+
}
108+
109+
public struct EIP712TypeProperty: Codable {
110+
/// Property name. An arbitrary string.
111+
public let name: String
112+
/// Property type. A type that's ABI encodable.
113+
public let type: String
114+
115+
public init(name: String, type: String) {
116+
self.name = name
117+
self.type = type
118+
}
119+
}
120+
121+
public struct EIP712TypedData {
122+
public let types: [String: [EIP712TypeProperty]]
123+
/// A name of one of the types from `types`.
124+
public let primaryType: String
125+
/// A JSON object as a string
126+
public let domain: [String : AnyObject]
127+
/// A JSON object as a string
128+
public let message: [String : AnyObject]
129+
130+
public init(types: [String : [EIP712TypeProperty]],
131+
primaryType: String,
132+
domain: [String : AnyObject],
133+
message: [String : AnyObject]) {
134+
self.types = types
135+
self.primaryType = primaryType
136+
self.domain = domain
137+
self.message = message
138+
}
139+
140+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// Data+Extension.swift
3+
//
4+
// Created by JeneaVranceanu on 18.10.2023.
5+
//
6+
7+
import Foundation
8+
9+
extension Data {
10+
11+
func asJsonDictionary() throws -> [String: AnyObject]? {
12+
try JSONSerialization.jsonObject(with: self, options: .mutableContainers) as? [String:AnyObject]
13+
}
14+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// String+Extension.swift
3+
//
4+
//
5+
// Created by JeneaVranceanu on 17.10.2023.
6+
//
7+
8+
import Foundation
9+
10+
extension String {
11+
12+
func asJsonDictionary() throws -> [String: AnyObject]? {
13+
guard let data = data(using: .utf8) else { return nil }
14+
return try data.asJsonDictionary()
15+
}
16+
}

Sources/web3swift/Web3/Web3+Signing.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ public struct Web3Signer {
4040
return compressedSignature
4141
}
4242

43+
public static func signEIP712(_ eip712TypedDataPayload: EIP712TypedData,
44+
keystore: BIP32Keystore,
45+
account: EthereumAddress,
46+
password: String? = nil) throws -> Data {
47+
let hash = try eip712hash(eip712TypedDataPayload)
48+
guard let signature = try Web3Signer.signPersonalMessage(hash,
49+
keystore: keystore,
50+
account: account,
51+
password: password ?? "")
52+
else {
53+
throw Web3Error.dataError
54+
}
55+
return signature
56+
}
57+
4358
public static func signEIP712(_ eip712Hashable: EIP712Hashable,
4459
keystore: BIP32Keystore,
4560
verifyingContract: EthereumAddress,
@@ -48,7 +63,7 @@ public struct Web3Signer {
4863
chainId: BigUInt? = nil) throws -> Data {
4964

5065
let domainSeparator: EIP712Hashable = EIP712Domain(chainId: chainId, verifyingContract: verifyingContract)
51-
let hash = try eip712encode(domainSeparator: domainSeparator, message: eip712Hashable)
66+
let hash = try eip712hash(domainSeparator: domainSeparator, message: eip712Hashable)
5267
guard let signature = try Web3Signer.signPersonalMessage(hash,
5368
keystore: keystore,
5469
account: account,

0 commit comments

Comments
 (0)