Skip to content

Commit 27d5b61

Browse files
feat(EIP712): impl of encodeData, structHash and signHash functions for EIP712TypedData
1 parent 89595ad commit 27d5b61

File tree

4 files changed

+120
-62
lines changed

4 files changed

+120
-62
lines changed

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

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -84,64 +84,6 @@ public func eip712hash(domainSeparator: EIP712Hashable, message: EIP712Hashable)
8484
try eip712hash(domainSeparatorHash: domainSeparator.hash(), messageHash: message.hash())
8585
}
8686

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-
14587
public func eip712hash(domainSeparatorHash: Data, messageHash: Data) -> Data {
14688
(Data([UInt8(0x19), UInt8(0x01)]) + domainSeparatorHash + messageHash).sha3(.keccak256)
14789
}

Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,66 @@ public struct EIP712TypedData {
233233
}
234234
return type + "(" + parameters.joined(separator: ",") + ")" + encodedSubtypes.joined(separator: "")
235235
}
236+
237+
/// Convenience function for ``encodeData(_:data:)`` that uses ``primaryType`` and ``message`` as values.
238+
/// - Returns: encoded data based on ``primaryType`` and ``message``.
239+
public func encodeData() throws -> Data {
240+
try encodeData(primaryType, data: message)
241+
}
242+
243+
public func encodeData(_ type: String, data: [String : AnyObject]) throws -> Data {
244+
// Adding typehash
245+
var encTypes: [ABI.Element.ParameterType] = [.bytes(length: 32)]
246+
var encValues: [Any] = [try typeHash(type)]
247+
248+
guard let typeData = types[type] else {
249+
throw Web3Error.processingError(desc: "EIP712. Attempting to encode data for type that doesn't exist in this payload. Given type: \(type). Available types: \(types.values).")
250+
}
251+
252+
// Add field contents
253+
for field in typeData {
254+
let value = data[field.name]
255+
if field.type == "string" {
256+
guard let value = value as? String else {
257+
throw Web3Error.processingError(desc: "EIP712. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to String.")
258+
}
259+
encTypes.append(.bytes(length: 32))
260+
encValues.append(value.sha3(.keccak256).addHexPrefix())
261+
} else if field.type == "bytes"{
262+
guard let value = value as? Data else {
263+
throw Web3Error.processingError(desc: "EIP712. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to Data.")
264+
}
265+
encTypes.append(.bytes(length: 32))
266+
encValues.append(value.sha3(.keccak256))
267+
} else if types[field.type] != nil {
268+
guard let value = value as? [String : AnyObject] else {
269+
throw Web3Error.processingError(desc: "EIP712. Custom type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to [String : AnyObject].")
270+
}
271+
encTypes.append(.bytes(length: 32))
272+
encValues.append(try encodeData(field.type, data: value).sha3(.keccak256))
273+
} else {
274+
encTypes.append(try ABITypeParser.parseTypeString(field.type))
275+
encValues.append(value as Any)
276+
}
277+
}
278+
279+
guard let encodedData = ABIEncoder.encode(types: encTypes, values: encValues) else {
280+
throw Web3Error.processingError(desc: "EIP712. ABIEncoder.encode failed with the following types and values: \(encTypes); \(encValues)")
281+
}
282+
return encodedData
283+
}
284+
285+
/// Convenience function for ``structHash(_:data:)`` that uses ``primaryType`` and ``message`` as values.
286+
/// - Returns: SH# keccak256 hash of encoded data based on ``primaryType`` and ``message``.
287+
public func structHash() throws -> Data {
288+
try structHash(primaryType, data: message)
289+
}
290+
291+
public func structHash(_ type: String, data: [String : AnyObject]) throws -> Data {
292+
try encodeData(type, data: data).sha3(.keccak256)
293+
}
294+
295+
public func signHash() throws -> Data {
296+
try (Data.fromHex("0x1901")! + structHash("EIP712Domain", data: domain) + structHash()).sha3(.keccak256)
297+
}
236298
}

Sources/web3swift/Web3/Web3+Signing.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@ public struct Web3Signer {
4444
keystore: BIP32Keystore,
4545
account: EthereumAddress,
4646
password: String? = nil) throws -> Data {
47-
let hash = try eip712hash(eip712TypedDataPayload)
47+
let hash = try eip712TypedDataPayload.signHash()
4848
guard let signature = try Web3Signer.signPersonalMessage(hash,
4949
keystore: keystore,
5050
account: account,
51-
password: password ?? "")
51+
password: password ?? "",
52+
useHash: false)
5253
else {
5354
throw Web3Error.dataError
5455
}
@@ -67,7 +68,8 @@ public struct Web3Signer {
6768
guard let signature = try Web3Signer.signPersonalMessage(hash,
6869
keystore: keystore,
6970
account: account,
70-
password: password ?? "")
71+
password: password ?? "",
72+
useHash: false)
7173
else {
7274
throw Web3Error.dataError
7375
}

Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import Foundation
88
import XCTest
99
import web3swift
10-
import Web3Core
10+
@testable import Web3Core
1111

12+
// TODO: take more tests from https://github.com/Mrtenz/eip-712/blob/master/src/eip-712.test.ts
1213

1314
/// Tests based primarily on the following example https://eips.ethereum.org/assets/eip-712/Example.js
1415
class EIP712TypedDataPayloadTests: XCTestCase {
@@ -221,4 +222,55 @@ class EIP712TypedDataPayloadTests: XCTestCase {
221222
try XCTAssertEqual(parsedEip712TypedData.typeHash("Person"), "0xb9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c79500")
222223
try XCTAssertEqual(parsedEip712TypedData.typeHash("Mail"), "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2")
223224
}
225+
226+
func testEIP712EncodeData() throws {
227+
let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload)
228+
let encodedMessage = "a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8"
229+
XCTAssertEqual(try parsedEip712TypedData.encodeData().toHexString(), encodedMessage)
230+
XCTAssertEqual(try parsedEip712TypedData.encodeData(parsedEip712TypedData.primaryType, data: parsedEip712TypedData.message).toHexString(), encodedMessage)
231+
232+
XCTAssertEqual(try parsedEip712TypedData.encodeData("EIP712Domain", data: parsedEip712TypedData.domain).toHexString(),
233+
"8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400fc70ef06638535b4881fafcac8287e210e3769ff1a8e91f1b95d6246e61e4d3c6c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cccccccccccccccccccccccccccccccccccccccc")
234+
235+
XCTAssertEqual(try parsedEip712TypedData.encodeData("Person", data: parsedEip712TypedData.message["from"] as! [String : AnyObject]).toHexString(),
236+
"b9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c795008c1d2bd5348394761719da11ec67eedae9502d137e8940fee8ecd6f641ee1648000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826")
237+
238+
XCTAssertEqual(try parsedEip712TypedData.encodeData("Person",
239+
data: ["wallet" : "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
240+
"name" : "Cow"] as [String : AnyObject]).toHexString(),
241+
"b9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c795008c1d2bd5348394761719da11ec67eedae9502d137e8940fee8ecd6f641ee1648000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826")
242+
}
243+
244+
func testEIP712StructHash() throws {
245+
let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload)
246+
XCTAssertEqual(try parsedEip712TypedData.structHash().toHexString(), "c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e")
247+
XCTAssertEqual(try parsedEip712TypedData.structHash("EIP712Domain", data: parsedEip712TypedData.domain).toHexString(),
248+
"f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f")
249+
}
250+
251+
func testEIP712SignHash() throws {
252+
let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload)
253+
XCTAssertEqual(try parsedEip712TypedData.signHash().toHexString(), "be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2")
254+
}
255+
256+
func testEIP712Signing() throws {
257+
let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload)
258+
let privateKey = Data.fromHex("cow".sha3(.keccak256).addHexPrefix())!
259+
let publicKey = Utilities.privateToPublic(privateKey)!
260+
let address = Utilities.publicToAddress(publicKey)!
261+
XCTAssertEqual(address, EthereumAddress("0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826"));
262+
263+
/// This signing doesn't use `"\u{19}Ethereum Signed Message:\n"`. As per EIP712 standard
264+
/// the following format is used instead:
265+
/// ```
266+
/// encode(domainSeparator : 𝔹²⁵⁶, message : 𝕊) = "\x19\x01" ‖ domainSeparator ‖ hashStruct(message)
267+
/// ```
268+
///
269+
/// The output of ``EIP712TypedData.signHash`` is exactly that.
270+
let (compressedSignature, _) = try SECP256K1.signForRecovery(hash: parsedEip712TypedData.signHash(), privateKey: privateKey)
271+
let unmarshalledSignature = Utilities.unmarshalSignature(signatureData: compressedSignature!)!
272+
XCTAssertEqual(unmarshalledSignature.v, 28)
273+
XCTAssertEqual(unmarshalledSignature.r.toHexString(), "4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d")
274+
XCTAssertEqual(unmarshalledSignature.s.toHexString(), "07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562")
275+
}
224276
}

0 commit comments

Comments
 (0)