From 354589f426e3be9e6a00d010cebf14b03ab2b4a9 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Wed, 18 Oct 2023 10:51:33 +0300 Subject: [PATCH 01/23] feat(EIP712): parsing of TypedData payload; encoding + hashing; --- .../Web3Core/Utility/String+Extension.swift | 2 +- .../Utils/EIP/{ => EIP712}/EIP712.swift | 78 +++++++++- .../Utils/EIP/EIP712/EIP712Parser.swift | 140 ++++++++++++++++++ .../Utils/Extensions/Data+Extension.swift | 14 ++ .../Utils/Extensions/String+Extension.swift | 16 ++ Sources/web3swift/Web3/Web3+Signing.swift | 17 ++- .../localTests/EIP712Tests.swift | 73 ++++++++- 7 files changed, 333 insertions(+), 7 deletions(-) rename Sources/web3swift/Utils/EIP/{ => EIP712}/EIP712.swift (59%) create mode 100644 Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift create mode 100644 Sources/web3swift/Utils/Extensions/Data+Extension.swift create mode 100644 Sources/web3swift/Utils/Extensions/String+Extension.swift diff --git a/Sources/Web3Core/Utility/String+Extension.swift b/Sources/Web3Core/Utility/String+Extension.swift index dbe0a10ca..695d4ec63 100755 --- a/Sources/Web3Core/Utility/String+Extension.swift +++ b/Sources/Web3Core/Utility/String+Extension.swift @@ -120,7 +120,7 @@ extension String { let to16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location + nsRange.length, limitedBy: utf16.endIndex), let from = from16.samePosition(in: self), let to = to16.samePosition(in: self) - else { return nil } + else { return nil } return from ..< to } diff --git a/Sources/web3swift/Utils/EIP/EIP712.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712.swift similarity index 59% rename from Sources/web3swift/Utils/EIP/EIP712.swift rename to Sources/web3swift/Utils/EIP/EIP712/EIP712.swift index e3a4bcb87..1d0b85147 100644 --- a/Sources/web3swift/Utils/EIP/EIP712.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712.swift @@ -16,6 +16,12 @@ public class EIP712 { public typealias Bytes = Data } +// FIXME: this type is wrong - The minimum number of optional fields is 5, and those are +// string name the user readable name of signing domain, i.e. the name of the DApp or the protocol. +// string version the current major version of the signing domain. Signatures from different versions are not compatible. +// uint256 chainId the EIP-155 chain id. The user-agent should refuse signing if it does not match the currently active chain. +// address verifyingContract the address of the contract that will verify the signature. The user-agent may do contract specific phishing prevention. +// bytes32 salt an disambiguating salt for the protocol. This can be used as a domain separator of last resort. public struct EIP712Domain: EIP712Hashable { public let chainId: EIP712.UInt256? public let verifyingContract: EIP712.Address @@ -54,7 +60,10 @@ public extension EIP712Hashable { result = ABIEncoder.encodeSingleType(type: .uint(bits: 256), value: field)! case is EIP712.Address: result = ABIEncoder.encodeSingleType(type: .address, value: field)! + case let boolean as Bool: + result = ABIEncoder.encodeSingleType(type: .uint(bits: 8), value: boolean ? 1 : 0)! case let hashable as EIP712Hashable: + // TODO: should it be hashed here? result = try hashable.hash() default: /// Cast to `AnyObject` is required. Otherwise, `nil` value will fail this condition. @@ -64,16 +73,77 @@ public extension EIP712Hashable { preconditionFailure("Not solidity type") } } - guard result.count == 32 else { preconditionFailure("ABI encode error") } + guard result.count % 32 == 0 else { preconditionFailure("ABI encode error") } parameters.append(result) } return Data(parameters.flatMap { $0.bytes }).sha3(.keccak256) } } -public func eip712encode(domainSeparator: EIP712Hashable, message: EIP712Hashable) throws -> Data { - let data = try Data([UInt8(0x19), UInt8(0x01)]) + domainSeparator.hash() + message.hash() - return data.sha3(.keccak256) +public func eip712hash(domainSeparator: EIP712Hashable, message: EIP712Hashable) throws -> Data { + try eip712hash(domainSeparatorHash: domainSeparator.hash(), messageHash: message.hash()) +} + +public func eip712hash(_ eip712TypedData: EIP712TypedData) throws -> Data { + guard let chainId = eip712TypedData.domain["chainId"] as? Int64, + let verifyingContract = eip712TypedData.domain["verifyingContract"] as? String, + let verifyingContractAddress = EIP712.Address(verifyingContract) + else { + throw Web3Error.inputError(desc: "Failed to parse chainId or verifyingContract address. Domain object is \(eip712TypedData.domain).") + } + + let domainHash = try EIP712Domain(chainId: EIP712.UInt256(chainId), verifyingContract: verifyingContractAddress).hash() + guard let primaryTypeData = eip712TypedData.types[eip712TypedData.primaryType] else { + throw Web3Error.inputError(desc: "EIP712 hashing error. Given primary type name is not present amongst types. primaryType - \(eip712TypedData.primaryType); available types - \(eip712TypedData.types.values)") + } + + let messageHash = try hashEip712Message(eip712TypedData, + eip712TypedData.message, + messageTypeData: primaryTypeData) + return eip712hash(domainSeparatorHash: domainHash, messageHash: messageHash) +} + +func hashEip712Message(_ typedData: EIP712TypedData, _ message: [String: AnyObject], messageTypeData: [EIP712TypeProperty]) throws -> Data { + var messageData: [Data] = [] + for field in messageTypeData { + guard let fieldValue = message[field.name] else { + throw Web3Error.inputError(desc: "EIP712 message doesn't have field with name \(field.name).") + } + + if let customType = typedData.types[field.type] { + guard let objectAttribute = fieldValue as? [String: AnyObject] else { + 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])).") + } + try messageData.append(hashEip712Message(typedData, objectAttribute, messageTypeData: customType)) + } else { + let type = try ABITypeParser.parseTypeString(field.type) + var data: Data? + switch type { + case .dynamicBytes, .bytes: + if let bytes = fieldValue as? Data { + data = bytes.sha3(.keccak256) + } + case .string: + if let string = fieldValue as? String { + data = Data(string.bytes).sha3(.keccak256) + } + default: + data = ABIEncoder.encodeSingleType(type: type, value: fieldValue) + } + + if let data = data { + messageData.append(data) + } else { + throw Web3Error.processingError(desc: "Failed to encode property of EIP712 message. Property name \(field.name); value \(String(describing: message[field.name]))") + } + } + } + + return Data(messageData.flatMap { $0.bytes }).sha3(.keccak256) +} + +public func eip712hash(domainSeparatorHash: Data, messageHash: Data) -> Data { + (Data([UInt8(0x19), UInt8(0x01)]) + domainSeparatorHash + messageHash).sha3(.keccak256) } // MARK: - Additional private and public extensions with support members diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift new file mode 100644 index 000000000..31c72b6ee --- /dev/null +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -0,0 +1,140 @@ +// +// EIP712Parser.swift +// +// Created by JeneaVranceanu on 17.10.2023. +// + +import Foundation +import Web3Core + +/// The only purpose of this class is to parse raw JSON and output an EIP712 hash. +/// Example of a payload that is received via `eth_signTypedData` for signing: +/// ``` +/// { +/// "types":{ +/// "EIP712Domain":[ +/// { +/// "name":"name", +/// "type":"string" +/// }, +/// { +/// "name":"version", +/// "type":"string" +/// }, +/// { +/// "name":"chainId", +/// "type":"uint256" +/// }, +/// { +/// "name":"verifyingContract", +/// "type":"address" +/// } +/// ], +/// "Person":[ +/// { +/// "name":"name", +/// "type":"string" +/// }, +/// { +/// "name":"wallet", +/// "type":"address" +/// } +/// ], +/// "Mail":[ +/// { +/// "name":"from", +/// "type":"Person" +/// }, +/// { +/// "name":"to", +/// "type":"Person" +/// }, +/// { +/// "name":"contents", +/// "type":"string" +/// } +/// ] +/// }, +/// "primaryType":"Mail", +/// "domain":{ +/// "name":"Ether Mail", +/// "version":"1", +/// "chainId":1, +/// "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" +/// }, +/// "message":{ +/// "from":{ +/// "name":"Cow", +/// "wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" +/// }, +/// "to":{ +/// "name":"Bob", +/// "wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" +/// }, +/// "contents":"Hello, Bob!" +/// } +/// } +/// ``` +public class EIP712Parser { + static func toData(_ json: String) throws -> Data { + guard let json = json.data(using: .utf8) else { + throw Web3Error.inputError(desc: "Failed to parse EIP712 payload. Given string is not valid UTF8 string. \(json)") + } + return json + } + + public static func parse(_ rawJson: String) throws -> EIP712TypedData { + try parse(try toData(rawJson)) + } + + public static func parse(_ rawJson: Data) throws -> EIP712TypedData { + let decoder = JSONDecoder() + let types = try decoder.decode(EIP712TypeArray.self, from: rawJson).types + guard let json = try rawJson.asJsonDictionary(), + let primaryType = json["primaryType"] as? String, + let domain = json["domain"] as? [String : AnyObject], + let message = json["message"] as? [String : AnyObject] + else { + throw Web3Error.inputError(desc: "EIP712Parser: cannot decode EIP712TypedData object. Failed to parse one of primaryType, domain or message fields. Is any field missing?") + } + + return EIP712TypedData(types: types, primaryType: primaryType, domain: domain, message: message) + } +} + +internal struct EIP712TypeArray: Codable { + public let types: [String : [EIP712TypeProperty]] +} + +public struct EIP712TypeProperty: Codable { + /// Property name. An arbitrary string. + public let name: String + /// Property type. A type that's ABI encodable. + public let type: String + + public init(name: String, type: String) { + self.name = name + self.type = type + } +} + +public struct EIP712TypedData { + public let types: [String: [EIP712TypeProperty]] + /// A name of one of the types from `types`. + public let primaryType: String + /// A JSON object as a string + public let domain: [String : AnyObject] + /// A JSON object as a string + public let message: [String : AnyObject] + + public init(types: [String : [EIP712TypeProperty]], + primaryType: String, + domain: [String : AnyObject], + message: [String : AnyObject]) { + self.types = types + self.primaryType = primaryType + self.domain = domain + self.message = message + } + +} diff --git a/Sources/web3swift/Utils/Extensions/Data+Extension.swift b/Sources/web3swift/Utils/Extensions/Data+Extension.swift new file mode 100644 index 000000000..32aeb2853 --- /dev/null +++ b/Sources/web3swift/Utils/Extensions/Data+Extension.swift @@ -0,0 +1,14 @@ +// +// Data+Extension.swift +// +// Created by JeneaVranceanu on 18.10.2023. +// + +import Foundation + +extension Data { + + func asJsonDictionary() throws -> [String: AnyObject]? { + try JSONSerialization.jsonObject(with: self, options: .mutableContainers) as? [String:AnyObject] + } +} diff --git a/Sources/web3swift/Utils/Extensions/String+Extension.swift b/Sources/web3swift/Utils/Extensions/String+Extension.swift new file mode 100644 index 000000000..704897c8b --- /dev/null +++ b/Sources/web3swift/Utils/Extensions/String+Extension.swift @@ -0,0 +1,16 @@ +// +// String+Extension.swift +// +// +// Created by JeneaVranceanu on 17.10.2023. +// + +import Foundation + +extension String { + + func asJsonDictionary() throws -> [String: AnyObject]? { + guard let data = data(using: .utf8) else { return nil } + return try data.asJsonDictionary() + } +} diff --git a/Sources/web3swift/Web3/Web3+Signing.swift b/Sources/web3swift/Web3/Web3+Signing.swift index 46fe01ce5..d87c4f3dc 100755 --- a/Sources/web3swift/Web3/Web3+Signing.swift +++ b/Sources/web3swift/Web3/Web3+Signing.swift @@ -40,6 +40,21 @@ public struct Web3Signer { return compressedSignature } + public static func signEIP712(_ eip712TypedDataPayload: EIP712TypedData, + keystore: BIP32Keystore, + account: EthereumAddress, + password: String? = nil) throws -> Data { + let hash = try eip712hash(eip712TypedDataPayload) + guard let signature = try Web3Signer.signPersonalMessage(hash, + keystore: keystore, + account: account, + password: password ?? "") + else { + throw Web3Error.dataError + } + return signature + } + public static func signEIP712(_ eip712Hashable: EIP712Hashable, keystore: BIP32Keystore, verifyingContract: EthereumAddress, @@ -48,7 +63,7 @@ public struct Web3Signer { chainId: BigUInt? = nil) throws -> Data { let domainSeparator: EIP712Hashable = EIP712Domain(chainId: chainId, verifyingContract: verifyingContract) - let hash = try eip712encode(domainSeparator: domainSeparator, message: eip712Hashable) + let hash = try eip712hash(domainSeparator: domainSeparator, message: eip712Hashable) guard let signature = try Web3Signer.signPersonalMessage(hash, keystore: keystore, account: account, diff --git a/Tests/web3swiftTests/localTests/EIP712Tests.swift b/Tests/web3swiftTests/localTests/EIP712Tests.swift index 3f527d2cf..00a40e1fc 100644 --- a/Tests/web3swiftTests/localTests/EIP712Tests.swift +++ b/Tests/web3swiftTests/localTests/EIP712Tests.swift @@ -2,7 +2,74 @@ import XCTest import Web3Core @testable import web3swift -class EIP712Tests: LocalTestCase { +class EIP712Tests: XCTestCase { + let testTypedDataPayload = """ + { + "types":{ + "EIP712Domain":[ + { + "name":"name", + "type":"string" + }, + { + "name":"version", + "type":"string" + }, + { + "name":"chainId", + "type":"uint256" + }, + { + "name":"verifyingContract", + "type":"address" + } + ], + "Person":[ + { + "name":"name", + "type":"string" + }, + { + "name":"wallet", + "type":"address" + } + ], + "Mail":[ + { + "name":"from", + "type":"Person" + }, + { + "name":"to", + "type":"Person" + }, + { + "name":"contents", + "type":"string" + } + ] + }, + "primaryType":"Mail", + "domain":{ + "name":"Ether Mail", + "version":"1", + "chainId":1, + "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message":{ + "from":{ + "name":"Cow", + "wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to":{ + "name":"Bob", + "wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents":"Hello, Bob!" + } + } + """ + func testWithoutChainId() throws { let to = EthereumAddress("0x3F06bAAdA68bB997daB03d91DBD0B73e196c5A4d")! let value = EIP712.UInt256(0) @@ -104,4 +171,8 @@ class EIP712Tests: LocalTestCase { chainId: chainId) XCTAssertEqual(signature.toHexString(), "9ee2aadf14739e1cafc3bc1a0b48457c12419d5b480a8ffa86eb7df538c82d0753ca2a6f8024dea576b383cbcbe5e2b181b087e489298674bf6512756cabc5b01b") } + + func testEIP712Parser() throws { + try NSLog(String.init(describing: EIP712Parser.parse(testTypedDataPayload))) + } } From 0342f911c8caf121453936ddfb3f4d9c4b03c515 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Wed, 18 Oct 2023 13:43:29 +0300 Subject: [PATCH 02/23] featEIP712): parser test --- .../localTests/EIP712ParserTests.swift | 124 ++++++++++++++++++ .../localTests/EIP712Tests.swift | 70 ---------- 2 files changed, 124 insertions(+), 70 deletions(-) create mode 100644 Tests/web3swiftTests/localTests/EIP712ParserTests.swift diff --git a/Tests/web3swiftTests/localTests/EIP712ParserTests.swift b/Tests/web3swiftTests/localTests/EIP712ParserTests.swift new file mode 100644 index 000000000..54274c46f --- /dev/null +++ b/Tests/web3swiftTests/localTests/EIP712ParserTests.swift @@ -0,0 +1,124 @@ +// +// EIP712ParserTests.swift +// +// +// Created by JeneaVranceanu on 18.10.2023. +// + +import Foundation +import XCTest +import web3swift +import Web3Core + +class EIP712ParserTests: XCTestCase { + let testTypedDataPayload = """ + { + "types":{ + "EIP712Domain":[ + { + "name":"name", + "type":"string" + }, + { + "name":"version", + "type":"string" + }, + { + "name":"chainId", + "type":"uint256" + }, + { + "name":"verifyingContract", + "type":"address" + } + ], + "Person":[ + { + "name":"name", + "type":"string" + }, + { + "name":"wallet", + "type":"address" + } + ], + "Mail":[ + { + "name":"from", + "type":"Person" + }, + { + "name":"to", + "type":"Person" + }, + { + "name":"contents", + "type":"string" + } + ] + }, + "primaryType":"Mail", + "domain":{ + "name":"Ether Mail", + "version":"1", + "chainId":1, + "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message":{ + "from":{ + "name":"Cow", + "wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to":{ + "name":"Bob", + "wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents":"Hello, Bob!" + } + } + """ + + func testEIP712Parser() throws { + let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + + XCTAssertEqual(parsedEip712TypedData.types.count, 3) + let eip712Domain = parsedEip712TypedData.types["EIP712Domain"] + XCTAssertNotNil(eip712Domain) + let person = parsedEip712TypedData.types["Person"] + XCTAssertNotNil(person) + let mail = parsedEip712TypedData.types["Mail"] + XCTAssertNotNil(mail) + + + XCTAssertNotNil(eip712Domain?.first { $0.name == "name" && $0.type == "string"}) + XCTAssertNotNil(eip712Domain?.first { $0.name == "version" && $0.type == "string"}) + XCTAssertNotNil(eip712Domain?.first { $0.name == "chainId" && $0.type == "uint256"}) + XCTAssertNotNil(eip712Domain?.first { $0.name == "verifyingContract" && $0.type == "address"}) + + + XCTAssertNotNil(person?.first { $0.name == "name" && $0.type == "string"}) + XCTAssertNotNil(person?.first { $0.name == "wallet" && $0.type == "address"}) + + XCTAssertNotNil(mail?.first { $0.name == "from" && $0.type == "Person"}) + XCTAssertNotNil(mail?.first { $0.name == "to" && $0.type == "Person"}) + XCTAssertNotNil(mail?.first { $0.name == "contents" && $0.type == "string"}) + + XCTAssertEqual(parsedEip712TypedData.primaryType, "Mail") + + XCTAssertEqual(parsedEip712TypedData.domain.count, 4) + XCTAssertEqual(parsedEip712TypedData.domain["name"] as? String, "Ether Mail") + XCTAssertEqual(parsedEip712TypedData.domain["version"] as? String, "1") + XCTAssertEqual(parsedEip712TypedData.domain["chainId"] as? Int, 1) + XCTAssertEqual(parsedEip712TypedData.domain["verifyingContract"] as? String, "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC") + + + XCTAssertEqual(parsedEip712TypedData.message.count, 3) + XCTAssertEqual(parsedEip712TypedData.message["from"] as? [String : String], + ["name" : "Cow", + "wallet" : "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"]) + XCTAssertEqual(parsedEip712TypedData.message["to"] as? [String : String], + ["name" : "Bob", + "wallet" : "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"]) + XCTAssertEqual(parsedEip712TypedData.message["contents"] as? String, "Hello, Bob!") + } +} diff --git a/Tests/web3swiftTests/localTests/EIP712Tests.swift b/Tests/web3swiftTests/localTests/EIP712Tests.swift index 00a40e1fc..908490e88 100644 --- a/Tests/web3swiftTests/localTests/EIP712Tests.swift +++ b/Tests/web3swiftTests/localTests/EIP712Tests.swift @@ -3,72 +3,6 @@ import Web3Core @testable import web3swift class EIP712Tests: XCTestCase { - let testTypedDataPayload = """ - { - "types":{ - "EIP712Domain":[ - { - "name":"name", - "type":"string" - }, - { - "name":"version", - "type":"string" - }, - { - "name":"chainId", - "type":"uint256" - }, - { - "name":"verifyingContract", - "type":"address" - } - ], - "Person":[ - { - "name":"name", - "type":"string" - }, - { - "name":"wallet", - "type":"address" - } - ], - "Mail":[ - { - "name":"from", - "type":"Person" - }, - { - "name":"to", - "type":"Person" - }, - { - "name":"contents", - "type":"string" - } - ] - }, - "primaryType":"Mail", - "domain":{ - "name":"Ether Mail", - "version":"1", - "chainId":1, - "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message":{ - "from":{ - "name":"Cow", - "wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to":{ - "name":"Bob", - "wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents":"Hello, Bob!" - } - } - """ func testWithoutChainId() throws { let to = EthereumAddress("0x3F06bAAdA68bB997daB03d91DBD0B73e196c5A4d")! @@ -171,8 +105,4 @@ class EIP712Tests: XCTestCase { chainId: chainId) XCTAssertEqual(signature.toHexString(), "9ee2aadf14739e1cafc3bc1a0b48457c12419d5b480a8ffa86eb7df538c82d0753ca2a6f8024dea576b383cbcbe5e2b181b087e489298674bf6512756cabc5b01b") } - - func testEIP712Parser() throws { - try NSLog(String.init(describing: EIP712Parser.parse(testTypedDataPayload))) - } } From a9b23fb76690be6c7c613c76de5a0fb3e7ea41bf Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Wed, 18 Oct 2023 13:45:01 +0300 Subject: [PATCH 03/23] chore(EIP712): renamed test file --- ...12ParserTests.swift => EIP712TypedDataPayloadTests.swift} | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename Tests/web3swiftTests/localTests/{EIP712ParserTests.swift => EIP712TypedDataPayloadTests.swift} (97%) diff --git a/Tests/web3swiftTests/localTests/EIP712ParserTests.swift b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift similarity index 97% rename from Tests/web3swiftTests/localTests/EIP712ParserTests.swift rename to Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift index 54274c46f..54f985c8f 100644 --- a/Tests/web3swiftTests/localTests/EIP712ParserTests.swift +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -1,6 +1,5 @@ // -// EIP712ParserTests.swift -// +// EIP712TypedDataPayloadTests.swift // // Created by JeneaVranceanu on 18.10.2023. // @@ -10,7 +9,7 @@ import XCTest import web3swift import Web3Core -class EIP712ParserTests: XCTestCase { +class EIP712TypedDataPayloadTests: XCTestCase { let testTypedDataPayload = """ { "types":{ From ce459b6ef675c761997f42db71e36201b5865649 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Wed, 18 Oct 2023 15:28:30 +0300 Subject: [PATCH 04/23] feat(EIP712): type encoding, type hashing, circular dependency checks, tests; --- .../Utils/EIP/EIP712/EIP712Parser.swift | 100 +++++++++++++++++- .../EIP712TypedDataPayloadTests.swift | 99 +++++++++++++++++ 2 files changed, 197 insertions(+), 2 deletions(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index 31c72b6ee..8e1370a75 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -98,7 +98,7 @@ public class EIP712Parser { throw Web3Error.inputError(desc: "EIP712Parser: cannot decode EIP712TypedData object. Failed to parse one of primaryType, domain or message fields. Is any field missing?") } - return EIP712TypedData(types: types, primaryType: primaryType, domain: domain, message: message) + return try EIP712TypedData(types: types, primaryType: primaryType, domain: domain, message: message) } } @@ -130,11 +130,107 @@ public struct EIP712TypedData { public init(types: [String : [EIP712TypeProperty]], primaryType: String, domain: [String : AnyObject], - message: [String : AnyObject]) { + message: [String : AnyObject]) throws { self.types = types self.primaryType = primaryType self.domain = domain self.message = message + if let problematicType = hasCircularDependency() { + throw Web3Error.inputError(desc: "Created EIP712TypedData has a circular dependency amongst it's types. Cycle was first identified in '\(problematicType)'. Review it's uses in 'types'.") + } } + /// Checks for a circular dependency among the given types. + /// + /// If a circular dependency is detected, it returns the name of the type where the cycle was first identified. + /// Otherwise, it returns `nil`. + /// + /// - Returns: The type name where a circular dependency is detected, or `nil` if no circular dependency exists. + /// - Note: The function utilizes depth-first search to identify the circular dependencies. + func hasCircularDependency() -> String? { + + /// Generates an adjacency list for the given types, representing their dependencies. + /// + /// - Parameter types: A dictionary mapping type names to their property definitions. + /// - Returns: An adjacency list representing type dependencies. + func createAdjacencyList(types: [String: [EIP712TypeProperty]]) -> [String: [String]] { + var adjList: [String: [String]] = [:] + + for (typeName, fields) in types { + adjList[typeName] = [] + for field in fields { + if types.keys.contains(field.type) { + adjList[typeName]?.append(field.type) + } + } + } + + return adjList + } + + let adjList = createAdjacencyList(types: types) + + /// Depth-first search to check for circular dependencies. + /// + /// - Parameters: + /// - node: The current type being checked. + /// - visited: A dictionary keeping track of the visited types. + /// - stack: A dictionary used for checking the current path for cycles. + /// + /// - Returns: `true` if a cycle is detected from the current node, `false` otherwise. + func depthFirstSearch(node: String, visited: inout [String: Bool], stack: inout [String: Bool]) -> Bool { + visited[node] = true + stack[node] = true + + for neighbor in adjList[node] ?? [] { + if visited[neighbor] == nil { + if depthFirstSearch(node: neighbor, visited: &visited, stack: &stack) { + return true + } + } else if stack[neighbor] == true { + return true + } + } + + stack[node] = false + return false + } + + var visited: [String: Bool] = [:] + var stack: [String: Bool] = [:] + + for typeName in adjList.keys { + if visited[typeName] == nil { + if depthFirstSearch(node: typeName, visited: &visited, stack: &stack) { + return typeName + } + } + } + + return nil + } + + public func encodeType(_ type: String) throws -> String { + guard let typeData = types[type] else { + throw Web3Error.processingError(desc: "EIP712. Attempting to encode type that doesn't exist in this payload. Given type: \(type). Available types: \(types.values).") + } + return try encodeType(type, typeData) + } + + public func typeHash(_ type: String) throws -> String { + try encodeType(type).sha3(.keccak256).addHexPrefix() + } + + internal func encodeType(_ type: String, _ typeData: [EIP712TypeProperty], typesCovered: [String] = []) throws -> String { + var typesCovered = typesCovered + var encodedSubtypes: [String] = [] + let parameters = try typeData.map { attributeType in + if let innerTypes = types[attributeType.type], !typesCovered.contains(attributeType.type) { + encodedSubtypes.append(try encodeType(attributeType.type, innerTypes)) + typesCovered.append(attributeType.type) + } + return "\(attributeType.type) \(attributeType.name)" + } + return type + "(" + parameters.joined(separator: ",") + ")" + encodedSubtypes.joined(separator: "") + } } diff --git a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift index 54f985c8f..ddf6321ad 100644 --- a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -120,4 +120,103 @@ class EIP712TypedDataPayloadTests: XCTestCase { "wallet" : "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"]) XCTAssertEqual(parsedEip712TypedData.message["contents"] as? String, "Hello, Bob!") } + + func testEIP712CircularDependency() throws { + let problematicTypeExample = """ + { + "types":{ + "EIP712Domain":[ + { + "name":"name", + "type":"string" + }, + { + "name":"version", + "type":"string" + }, + { + "name":"chainId", + "type":"uint256" + }, + { + "name":"verifyingContract", + "type":"address" + } + ], + "Person":[ + { + "name":"name", + "type":"string" + }, + { + "name":"wallet", + "type":"address" + }, + { + "name":"mail", + "type":"Mail" + } + ], + "Mail":[ + { + "name":"from", + "type":"Person" + }, + { + "name":"to", + "type":"Person" + }, + { + "name":"contents", + "type":"string" + } + ] + }, + "primaryType":"Mail", + "domain":{ + "name":"Ether Mail", + "version":"1", + "chainId":1, + "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message":{ + "from":{ + "name":"Cow", + "wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to":{ + "name":"Bob", + "wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents":"Hello, Bob!" + } + } + """ + XCTAssertThrowsError(try EIP712Parser.parse(problematicTypeExample)) { error in + guard let error = error as? Web3Error else { + XCTFail("Thrown error is not Web3Error.") + return + } + + if case let .inputError(desc) = error { + XCTAssertTrue(desc.hasPrefix("Created EIP712TypedData has a circular dependency amongst it's types.")) + } else { + XCTFail("A different Web3Error is thrown. Something changed?") + } + } + } + + func testEIP712EncodeType() throws { + let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + try XCTAssertEqual(parsedEip712TypedData.encodeType("EIP712Domain"), "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") + try XCTAssertEqual(parsedEip712TypedData.encodeType("Person"), "Person(string name,address wallet)") + try XCTAssertEqual(parsedEip712TypedData.encodeType("Mail"), "Mail(Person from,Person to,string contents)Person(string name,address wallet)") + } + + func testEIP712TypeHash() throws { + let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + try XCTAssertEqual(parsedEip712TypedData.typeHash("EIP712Domain"), "0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f") + try XCTAssertEqual(parsedEip712TypedData.typeHash("Person"), "0xb9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c79500") + try XCTAssertEqual(parsedEip712TypedData.typeHash("Mail"), "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2") + } } From 89595ad46ed5161d18e3459b2c77ac7fc7593ed0 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Wed, 18 Oct 2023 15:32:15 +0300 Subject: [PATCH 05/23] chore(EIP712): added link to the tests + implementation source --- .../web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift index ddf6321ad..d5c9cb11c 100644 --- a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -9,6 +9,8 @@ import XCTest import web3swift import Web3Core + +/// Tests based primarily on the following example https://eips.ethereum.org/assets/eip-712/Example.js class EIP712TypedDataPayloadTests: XCTestCase { let testTypedDataPayload = """ { From 27d5b611d9a7194453773402660a43f6a00d6b28 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Thu, 19 Oct 2023 00:07:46 +0300 Subject: [PATCH 06/23] feat(EIP712): impl of encodeData, structHash and signHash functions for EIP712TypedData --- .../web3swift/Utils/EIP/EIP712/EIP712.swift | 58 ----------------- .../Utils/EIP/EIP712/EIP712Parser.swift | 62 +++++++++++++++++++ Sources/web3swift/Web3/Web3+Signing.swift | 8 ++- .../EIP712TypedDataPayloadTests.swift | 54 +++++++++++++++- 4 files changed, 120 insertions(+), 62 deletions(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712.swift index 1d0b85147..245fab7e6 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712.swift @@ -84,64 +84,6 @@ public func eip712hash(domainSeparator: EIP712Hashable, message: EIP712Hashable) try eip712hash(domainSeparatorHash: domainSeparator.hash(), messageHash: message.hash()) } -public func eip712hash(_ eip712TypedData: EIP712TypedData) throws -> Data { - guard let chainId = eip712TypedData.domain["chainId"] as? Int64, - let verifyingContract = eip712TypedData.domain["verifyingContract"] as? String, - let verifyingContractAddress = EIP712.Address(verifyingContract) - else { - throw Web3Error.inputError(desc: "Failed to parse chainId or verifyingContract address. Domain object is \(eip712TypedData.domain).") - } - - let domainHash = try EIP712Domain(chainId: EIP712.UInt256(chainId), verifyingContract: verifyingContractAddress).hash() - guard let primaryTypeData = eip712TypedData.types[eip712TypedData.primaryType] else { - throw Web3Error.inputError(desc: "EIP712 hashing error. Given primary type name is not present amongst types. primaryType - \(eip712TypedData.primaryType); available types - \(eip712TypedData.types.values)") - } - - let messageHash = try hashEip712Message(eip712TypedData, - eip712TypedData.message, - messageTypeData: primaryTypeData) - return eip712hash(domainSeparatorHash: domainHash, messageHash: messageHash) -} - -func hashEip712Message(_ typedData: EIP712TypedData, _ message: [String: AnyObject], messageTypeData: [EIP712TypeProperty]) throws -> Data { - var messageData: [Data] = [] - for field in messageTypeData { - guard let fieldValue = message[field.name] else { - throw Web3Error.inputError(desc: "EIP712 message doesn't have field with name \(field.name).") - } - - if let customType = typedData.types[field.type] { - guard let objectAttribute = fieldValue as? [String: AnyObject] else { - 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])).") - } - try messageData.append(hashEip712Message(typedData, objectAttribute, messageTypeData: customType)) - } else { - let type = try ABITypeParser.parseTypeString(field.type) - var data: Data? - switch type { - case .dynamicBytes, .bytes: - if let bytes = fieldValue as? Data { - data = bytes.sha3(.keccak256) - } - case .string: - if let string = fieldValue as? String { - data = Data(string.bytes).sha3(.keccak256) - } - default: - data = ABIEncoder.encodeSingleType(type: type, value: fieldValue) - } - - if let data = data { - messageData.append(data) - } else { - throw Web3Error.processingError(desc: "Failed to encode property of EIP712 message. Property name \(field.name); value \(String(describing: message[field.name]))") - } - } - } - - return Data(messageData.flatMap { $0.bytes }).sha3(.keccak256) -} - public func eip712hash(domainSeparatorHash: Data, messageHash: Data) -> Data { (Data([UInt8(0x19), UInt8(0x01)]) + domainSeparatorHash + messageHash).sha3(.keccak256) } diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index 8e1370a75..fb5860ce5 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -233,4 +233,66 @@ public struct EIP712TypedData { } return type + "(" + parameters.joined(separator: ",") + ")" + encodedSubtypes.joined(separator: "") } + + /// Convenience function for ``encodeData(_:data:)`` that uses ``primaryType`` and ``message`` as values. + /// - Returns: encoded data based on ``primaryType`` and ``message``. + public func encodeData() throws -> Data { + try encodeData(primaryType, data: message) + } + + public func encodeData(_ type: String, data: [String : AnyObject]) throws -> Data { + // Adding typehash + var encTypes: [ABI.Element.ParameterType] = [.bytes(length: 32)] + var encValues: [Any] = [try typeHash(type)] + + guard let typeData = types[type] else { + 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).") + } + + // Add field contents + for field in typeData { + let value = data[field.name] + if field.type == "string" { + guard let value = value as? String else { + throw Web3Error.processingError(desc: "EIP712. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to String.") + } + encTypes.append(.bytes(length: 32)) + encValues.append(value.sha3(.keccak256).addHexPrefix()) + } else if field.type == "bytes"{ + guard let value = value as? Data else { + throw Web3Error.processingError(desc: "EIP712. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to Data.") + } + encTypes.append(.bytes(length: 32)) + encValues.append(value.sha3(.keccak256)) + } else if types[field.type] != nil { + guard let value = value as? [String : AnyObject] else { + 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].") + } + encTypes.append(.bytes(length: 32)) + encValues.append(try encodeData(field.type, data: value).sha3(.keccak256)) + } else { + encTypes.append(try ABITypeParser.parseTypeString(field.type)) + encValues.append(value as Any) + } + } + + guard let encodedData = ABIEncoder.encode(types: encTypes, values: encValues) else { + throw Web3Error.processingError(desc: "EIP712. ABIEncoder.encode failed with the following types and values: \(encTypes); \(encValues)") + } + return encodedData + } + + /// Convenience function for ``structHash(_:data:)`` that uses ``primaryType`` and ``message`` as values. + /// - Returns: SH# keccak256 hash of encoded data based on ``primaryType`` and ``message``. + public func structHash() throws -> Data { + try structHash(primaryType, data: message) + } + + public func structHash(_ type: String, data: [String : AnyObject]) throws -> Data { + try encodeData(type, data: data).sha3(.keccak256) + } + + public func signHash() throws -> Data { + try (Data.fromHex("0x1901")! + structHash("EIP712Domain", data: domain) + structHash()).sha3(.keccak256) + } } diff --git a/Sources/web3swift/Web3/Web3+Signing.swift b/Sources/web3swift/Web3/Web3+Signing.swift index d87c4f3dc..295d1a497 100755 --- a/Sources/web3swift/Web3/Web3+Signing.swift +++ b/Sources/web3swift/Web3/Web3+Signing.swift @@ -44,11 +44,12 @@ public struct Web3Signer { keystore: BIP32Keystore, account: EthereumAddress, password: String? = nil) throws -> Data { - let hash = try eip712hash(eip712TypedDataPayload) + let hash = try eip712TypedDataPayload.signHash() guard let signature = try Web3Signer.signPersonalMessage(hash, keystore: keystore, account: account, - password: password ?? "") + password: password ?? "", + useHash: false) else { throw Web3Error.dataError } @@ -67,7 +68,8 @@ public struct Web3Signer { guard let signature = try Web3Signer.signPersonalMessage(hash, keystore: keystore, account: account, - password: password ?? "") + password: password ?? "", + useHash: false) else { throw Web3Error.dataError } diff --git a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift index d5c9cb11c..3c62db42f 100644 --- a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -7,8 +7,9 @@ import Foundation import XCTest import web3swift -import Web3Core +@testable import Web3Core +// TODO: take more tests from https://github.com/Mrtenz/eip-712/blob/master/src/eip-712.test.ts /// Tests based primarily on the following example https://eips.ethereum.org/assets/eip-712/Example.js class EIP712TypedDataPayloadTests: XCTestCase { @@ -221,4 +222,55 @@ class EIP712TypedDataPayloadTests: XCTestCase { try XCTAssertEqual(parsedEip712TypedData.typeHash("Person"), "0xb9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c79500") try XCTAssertEqual(parsedEip712TypedData.typeHash("Mail"), "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2") } + + func testEIP712EncodeData() throws { + let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + let encodedMessage = "a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8" + XCTAssertEqual(try parsedEip712TypedData.encodeData().toHexString(), encodedMessage) + XCTAssertEqual(try parsedEip712TypedData.encodeData(parsedEip712TypedData.primaryType, data: parsedEip712TypedData.message).toHexString(), encodedMessage) + + XCTAssertEqual(try parsedEip712TypedData.encodeData("EIP712Domain", data: parsedEip712TypedData.domain).toHexString(), + "8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400fc70ef06638535b4881fafcac8287e210e3769ff1a8e91f1b95d6246e61e4d3c6c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc60000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cccccccccccccccccccccccccccccccccccccccc") + + XCTAssertEqual(try parsedEip712TypedData.encodeData("Person", data: parsedEip712TypedData.message["from"] as! [String : AnyObject]).toHexString(), + "b9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c795008c1d2bd5348394761719da11ec67eedae9502d137e8940fee8ecd6f641ee1648000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826") + + XCTAssertEqual(try parsedEip712TypedData.encodeData("Person", + data: ["wallet" : "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "name" : "Cow"] as [String : AnyObject]).toHexString(), + "b9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c795008c1d2bd5348394761719da11ec67eedae9502d137e8940fee8ecd6f641ee1648000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826") + } + + func testEIP712StructHash() throws { + let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + XCTAssertEqual(try parsedEip712TypedData.structHash().toHexString(), "c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e") + XCTAssertEqual(try parsedEip712TypedData.structHash("EIP712Domain", data: parsedEip712TypedData.domain).toHexString(), + "f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f") + } + + func testEIP712SignHash() throws { + let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + XCTAssertEqual(try parsedEip712TypedData.signHash().toHexString(), "be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2") + } + + func testEIP712Signing() throws { + let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + let privateKey = Data.fromHex("cow".sha3(.keccak256).addHexPrefix())! + let publicKey = Utilities.privateToPublic(privateKey)! + let address = Utilities.publicToAddress(publicKey)! + XCTAssertEqual(address, EthereumAddress("0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826")); + + /// This signing doesn't use `"\u{19}Ethereum Signed Message:\n"`. As per EIP712 standard + /// the following format is used instead: + /// ``` + /// encode(domainSeparator : ๐”นยฒโตโถ, message : ๐•Š) = "\x19\x01" โ€– domainSeparator โ€– hashStruct(message) + /// ``` + /// + /// The output of ``EIP712TypedData.signHash`` is exactly that. + let (compressedSignature, _) = try SECP256K1.signForRecovery(hash: parsedEip712TypedData.signHash(), privateKey: privateKey) + let unmarshalledSignature = Utilities.unmarshalSignature(signatureData: compressedSignature!)! + XCTAssertEqual(unmarshalledSignature.v, 28) + XCTAssertEqual(unmarshalledSignature.r.toHexString(), "4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d") + XCTAssertEqual(unmarshalledSignature.s.toHexString(), "07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562") + } } From 07965a1a126192f7452645106a237a67ce213ee6 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Thu, 19 Oct 2023 00:10:14 +0300 Subject: [PATCH 07/23] fix: typo --- Sources/web3swift/Web3/Web3+Contract.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/web3swift/Web3/Web3+Contract.swift b/Sources/web3swift/Web3/Web3+Contract.swift index 7bf192710..1877bb65c 100755 --- a/Sources/web3swift/Web3/Web3+Contract.swift +++ b/Sources/web3swift/Web3/Web3+Contract.swift @@ -47,7 +47,7 @@ extension Web3 { // MARK: Writing Data flow // FIXME: Rewrite this to CodableTransaction - /// Deploys a constact instance using the previously provided ABI, some bytecode, constructor parameters and options. + /// Deploys a contract instance using the previously provided ABI, some bytecode, constructor parameters and options. /// If extraData is supplied it is appended to encoded bytecode and constructor parameters. /// /// Returns a "Transaction intermediate" object. From 5493dd0409a862071e9bc926d016d70ed4e93040 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Thu, 19 Oct 2023 00:20:11 +0300 Subject: [PATCH 08/23] chore(EIP712): new test + moved EIP712 reused data into one file; --- .../localTests/EIP712TestData.swift | 76 +++++++++++++++++ .../localTests/EIP712Tests.swift | 12 +++ .../EIP712TypedDataPayloadTests.swift | 81 ++----------------- 3 files changed, 95 insertions(+), 74 deletions(-) create mode 100644 Tests/web3swiftTests/localTests/EIP712TestData.swift diff --git a/Tests/web3swiftTests/localTests/EIP712TestData.swift b/Tests/web3swiftTests/localTests/EIP712TestData.swift new file mode 100644 index 000000000..52ab9a7dc --- /dev/null +++ b/Tests/web3swiftTests/localTests/EIP712TestData.swift @@ -0,0 +1,76 @@ +// +// EIP712TestData.swift +// +// Created by JeneaVranceanu on 19.10.2023. +// + +import Foundation + +class EIP712TestData { + static let testTypedDataPayload = """ + { + "types":{ + "EIP712Domain":[ + { + "name":"name", + "type":"string" + }, + { + "name":"version", + "type":"string" + }, + { + "name":"chainId", + "type":"uint256" + }, + { + "name":"verifyingContract", + "type":"address" + } + ], + "Person":[ + { + "name":"name", + "type":"string" + }, + { + "name":"wallet", + "type":"address" + } + ], + "Mail":[ + { + "name":"from", + "type":"Person" + }, + { + "name":"to", + "type":"Person" + }, + { + "name":"contents", + "type":"string" + } + ] + }, + "primaryType":"Mail", + "domain":{ + "name":"Ether Mail", + "version":"1", + "chainId":1, + "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message":{ + "from":{ + "name":"Cow", + "wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" + }, + "to":{ + "name":"Bob", + "wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" + }, + "contents":"Hello, Bob!" + } + } +""" +} diff --git a/Tests/web3swiftTests/localTests/EIP712Tests.swift b/Tests/web3swiftTests/localTests/EIP712Tests.swift index 908490e88..149c43221 100644 --- a/Tests/web3swiftTests/localTests/EIP712Tests.swift +++ b/Tests/web3swiftTests/localTests/EIP712Tests.swift @@ -105,4 +105,16 @@ class EIP712Tests: XCTestCase { chainId: chainId) XCTAssertEqual(signature.toHexString(), "9ee2aadf14739e1cafc3bc1a0b48457c12419d5b480a8ffa86eb7df538c82d0753ca2a6f8024dea576b383cbcbe5e2b181b087e489298674bf6512756cabc5b01b") } + + func testEIP712TypedDataSigning() throws { + let mnemonic = "normal dune pole key case cradle unfold require tornado mercy hospital buyer" + let keystore = try! BIP32Keystore(mnemonics: mnemonic, password: "", mnemonicsPassword: "")! + let account = keystore.addresses?[0] + let eip712TypedData = try EIP712Parser.parse(EIP712TestData.testTypedDataPayload) + let signature = try Web3Signer.signEIP712( + eip712TypedData, + keystore: keystore, + account: account!) + XCTAssertEqual(signature.toHexString(), "70d1f5d9eac7b6303683d0792ea8dc93369e3b79888c4e0b86121bec19f479ba4067cf7ac3f8208cbc60a706c4793c2c17e19637298bb31642e531619272b26e1b") + } } diff --git a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift index 3c62db42f..7189af1a6 100644 --- a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -13,75 +13,8 @@ import web3swift /// Tests based primarily on the following example https://eips.ethereum.org/assets/eip-712/Example.js class EIP712TypedDataPayloadTests: XCTestCase { - let testTypedDataPayload = """ - { - "types":{ - "EIP712Domain":[ - { - "name":"name", - "type":"string" - }, - { - "name":"version", - "type":"string" - }, - { - "name":"chainId", - "type":"uint256" - }, - { - "name":"verifyingContract", - "type":"address" - } - ], - "Person":[ - { - "name":"name", - "type":"string" - }, - { - "name":"wallet", - "type":"address" - } - ], - "Mail":[ - { - "name":"from", - "type":"Person" - }, - { - "name":"to", - "type":"Person" - }, - { - "name":"contents", - "type":"string" - } - ] - }, - "primaryType":"Mail", - "domain":{ - "name":"Ether Mail", - "version":"1", - "chainId":1, - "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message":{ - "from":{ - "name":"Cow", - "wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to":{ - "name":"Bob", - "wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents":"Hello, Bob!" - } - } - """ - func testEIP712Parser() throws { - let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + let parsedEip712TypedData = try EIP712Parser.parse(EIP712TestData.testTypedDataPayload) XCTAssertEqual(parsedEip712TypedData.types.count, 3) let eip712Domain = parsedEip712TypedData.types["EIP712Domain"] @@ -210,21 +143,21 @@ class EIP712TypedDataPayloadTests: XCTestCase { } func testEIP712EncodeType() throws { - let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + let parsedEip712TypedData = try EIP712Parser.parse(EIP712TestData.testTypedDataPayload) try XCTAssertEqual(parsedEip712TypedData.encodeType("EIP712Domain"), "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") try XCTAssertEqual(parsedEip712TypedData.encodeType("Person"), "Person(string name,address wallet)") try XCTAssertEqual(parsedEip712TypedData.encodeType("Mail"), "Mail(Person from,Person to,string contents)Person(string name,address wallet)") } func testEIP712TypeHash() throws { - let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + let parsedEip712TypedData = try EIP712Parser.parse(EIP712TestData.testTypedDataPayload) try XCTAssertEqual(parsedEip712TypedData.typeHash("EIP712Domain"), "0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f") try XCTAssertEqual(parsedEip712TypedData.typeHash("Person"), "0xb9d8c78acf9b987311de6c7b45bb6a9c8e1bf361fa7fd3467a2163f994c79500") try XCTAssertEqual(parsedEip712TypedData.typeHash("Mail"), "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2") } func testEIP712EncodeData() throws { - let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + let parsedEip712TypedData = try EIP712Parser.parse(EIP712TestData.testTypedDataPayload) let encodedMessage = "a0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8" XCTAssertEqual(try parsedEip712TypedData.encodeData().toHexString(), encodedMessage) XCTAssertEqual(try parsedEip712TypedData.encodeData(parsedEip712TypedData.primaryType, data: parsedEip712TypedData.message).toHexString(), encodedMessage) @@ -242,19 +175,19 @@ class EIP712TypedDataPayloadTests: XCTestCase { } func testEIP712StructHash() throws { - let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + let parsedEip712TypedData = try EIP712Parser.parse(EIP712TestData.testTypedDataPayload) XCTAssertEqual(try parsedEip712TypedData.structHash().toHexString(), "c52c0ee5d84264471806290a3f2c4cecfc5490626bf912d01f240d7a274b371e") XCTAssertEqual(try parsedEip712TypedData.structHash("EIP712Domain", data: parsedEip712TypedData.domain).toHexString(), "f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f") } func testEIP712SignHash() throws { - let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + let parsedEip712TypedData = try EIP712Parser.parse(EIP712TestData.testTypedDataPayload) XCTAssertEqual(try parsedEip712TypedData.signHash().toHexString(), "be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2") } func testEIP712Signing() throws { - let parsedEip712TypedData = try EIP712Parser.parse(testTypedDataPayload) + let parsedEip712TypedData = try EIP712Parser.parse(EIP712TestData.testTypedDataPayload) let privateKey = Data.fromHex("cow".sha3(.keccak256).addHexPrefix())! let publicKey = Utilities.privateToPublic(privateKey)! let address = Utilities.publicToAddress(publicKey)! From 4669bfe91edc325ecd23accb16cebd75ca538896 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Thu, 19 Oct 2023 12:58:39 +0300 Subject: [PATCH 09/23] fix(EIP712): fixed values in tests since personal-message-hashing is not used for EIP712 --- Tests/web3swiftTests/localTests/EIP712Tests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/web3swiftTests/localTests/EIP712Tests.swift b/Tests/web3swiftTests/localTests/EIP712Tests.swift index 149c43221..3473dbf2b 100644 --- a/Tests/web3swiftTests/localTests/EIP712Tests.swift +++ b/Tests/web3swiftTests/localTests/EIP712Tests.swift @@ -52,7 +52,7 @@ class EIP712Tests: XCTestCase { account: account!, password: password, chainId: chainId) - XCTAssertEqual(signature.toHexString(), "c0567b120d3de6b3042ae3de1aa346e167454c675e1eaf40ea2f9de89e6a95c2783c1aa6c96aa1e0aaead4ae8901052fa9fd7abe4acb331adafd61610e93c3f01c") + XCTAssertEqual(signature.toHexString(), "39e48b17008344acd58c86fba540ce65a9a4dad048e0d4d10efced291e02174c7267c9749cd2c1f9738ba1267f6fb8caadd054497daa20e2eaaee6472e7fde4e1b") } func testWithChainId() throws { @@ -103,7 +103,7 @@ class EIP712Tests: XCTestCase { account: account!, password: password, chainId: chainId) - XCTAssertEqual(signature.toHexString(), "9ee2aadf14739e1cafc3bc1a0b48457c12419d5b480a8ffa86eb7df538c82d0753ca2a6f8024dea576b383cbcbe5e2b181b087e489298674bf6512756cabc5b01b") + XCTAssertEqual(signature.toHexString(), "e5ebc20f5794b756f01adb271db9e535df74751dfce4328b2f5bae4740d6e5ef392626b95ae0c0975a91b99033b079e6e0ccd41cb6fa70dd5f8833d78af4282f1c") } func testEIP712TypedDataSigning() throws { From b8e55b227598eebd88cd2b826365b00b403ce7d5 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Fri, 20 Oct 2023 00:37:21 +0300 Subject: [PATCH 10/23] chore(EIP712): example for EIP712Parser; --- Sources/web3swift/Utils/EIP/EIP712/EIP712.swift | 1 - .../web3swift/Utils/EIP/EIP712/EIP712Parser.swift | 14 +++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712.swift index 245fab7e6..21458618b 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712.swift @@ -63,7 +63,6 @@ public extension EIP712Hashable { case let boolean as Bool: result = ABIEncoder.encodeSingleType(type: .uint(bits: 8), value: boolean ? 1 : 0)! case let hashable as EIP712Hashable: - // TODO: should it be hashed here? result = try hashable.hash() default: /// Cast to `AnyObject` is required. Otherwise, `nil` value will fail this condition. diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index fb5860ce5..6078392d2 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -7,7 +7,7 @@ import Foundation import Web3Core -/// The only purpose of this class is to parse raw JSON and output an EIP712 hash. +/// The only purpose of this class is to parse raw JSON and output an EIP712 hash ready for signing. /// Example of a payload that is received via `eth_signTypedData` for signing: /// ``` /// { @@ -75,7 +75,19 @@ import Web3Core /// } /// } /// ``` +/// +/// Example use case: +/// ``` +/// let payload: String = ... // This is the payload received from eth_signTypedData +/// let eip712TypedData = try EIP712Parser.parse(payload) +/// let signature = try Web3Signer.signEIP712( +/// eip712TypedData, +/// keystore: keystore, +/// account: account, +/// password: password) +/// ``` public class EIP712Parser { + static func toData(_ json: String) throws -> Data { guard let json = json.data(using: .utf8) else { throw Web3Error.inputError(desc: "Failed to parse EIP712 payload. Given string is not valid UTF8 string. \(json)") From c01f6a164ee27b56f58cdfb74527f380b1cd8ca1 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Tue, 31 Oct 2023 10:23:50 +0200 Subject: [PATCH 11/23] fix: ParsingError and AbstractKeystoreError are now extensions of LocalizedError type; - updated ParsingError and AbstractKeystoreError with description; - minor refactoring; - added 2 more EIP712 tests; --- Sources/Web3Core/EthereumABI/ABIParsing.swift | 31 ++++- .../Web3Core/EthereumABI/ABITypeParser.swift | 2 +- .../KeystoreManager/AbstractKeystore.swift | 31 ++++- .../KeystoreManager/BIP32Keystore.swift | 115 +++++++++--------- Sources/Web3Core/KeystoreManager/BIP39.swift | 8 +- .../KeystoreManager/EthereumKeystoreV3.swift | 62 ++++------ .../KeystoreManager/KeystoreManager.swift | 2 +- .../Transaction/CodableTransaction.swift | 2 +- .../EIP712TypedDataPayloadTests.swift | 18 +++ 9 files changed, 163 insertions(+), 108 deletions(-) diff --git a/Sources/Web3Core/EthereumABI/ABIParsing.swift b/Sources/Web3Core/EthereumABI/ABIParsing.swift index c4e68aaef..3eaf5d006 100755 --- a/Sources/Web3Core/EthereumABI/ABIParsing.swift +++ b/Sources/Web3Core/EthereumABI/ABIParsing.swift @@ -7,9 +7,9 @@ import Foundation extension ABI { - public enum ParsingError: Swift.Error { + public enum ParsingError: LocalizedError { case invalidJsonFile - case elementTypeInvalid + case elementTypeInvalid(_ desc: String? = nil) case elementNameInvalid case functionInputInvalid case functionOutputInvalid @@ -17,6 +17,31 @@ extension ABI { case parameterTypeInvalid case parameterTypeNotFound case abiInvalid + + public var errorDescription: String? { + var errorMessage: [String?] + switch self { + case .invalidJsonFile: + errorMessage = ["invalidJsonFile"] + case .elementTypeInvalid(let desc): + errorMessage = ["elementTypeInvalid", desc] + case .elementNameInvalid: + errorMessage = ["elementNameInvalid"] + case .functionInputInvalid: + errorMessage = ["functionInputInvalid"] + case .functionOutputInvalid: + errorMessage = ["functionOutputInvalid"] + case .eventInputInvalid: + errorMessage = ["eventInputInvalid"] + case .parameterTypeInvalid: + errorMessage = ["parameterTypeInvalid"] + case .parameterTypeNotFound: + errorMessage = ["parameterTypeNotFound"] + case .abiInvalid: + errorMessage = ["abiInvalid"] + } + return errorMessage.compactMap { $0 }.joined(separator: " ") + } } enum TypeParsingExpressions { @@ -39,7 +64,7 @@ extension ABI.Record { public func parse() throws -> ABI.Element { let typeString = self.type ?? "function" guard let type = ABI.ElementType(rawValue: typeString) else { - throw ABI.ParsingError.elementTypeInvalid + throw ABI.ParsingError.elementTypeInvalid("Invalid ABI type \(typeString).") } return try parseToElement(from: self, type: type) } diff --git a/Sources/Web3Core/EthereumABI/ABITypeParser.swift b/Sources/Web3Core/EthereumABI/ABITypeParser.swift index 753c8788f..d12af4005 100755 --- a/Sources/Web3Core/EthereumABI/ABITypeParser.swift +++ b/Sources/Web3Core/EthereumABI/ABITypeParser.swift @@ -46,7 +46,7 @@ public struct ABITypeParser { public static func parseTypeString(_ string: String) throws -> ABI.Element.ParameterType { let (type, tail) = recursiveParseType(string) - guard let t = type, tail == nil else {throw ABI.ParsingError.elementTypeInvalid} + guard let t = type, tail == nil else { throw ABI.ParsingError.elementTypeInvalid("Invalid ABI type \(string).") } return t } diff --git a/Sources/Web3Core/KeystoreManager/AbstractKeystore.swift b/Sources/Web3Core/KeystoreManager/AbstractKeystore.swift index e8c515241..97b90ce88 100755 --- a/Sources/Web3Core/KeystoreManager/AbstractKeystore.swift +++ b/Sources/Web3Core/KeystoreManager/AbstractKeystore.swift @@ -11,11 +11,30 @@ public protocol AbstractKeystore { func UNSAFE_getPrivateKeyData(password: String, account: EthereumAddress) throws -> Data } -public enum AbstractKeystoreError: Error { - case noEntropyError - case keyDerivationError - case aesError - case invalidAccountError +public enum AbstractKeystoreError: LocalizedError { + case noEntropyError(_ additionalDescription: String? = nil) + case keyDerivationError(_ additionalDescription: String? = nil) + case aesError(_ additionalDescription: String? = nil) + case invalidAccountError(_ additionalDescription: String? = nil) case invalidPasswordError - case encryptionError(String) + case encryptionError(_ additionalDescription: String? = nil) + + public var errorDescription: String? { + var errorMessage: [String?] + switch self { + case .noEntropyError(let additionalDescription): + errorMessage = ["Entropy error (e.g. failed to generate a random array of bytes).", additionalDescription] + case .keyDerivationError(let additionalDescription): + errorMessage = ["Key derivation error.", additionalDescription] + case .aesError(let additionalDescription): + errorMessage = ["AES error.", additionalDescription] + case .invalidAccountError(let additionalDescription): + errorMessage = ["Invalid account error.", additionalDescription] + case .invalidPasswordError: + errorMessage = ["Invalid password error."] + case .encryptionError(let additionalDescription): + errorMessage = ["Encryption error.", additionalDescription] + } + return errorMessage.compactMap { $0 }.joined(separator: " ") + } } diff --git a/Sources/Web3Core/KeystoreManager/BIP32Keystore.swift b/Sources/Web3Core/KeystoreManager/BIP32Keystore.swift index 22b305431..1646f3631 100755 --- a/Sources/Web3Core/KeystoreManager/BIP32Keystore.swift +++ b/Sources/Web3Core/KeystoreManager/BIP32Keystore.swift @@ -40,22 +40,22 @@ public class BIP32Keystore: AbstractKeystore { } public func UNSAFE_getPrivateKeyData(password: String, account: EthereumAddress) throws -> Data { - if let key = addressStorage.path(by: account) { - guard let decryptedRootNode = try? self.getPrefixNodeData(password) else {throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore")} - guard let rootNode = HDNode(decryptedRootNode) else {throw AbstractKeystoreError.encryptionError("Failed to deserialize a root node")} - guard rootNode.depth == (self.rootPrefix.components(separatedBy: "/").count - 1) else {throw AbstractKeystoreError.encryptionError("Derivation depth mismatch")} - guard let index = UInt32(key.components(separatedBy: "/").last!) else { - throw AbstractKeystoreError.encryptionError("Derivation depth mismatch") + if let path = addressStorage.path(by: account) { + guard let decryptedRootNode = try? self.getPrefixNodeData(password) else { throw AbstractKeystoreError.encryptionError("BIP32Keystore. Failed to decrypt a keystore") } + guard let rootNode = HDNode(decryptedRootNode) else { throw AbstractKeystoreError.encryptionError("BIP32Keystore. Failed to deserialize a root node") } + guard rootNode.depth == (rootPrefix.components(separatedBy: "/").count - 1) else {throw AbstractKeystoreError.encryptionError("BIP32Keystore. Derivation depth mismatch")} + guard let index = UInt32(path.components(separatedBy: "/").last!) else { + throw AbstractKeystoreError.encryptionError("BIP32Keystore. Derivation depth mismatch. `path` doesn't have an index (UInt32) as the last path component: \(path).") } guard let keyNode = rootNode.derive(index: index, derivePrivateKey: true) else { - throw AbstractKeystoreError.encryptionError("Derivation failed") + throw AbstractKeystoreError.encryptionError("BIP32Keystore. Derivation from rootNode failed. derive(index: \(index), derivePrivateKey: true)") } guard let privateKey = keyNode.privateKey else { - throw AbstractKeystoreError.invalidAccountError + throw AbstractKeystoreError.invalidAccountError("BIP32Keystore. Derived node doesn't have private key. derive(index: \(index), derivePrivateKey: true)") } return privateKey } - throw AbstractKeystoreError.invalidAccountError + throw AbstractKeystoreError.invalidAccountError("BIP32Keystore. Failed to find path for given address \(account.address).") } // -------------- @@ -89,7 +89,7 @@ public class BIP32Keystore: AbstractKeystore { public convenience init?(mnemonics: String, password: String, mnemonicsPassword: String = "", language: BIP39Language = BIP39Language.english, prefixPath: String = HDNode.defaultPathMetamaskPrefix, aesMode: String = "aes-128-cbc") throws { guard var seed = BIP39.seedFromMmemonics(mnemonics, password: mnemonicsPassword, language: language) else { - throw AbstractKeystoreError.noEntropyError + throw AbstractKeystoreError.noEntropyError("BIP32Keystore. Failed to generate seed from given mnemonics, password and language.") } defer { Data.zero(&seed) @@ -99,7 +99,7 @@ public class BIP32Keystore: AbstractKeystore { public convenience init?(mnemonicsPhrase: [String], password: String, mnemonicsPassword: String = "", language: BIP39Language = .english, prefixPath: String = HDNode.defaultPathMetamaskPrefix, aesMode: String = "aes-128-cbc") throws { guard var seed = BIP39.seedFromMmemonics(mnemonicsPhrase, password: mnemonicsPassword, language: language) else { - throw AbstractKeystoreError.noEntropyError + throw AbstractKeystoreError.noEntropyError("BIP32Keystore. Failed to generate seed from given mnemonics, password and language.") } defer { Data.zero(&seed) @@ -111,11 +111,11 @@ public class BIP32Keystore: AbstractKeystore { addressStorage = PathAddressStorage() guard let rootNode = HDNode(seed: seed)?.derive(path: prefixPath, derivePrivateKey: true) else { return nil } self.rootPrefix = prefixPath - try createNewAccount(parentNode: rootNode, password: password) + try createNewAccount(parentNode: rootNode) guard let serializedRootNode = rootNode.serialize(serializePublic: false) else { - throw AbstractKeystoreError.keyDerivationError + throw AbstractKeystoreError.keyDerivationError("BIP32Keystore. Failed to serialize root node.") } - try encryptDataToStorage(password, data: serializedRootNode, aesMode: aesMode) + try encryptDataToStorage(password, serializedNodeData: serializedRootNode, aesMode: aesMode) } public func createNewChildAccount(password: String) throws { @@ -129,14 +129,14 @@ public class BIP32Keystore: AbstractKeystore { guard rootNode.depth == prefixPath.components(separatedBy: "/").count - 1 else { throw AbstractKeystoreError.encryptionError("Derivation depth mismatch") } - try createNewAccount(parentNode: rootNode, password: password) + try createNewAccount(parentNode: rootNode) guard let serializedRootNode = rootNode.serialize(serializePublic: false) else { - throw AbstractKeystoreError.keyDerivationError + throw AbstractKeystoreError.keyDerivationError("BIP32Keystore. Failed to serialize root node.") } - try encryptDataToStorage(password, data: serializedRootNode, aesMode: self.keystoreParams!.crypto.cipher) + try encryptDataToStorage(password, serializedNodeData: serializedRootNode, aesMode: self.keystoreParams!.crypto.cipher) } - func createNewAccount(parentNode: HDNode, password: String = "web3swift") throws { + func createNewAccount(parentNode: HDNode) throws { let maxIndex = addressStorage.paths .compactMap { $0.components(separatedBy: "/").last } .compactMap { UInt32($0) } @@ -151,10 +151,10 @@ public class BIP32Keystore: AbstractKeystore { } guard let newNode = parentNode.derive(index: newIndex, derivePrivateKey: true, hardened: false) else { - throw AbstractKeystoreError.keyDerivationError + throw AbstractKeystoreError.keyDerivationError("BIP32Keystore. Failed to derive a new node. Check given parent node.") } guard let newAddress = Utilities.publicToAddress(newNode.publicKey) else { - throw AbstractKeystoreError.keyDerivationError + throw AbstractKeystoreError.keyDerivationError("BIP32Keystore. Failed to derive a public address from the new derived node.") } let newPath = rootPrefix + "/" + String(newNode.index) addressStorage.add(address: newAddress, for: newPath) @@ -163,10 +163,10 @@ public class BIP32Keystore: AbstractKeystore { public func createNewCustomChildAccount(password: String, path: String) throws { guard let decryptedRootNode = try getPrefixNodeData(password), let keystoreParams else { - throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore") + throw AbstractKeystoreError.encryptionError("BIP32Keystore. Failed to decrypt the keystore. Check given password.") } guard let rootNode = HDNode(decryptedRootNode) else { - throw AbstractKeystoreError.encryptionError("Failed to deserialize a root node") + throw AbstractKeystoreError.encryptionError("BIP32Keystore. Failed to deserialize the root node.") } let prefixPath = rootPrefix @@ -176,29 +176,29 @@ public class BIP32Keystore: AbstractKeystore { if let upperIndex = (path.range(of: prefixPath)?.upperBound), upperIndex < path.endIndex { pathAppendix = String(path[path.index(after: upperIndex).. [EthereumAddress] { - guard let decryptedRootNode = try? getPrefixNodeData(password), + guard let decryptedRootNode = try getPrefixNodeData(password), let rootNode = HDNode(decryptedRootNode) else { - throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore") + throw AbstractKeystoreError.encryptionError("BIP32Keystore. Failed to decrypt a keystore. Check given password.") } return try [UInt](0.. Data? { guard let keystorePars = keystoreParams else { return nil diff --git a/Sources/Web3Core/KeystoreManager/BIP39.swift b/Sources/Web3Core/KeystoreManager/BIP39.swift index e9965ef5d..ce0b3ee28 100755 --- a/Sources/Web3Core/KeystoreManager/BIP39.swift +++ b/Sources/Web3Core/KeystoreManager/BIP39.swift @@ -95,11 +95,13 @@ public class BIP39 { } private static func entropyOf(size: Int) throws -> Data { + let isCorrectSize = size >= 128 && size <= 256 && size.isMultiple(of: 32) + let randomBytesCount = size / 8 guard - size >= 128 && size <= 256 && size.isMultiple(of: 32), - let entropy = Data.randomBytes(length: size/8) + isCorrectSize, + let entropy = Data.randomBytes(length: randomBytesCount) else { - throw AbstractKeystoreError.noEntropyError + throw AbstractKeystoreError.noEntropyError("BIP39. \(!isCorrectSize ? "Requested entropy of wrong bits size \(size)." : "Failed to generated \(randomBytesCount) of random bytes.")") } return entropy } diff --git a/Sources/Web3Core/KeystoreManager/EthereumKeystoreV3.swift b/Sources/Web3Core/KeystoreManager/EthereumKeystoreV3.swift index d2602637c..733721be6 100755 --- a/Sources/Web3Core/KeystoreManager/EthereumKeystoreV3.swift +++ b/Sources/Web3Core/KeystoreManager/EthereumKeystoreV3.swift @@ -23,13 +23,13 @@ public class EthereumKeystoreV3: AbstractKeystore { } public func UNSAFE_getPrivateKeyData(password: String, account: EthereumAddress) throws -> Data { - if self.addresses?.count == 1 && account == self.addresses?.last { - guard let privateKey = try? self.getKeyData(password) else { + if account == addresses?.last { + guard let privateKey = try? getKeyData(password) else { throw AbstractKeystoreError.invalidPasswordError } return privateKey } - throw AbstractKeystoreError.invalidAccountError + throw AbstractKeystoreError.invalidAccountError("EthereumKeystoreV3. Cannot get private key: keystore doesn't contain information about given address \(account.address).") } // Class @@ -77,7 +77,7 @@ public class EthereumKeystoreV3: AbstractKeystore { defer { Data.zero(&newPrivateKey) } - try encryptDataToStorage(password, keyData: newPrivateKey, aesMode: aesMode) + try encryptDataToStorage(password, privateKey: newPrivateKey, aesMode: aesMode) } public init?(privateKey: Data, password: String, aesMode: String = "aes-128-cbc") throws { @@ -87,53 +87,46 @@ public class EthereumKeystoreV3: AbstractKeystore { guard SECP256K1.verifyPrivateKey(privateKey: privateKey) else { return nil } - try encryptDataToStorage(password, keyData: privateKey, aesMode: aesMode) + try encryptDataToStorage(password, privateKey: privateKey, aesMode: aesMode) } - fileprivate func encryptDataToStorage(_ password: String, keyData: Data?, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1, aesMode: String = "aes-128-cbc") throws { - if keyData == nil { - throw AbstractKeystoreError.encryptionError("Encryption without key data") + fileprivate func encryptDataToStorage(_ password: String, privateKey: Data, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1, aesMode: String = "aes-128-cbc") throws { + if privateKey.count != 32 { + throw AbstractKeystoreError.encryptionError("EthereumKeystoreV3. Attempted encryption with private key of length != 32. Given private key length is \(privateKey.count).") } let saltLen = 32 guard let saltData = Data.randomBytes(length: saltLen) else { - throw AbstractKeystoreError.noEntropyError + throw AbstractKeystoreError.noEntropyError("EthereumKeystoreV3. Failed to generate random bytes: `Data.randomBytes(length: \(saltLen))`.") } guard let derivedKey = scrypt(password: password, salt: saltData, length: dkLen, N: N, R: R, P: P) else { - throw AbstractKeystoreError.keyDerivationError + throw AbstractKeystoreError.keyDerivationError("EthereumKeystoreV3. Scrypt function failed.") } let last16bytes = Data(derivedKey[(derivedKey.count - 16)...(derivedKey.count - 1)]) let encryptionKey = Data(derivedKey[0...15]) guard let IV = Data.randomBytes(length: 16) else { - throw AbstractKeystoreError.noEntropyError + throw AbstractKeystoreError.noEntropyError("EthereumKeystoreV3. Failed to generate random bytes: `Data.randomBytes(length: 16)`.") } - var aesCipher: AES? - switch aesMode { + var aesCipher: AES + switch aesMode.lowercased() { case "aes-128-cbc": - aesCipher = try? AES(key: encryptionKey.bytes, blockMode: CBC(iv: IV.bytes), padding: .noPadding) + aesCipher = try AES(key: encryptionKey.bytes, blockMode: CBC(iv: IV.bytes), padding: .noPadding) case "aes-128-ctr": - aesCipher = try? AES(key: encryptionKey.bytes, blockMode: CTR(iv: IV.bytes), padding: .noPadding) + aesCipher = try AES(key: encryptionKey.bytes, blockMode: CTR(iv: IV.bytes), padding: .noPadding) default: - aesCipher = nil + throw AbstractKeystoreError.aesError("EthereumKeystoreV3. AES error: given AES mode can be one of 'aes-128-cbc' or 'aes-128-ctr'. Instead '\(aesMode)' was given.") } - if aesCipher == nil { - throw AbstractKeystoreError.aesError - } - guard let encryptedKey = try aesCipher?.encrypt(keyData!.bytes) else { - throw AbstractKeystoreError.aesError - } - let encryptedKeyData = Data(encryptedKey) - var dataForMAC = Data() - dataForMAC.append(last16bytes) - dataForMAC.append(encryptedKeyData) + + let encryptedKeyData = Data(try aesCipher.encrypt(privateKey.bytes)) + let dataForMAC = last16bytes + encryptedKeyData let mac = dataForMAC.sha3(.keccak256) let kdfparams = KdfParamsV3(salt: saltData.toHexString(), dklen: dkLen, n: N, p: P, r: R, c: nil, prf: nil) let cipherparams = CipherParamsV3(iv: IV.toHexString()) let crypto = CryptoParamsV3(ciphertext: encryptedKeyData.toHexString(), cipher: aesMode, cipherparams: cipherparams, kdf: "scrypt", kdfparams: kdfparams, mac: mac.toHexString(), version: nil) - guard let pubKey = Utilities.privateToPublic(keyData!) else { - throw AbstractKeystoreError.keyDerivationError + guard let publicKey = Utilities.privateToPublic(privateKey) else { + throw AbstractKeystoreError.keyDerivationError("EthereumKeystoreV3. Failed to derive public key from given private key. `Utilities.privateToPublic(privateKey)` returned `nil`.") } - guard let addr = Utilities.publicToAddress(pubKey) else { - throw AbstractKeystoreError.keyDerivationError + guard let addr = Utilities.publicToAddress(publicKey) else { + throw AbstractKeystoreError.keyDerivationError("EthereumKeystoreV3. Failed to derive address from derived public key. `Utilities.publicToAddress(publicKey)` returned `nil`.") } self.address = addr let keystoreparams = KeystoreParamsV3(address: addr.address.lowercased(), crypto: crypto, id: UUID().uuidString.lowercased(), version: 3) @@ -141,14 +134,13 @@ public class EthereumKeystoreV3: AbstractKeystore { } public func regenerate(oldPassword: String, newPassword: String, dkLen: Int = 32, N: Int = 4096, R: Int = 6, P: Int = 1) throws { - var keyData = try self.getKeyData(oldPassword) - if keyData == nil { - throw AbstractKeystoreError.encryptionError("Failed to decrypt a keystore") + guard var privateKey = try getKeyData(oldPassword) else { + throw AbstractKeystoreError.encryptionError("EthereumKeystoreV3. Failed to decrypt a keystore") } defer { - Data.zero(&keyData!) + Data.zero(&privateKey) } - try self.encryptDataToStorage(newPassword, keyData: keyData!, aesMode: self.keystoreParams!.crypto.cipher) + try self.encryptDataToStorage(newPassword, privateKey: privateKey, aesMode: self.keystoreParams!.crypto.cipher) } fileprivate func getKeyData(_ password: String) throws -> Data? { diff --git a/Sources/Web3Core/KeystoreManager/KeystoreManager.swift b/Sources/Web3Core/KeystoreManager/KeystoreManager.swift index db2cfe22b..b0eedd077 100755 --- a/Sources/Web3Core/KeystoreManager/KeystoreManager.swift +++ b/Sources/Web3Core/KeystoreManager/KeystoreManager.swift @@ -43,7 +43,7 @@ public class KeystoreManager: AbstractKeystore { public func UNSAFE_getPrivateKeyData(password: String, account: EthereumAddress) throws -> Data { guard let keystore = walletForAddress(account) else { - throw AbstractKeystoreError.invalidAccountError + throw AbstractKeystoreError.invalidAccountError("KeystoreManager: no keystore/wallet found for given address. Address `\(account.address)`.") } return try keystore.UNSAFE_getPrivateKeyData(password: password, account: account) } diff --git a/Sources/Web3Core/Transaction/CodableTransaction.swift b/Sources/Web3Core/Transaction/CodableTransaction.swift index 806e2a36f..1246e2714 100644 --- a/Sources/Web3Core/Transaction/CodableTransaction.swift +++ b/Sources/Web3Core/Transaction/CodableTransaction.swift @@ -156,7 +156,7 @@ public struct CodableTransaction { let result = self.attemptSignature(privateKey: privateKey, useExtraEntropy: useExtraEntropy) if result { return } } - throw AbstractKeystoreError.invalidAccountError + throw AbstractKeystoreError.invalidAccountError("Failed to sign transaction with given private key.") } // actual signing algorithm implementation diff --git a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift index 7189af1a6..6cf5854db 100644 --- a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -142,6 +142,24 @@ class EIP712TypedDataPayloadTests: XCTestCase { } } + func testEIP712ParserWithCustomTypeArrays() throws { + let problematicTypeExample = """ + {"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"OrderComponents":[{"name":"offerer","type":"address"},{"name":"zone","type":"address"},{"name":"offer","type":"OfferItem[]"},{"name":"consideration","type":"ConsiderationItem[]"},{"name":"orderType","type":"uint8"},{"name":"startTime","type":"uint256"},{"name":"endTime","type":"uint256"},{"name":"zoneHash","type":"bytes32"},{"name":"salt","type":"uint256"},{"name":"conduitKey","type":"bytes32"},{"name":"counter","type":"uint256"}],"OfferItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"}],"ConsiderationItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"},{"name":"recipient","type":"address"}]},"primaryType":"OrderComponents","domain":{"name":"Seaport","version":"1.5","chainId":"5","verifyingContract":"0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC"},"message":{"offerer":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC","offer":[{"itemType":"2","token":"0xE84a7676aAe742770A179dd7431073429a88c7B8","identifierOrCriteria":"44","startAmount":"1","endAmount":"1"}],"consideration":[{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"950000000000000000","endAmount":"950000000000000000","recipient":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0x0000a26b00c1F0DF003000390027140000fAa719"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0xbDEf201FB5BE36579b6B66971d40A6e162b92B80"}],"startTime":"1698665491","endTime":"1701343891","orderType":"0","zone":"0x004C00500000aD104D7DBd00e3ae0A5C00560C00","zoneHash":"0x0000000000000000000000000000000000000000000000000000000000000000","salt":"24446860302761739304752683030156737591518664810215442929808784621098726351597","conduitKey":"0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000","totalOriginalConsiderationItems":"3","counter":"0"}} + """ + XCTAssertNoThrow(try EIP712Parser.parse(problematicTypeExample)) + } + + func testEIP712SignHashWithCustomTypeArrays() throws { + let problematicTypeExample = """ + {"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"OrderComponents":[{"name":"offerer","type":"address"},{"name":"zone","type":"address"},{"name":"offer","type":"OfferItem[]"},{"name":"consideration","type":"ConsiderationItem[]"},{"name":"orderType","type":"uint8"},{"name":"startTime","type":"uint256"},{"name":"endTime","type":"uint256"},{"name":"zoneHash","type":"bytes32"},{"name":"salt","type":"uint256"},{"name":"conduitKey","type":"bytes32"},{"name":"counter","type":"uint256"}],"OfferItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"}],"ConsiderationItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"},{"name":"recipient","type":"address"}]},"primaryType":"OrderComponents","domain":{"name":"Seaport","version":"1.5","chainId":"5","verifyingContract":"0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC"},"message":{"offerer":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC","offer":[{"itemType":"2","token":"0xE84a7676aAe742770A179dd7431073429a88c7B8","identifierOrCriteria":"44","startAmount":"1","endAmount":"1"}],"consideration":[{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"950000000000000000","endAmount":"950000000000000000","recipient":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0x0000a26b00c1F0DF003000390027140000fAa719"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0xbDEf201FB5BE36579b6B66971d40A6e162b92B80"}],"startTime":"1698665491","endTime":"1701343891","orderType":"0","zone":"0x004C00500000aD104D7DBd00e3ae0A5C00560C00","zoneHash":"0x0000000000000000000000000000000000000000000000000000000000000000","salt":"24446860302761739304752683030156737591518664810215442929808784621098726351597","conduitKey":"0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000","totalOriginalConsiderationItems":"3","counter":"0"}} + """ + let eip712Payload = try EIP712Parser.parse(problematicTypeExample) + XCTAssertEqual(try eip712Payload.encodeType("OrderComponents"), "OrderComponents(address offerer,address zone,OfferItem[] offer,ConsiderationItem[] consideration,uint8 orderType,uint256 startTime,uint256 endTime,bytes32 zoneHash,uint256 salt,bytes32 conduitKey,uint256 counter)OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)") + XCTAssertEqual(try eip712Payload.encodeType("OfferItem"), "OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)") + XCTAssertEqual(try eip712Payload.encodeType("ConsiderationItem"), "ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)") + XCTAssertNoThrow(try eip712Payload.signHash()) + } + func testEIP712EncodeType() throws { let parsedEip712TypedData = try EIP712Parser.parse(EIP712TestData.testTypedDataPayload) try XCTAssertEqual(parsedEip712TypedData.encodeType("EIP712Domain"), "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") From 41828fbab824ce545ff9cda358ba29e5efbebeeb Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Fri, 3 Nov 2023 10:25:44 +0200 Subject: [PATCH 12/23] feat: support for eth_signTypedDataV4 payload parsing --- .../Utils/EIP/EIP712/EIP712Parser.swift | 151 ++++----- .../EIP712TypedDataPayloadTests.swift | 308 +++++++++++++----- 2 files changed, 287 insertions(+), 172 deletions(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index 6078392d2..90426ce8f 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -123,10 +123,31 @@ public struct EIP712TypeProperty: Codable { public let name: String /// Property type. A type that's ABI encodable. public let type: String + /// Strips brackets (e.g. [] - denoting an array) and other characters augmenting the type. + /// If ``type`` is an array of then ``coreType`` will return the type of the array. + public let coreType: String + + public let isArray: Bool public init(name: String, type: String) { - self.name = name - self.type = type + self.name = name.trimmingCharacters(in: .whitespacesAndNewlines) + self.type = type.trimmingCharacters(in: .whitespacesAndNewlines) + + var _coreType = self.type + if _coreType.hasSuffix("[]") { + _coreType.removeLast(2) + isArray = true + } else { + isArray = false + } + self.coreType = _coreType + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let name = try container.decode(String.self, forKey: .name) + let type = try container.decode(String.self, forKey: .type) + self.init(name: name, type: type) } } @@ -144,82 +165,9 @@ public struct EIP712TypedData { domain: [String : AnyObject], message: [String : AnyObject]) throws { self.types = types - self.primaryType = primaryType + self.primaryType = primaryType.trimmingCharacters(in: .whitespacesAndNewlines) self.domain = domain self.message = message - if let problematicType = hasCircularDependency() { - throw Web3Error.inputError(desc: "Created EIP712TypedData has a circular dependency amongst it's types. Cycle was first identified in '\(problematicType)'. Review it's uses in 'types'.") - } - } - - /// Checks for a circular dependency among the given types. - /// - /// If a circular dependency is detected, it returns the name of the type where the cycle was first identified. - /// Otherwise, it returns `nil`. - /// - /// - Returns: The type name where a circular dependency is detected, or `nil` if no circular dependency exists. - /// - Note: The function utilizes depth-first search to identify the circular dependencies. - func hasCircularDependency() -> String? { - - /// Generates an adjacency list for the given types, representing their dependencies. - /// - /// - Parameter types: A dictionary mapping type names to their property definitions. - /// - Returns: An adjacency list representing type dependencies. - func createAdjacencyList(types: [String: [EIP712TypeProperty]]) -> [String: [String]] { - var adjList: [String: [String]] = [:] - - for (typeName, fields) in types { - adjList[typeName] = [] - for field in fields { - if types.keys.contains(field.type) { - adjList[typeName]?.append(field.type) - } - } - } - - return adjList - } - - let adjList = createAdjacencyList(types: types) - - /// Depth-first search to check for circular dependencies. - /// - /// - Parameters: - /// - node: The current type being checked. - /// - visited: A dictionary keeping track of the visited types. - /// - stack: A dictionary used for checking the current path for cycles. - /// - /// - Returns: `true` if a cycle is detected from the current node, `false` otherwise. - func depthFirstSearch(node: String, visited: inout [String: Bool], stack: inout [String: Bool]) -> Bool { - visited[node] = true - stack[node] = true - - for neighbor in adjList[node] ?? [] { - if visited[neighbor] == nil { - if depthFirstSearch(node: neighbor, visited: &visited, stack: &stack) { - return true - } - } else if stack[neighbor] == true { - return true - } - } - - stack[node] = false - return false - } - - var visited: [String: Bool] = [:] - var stack: [String: Bool] = [:] - - for typeName in adjList.keys { - if visited[typeName] == nil { - if depthFirstSearch(node: typeName, visited: &visited, stack: &stack) { - return typeName - } - } - } - - return nil } public func encodeType(_ type: String) throws -> String { @@ -237,9 +185,11 @@ public struct EIP712TypedData { var typesCovered = typesCovered var encodedSubtypes: [String] = [] let parameters = try typeData.map { attributeType in - if let innerTypes = types[attributeType.type], !typesCovered.contains(attributeType.type) { - encodedSubtypes.append(try encodeType(attributeType.type, innerTypes)) - typesCovered.append(attributeType.type) + if let innerTypes = types[attributeType.coreType], !typesCovered.contains(attributeType.coreType) { + typesCovered.append(attributeType.coreType) + if attributeType.coreType != type { + encodedSubtypes.append(try encodeType(attributeType.coreType, innerTypes)) + } } return "\(attributeType.type) \(attributeType.name)" } @@ -261,9 +211,10 @@ public struct EIP712TypedData { 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).") } - // Add field contents - for field in typeData { - let value = data[field.name] + func encodeField(_ field: EIP712TypeProperty, + value: AnyObject?) throws -> (encTypes: [ABI.Element.ParameterType], encValues: [Any]) { + var encTypes: [ABI.Element.ParameterType] = [] + var encValues: [Any] = [] if field.type == "string" { guard let value = value as? String else { throw Web3Error.processingError(desc: "EIP712. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to String.") @@ -276,16 +227,44 @@ public struct EIP712TypedData { } encTypes.append(.bytes(length: 32)) encValues.append(value.sha3(.keccak256)) - } else if types[field.type] != nil { - guard let value = value as? [String : AnyObject] else { - 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].") + } else if field.isArray { + guard let values = value as? [AnyObject] else { + throw Web3Error.processingError(desc: "EIP712. Custom type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to [AnyObject].") } encTypes.append(.bytes(length: 32)) - encValues.append(try encodeData(field.type, data: value).sha3(.keccak256)) + let subField = EIP712TypeProperty(name: field.name, type: field.coreType) + var encodedSubTypes: [ABI.Element.ParameterType] = [] + var encodedSubValues: [Any] = [] + try values.forEach { value in + let encoded = try encodeField(subField, value: value) + encodedSubTypes.append(contentsOf: encoded.encTypes) + encodedSubValues.append(contentsOf: encoded.encValues) + } + + guard let encodedValue = ABIEncoder.encode(types: encodedSubTypes, values: encodedSubValues) else { + throw Web3Error.processingError(desc: "EIP712. Failed to encode an array of custom type. Field: '\(field)'; value: '\(String(describing: value))'.") + } + + encValues.append(encodedValue.sha3(.keccak256)) + } else if types[field.coreType] != nil { + encTypes.append(.bytes(length: 32)) + if let value = value as? [String : AnyObject] { + encValues.append(try encodeData(field.type, data: value).sha3(.keccak256)) + } else { + encValues.append(Data(count: 32)) + } } else { encTypes.append(try ABITypeParser.parseTypeString(field.type)) encValues.append(value as Any) } + return (encTypes, encValues) + } + + // Add field contents + for field in typeData { + let (_encTypes, _encValues) = try encodeField(field, value: data[field.name]) + encTypes.append(contentsOf: _encTypes) + encValues.append(contentsOf: _encValues) } guard let encodedData = ABIEncoder.encode(types: encTypes, values: encValues) else { diff --git a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift index 6cf5854db..6786c1c58 100644 --- a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -57,91 +57,6 @@ class EIP712TypedDataPayloadTests: XCTestCase { XCTAssertEqual(parsedEip712TypedData.message["contents"] as? String, "Hello, Bob!") } - func testEIP712CircularDependency() throws { - let problematicTypeExample = """ - { - "types":{ - "EIP712Domain":[ - { - "name":"name", - "type":"string" - }, - { - "name":"version", - "type":"string" - }, - { - "name":"chainId", - "type":"uint256" - }, - { - "name":"verifyingContract", - "type":"address" - } - ], - "Person":[ - { - "name":"name", - "type":"string" - }, - { - "name":"wallet", - "type":"address" - }, - { - "name":"mail", - "type":"Mail" - } - ], - "Mail":[ - { - "name":"from", - "type":"Person" - }, - { - "name":"to", - "type":"Person" - }, - { - "name":"contents", - "type":"string" - } - ] - }, - "primaryType":"Mail", - "domain":{ - "name":"Ether Mail", - "version":"1", - "chainId":1, - "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" - }, - "message":{ - "from":{ - "name":"Cow", - "wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" - }, - "to":{ - "name":"Bob", - "wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" - }, - "contents":"Hello, Bob!" - } - } - """ - XCTAssertThrowsError(try EIP712Parser.parse(problematicTypeExample)) { error in - guard let error = error as? Web3Error else { - XCTFail("Thrown error is not Web3Error.") - return - } - - if case let .inputError(desc) = error { - XCTAssertTrue(desc.hasPrefix("Created EIP712TypedData has a circular dependency amongst it's types.")) - } else { - XCTFail("A different Web3Error is thrown. Something changed?") - } - } - } - func testEIP712ParserWithCustomTypeArrays() throws { let problematicTypeExample = """ {"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"OrderComponents":[{"name":"offerer","type":"address"},{"name":"zone","type":"address"},{"name":"offer","type":"OfferItem[]"},{"name":"consideration","type":"ConsiderationItem[]"},{"name":"orderType","type":"uint8"},{"name":"startTime","type":"uint256"},{"name":"endTime","type":"uint256"},{"name":"zoneHash","type":"bytes32"},{"name":"salt","type":"uint256"},{"name":"conduitKey","type":"bytes32"},{"name":"counter","type":"uint256"}],"OfferItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"}],"ConsiderationItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"},{"name":"recipient","type":"address"}]},"primaryType":"OrderComponents","domain":{"name":"Seaport","version":"1.5","chainId":"5","verifyingContract":"0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC"},"message":{"offerer":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC","offer":[{"itemType":"2","token":"0xE84a7676aAe742770A179dd7431073429a88c7B8","identifierOrCriteria":"44","startAmount":"1","endAmount":"1"}],"consideration":[{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"950000000000000000","endAmount":"950000000000000000","recipient":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0x0000a26b00c1F0DF003000390027140000fAa719"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0xbDEf201FB5BE36579b6B66971d40A6e162b92B80"}],"startTime":"1698665491","endTime":"1701343891","orderType":"0","zone":"0x004C00500000aD104D7DBd00e3ae0A5C00560C00","zoneHash":"0x0000000000000000000000000000000000000000000000000000000000000000","salt":"24446860302761739304752683030156737591518664810215442929808784621098726351597","conduitKey":"0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000","totalOriginalConsiderationItems":"3","counter":"0"}} @@ -214,7 +129,7 @@ class EIP712TypedDataPayloadTests: XCTestCase { /// This signing doesn't use `"\u{19}Ethereum Signed Message:\n"`. As per EIP712 standard /// the following format is used instead: /// ``` - /// encode(domainSeparator : ๐”นยฒโตโถ, message : ๐•Š) = "\x19\x01" โ€– domainSeparator โ€– hashStruct(message) + /// encode(domainSeparator : ๐”นยฒโตโถ, message : ๐•Š) = "\x19\x01" โ€– domainSeparator โ€– structHash(message) /// ``` /// /// The output of ``EIP712TypedData.signHash`` is exactly that. @@ -224,4 +139,225 @@ class EIP712TypedDataPayloadTests: XCTestCase { XCTAssertEqual(unmarshalledSignature.r.toHexString(), "4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d") XCTAssertEqual(unmarshalledSignature.s.toHexString(), "07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b91562") } + + func testEIP712SignedTypedDataV4() throws { + // Payload includes recursive types, arrays and empty fields + let rawPayload = """ + { + "types":{ + "EIP712Domain":[ + { + "name":"name", + "type":"string" + }, + { + "name":"version", + "type":"string" + }, + { + "name":"chainId", + "type":"uint256" + }, + { + "name":"verifyingContract", + "type":"address" + } + ], + "Person":[ + { + "name":"name", + "type":"string" + }, + { + "name":"wallets", + "type":"address[]" + } + ], + "Mail":[ + { + "name":"from", + "type":"Person" + }, + { + "name":"to", + "type":"Person[]" + }, + { + "name":"contents", + "type":"string" + } + ], + "Group":[ + { + "name":"name", + "type":"string" + }, + { + "name":"members", + "type":"Person[]" + } + ] + }, + "domain":{ + "name":"Ether Mail", + "version":"1", + "chainId":1, + "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "primaryType":"Mail", + "message":{ + "from":{ + "name":"Cow", + "wallets":[ + "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", + "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF" + ] + }, + "to":[ + { + "name":"Bob", + "wallets":[ + "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", + "0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57", + "0xB0B0b0b0b0b0B000000000000000000000000000" + ] + } + ], + "contents":"Hello, Bob!" + } + } + """ + let parsedEip712TypedData = try EIP712Parser.parse(rawPayload) + XCTAssertEqual(try parsedEip712TypedData.encodeType("Group"), + "Group(string name,Person[] members)Person(string name,address[] wallets)") + XCTAssertEqual(try parsedEip712TypedData.encodeType("Person"), + "Person(string name,address[] wallets)") + XCTAssertEqual(try parsedEip712TypedData.typeHash("Person"), + "0xfabfe1ed996349fc6027709802be19d047da1aa5d6894ff5f6486d92db2e6860") + + XCTAssertEqual(try parsedEip712TypedData.encodeData("Person", data: parsedEip712TypedData.message["from"] as! [String : AnyObject]).toHexString(), + "fabfe1ed996349fc6027709802be19d047da1aa5d6894ff5f6486d92db2e68608c1d2bd5348394761719da11ec67eedae9502d137e8940fee8ecd6f641ee16488a8bfe642b9fc19c25ada5dadfd37487461dc81dd4b0778f262c163ed81b5e2a") + XCTAssertEqual(try parsedEip712TypedData.structHash("Person", data: parsedEip712TypedData.message["from"] as! [String : AnyObject]).toHexString(), + "9b4846dd48b866f0ac54d61b9b21a9e746f921cefa4ee94c4c0a1c49c774f67f") + + XCTAssertEqual(try parsedEip712TypedData.encodeData("Person", data: (parsedEip712TypedData.message["to"] as! [[String : AnyObject]])[0]).toHexString(), + "fabfe1ed996349fc6027709802be19d047da1aa5d6894ff5f6486d92db2e686028cac318a86c8a0a6a9156c2dba2c8c2363677ba0514ef616592d81557e679b6d2734f4c86cc3bd9cabf04c3097589d3165d95e4648fc72d943ed161f651ec6d") + XCTAssertEqual(try parsedEip712TypedData.structHash("Person", data: (parsedEip712TypedData.message["to"] as! [[String : AnyObject]])[0]).toHexString(), + "efa62530c7ae3a290f8a13a5fc20450bdb3a6af19d9d9d2542b5a94e631a9168") + + XCTAssertEqual(try parsedEip712TypedData.encodeType("Mail"), + "Mail(Person from,Person[] to,string contents)Person(string name,address[] wallets)") + XCTAssertEqual(try parsedEip712TypedData.typeHash("Mail"), + "0x4bd8a9a2b93427bb184aca81e24beb30ffa3c747e2a33d4225ec08bf12e2e753") + XCTAssertEqual(try parsedEip712TypedData.encodeData().toHexString(), + "4bd8a9a2b93427bb184aca81e24beb30ffa3c747e2a33d4225ec08bf12e2e7539b4846dd48b866f0ac54d61b9b21a9e746f921cefa4ee94c4c0a1c49c774f67fca322beec85be24e374d18d582a6f2997f75c54e7993ab5bc07404ce176ca7cdb5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8") + XCTAssertEqual(try parsedEip712TypedData.structHash().toHexString(), + "eb4221181ff3f1a83ea7313993ca9218496e424604ba9492bb4052c03d5c3df8") + XCTAssertEqual(try parsedEip712TypedData.structHash("EIP712Domain", data: parsedEip712TypedData.domain).toHexString(), + "f2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f") + XCTAssertEqual(try parsedEip712TypedData.signHash().toHexString(), + "a85c2e2b118698e88db68a8105b794a8cc7cec074e89ef991cb4f5f533819cc2") + + let privateKey = Data.fromHex("cow".sha3(.keccak256).addHexPrefix())! + let publicKey = Utilities.privateToPublic(privateKey)! + let address = Utilities.publicToAddress(publicKey)! + XCTAssertEqual(address, EthereumAddress("0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826")); + let (compressedSignature, _) = try SECP256K1.signForRecovery(hash: parsedEip712TypedData.signHash(), privateKey: privateKey) + XCTAssertEqual(compressedSignature!.toHexString(), "65cbd956f2fae28a601bebc9b906cea0191744bd4c4247bcd27cd08f8eb6b71c78efdf7a31dc9abee78f492292721f362d296cf86b4538e07b51303b67f749061b") + } + + func testEIP712SignedTypedDataV4_differentPayload() throws { + let rawPayload = + """ + { + "types":{ + "EIP712Domain":[ + { + "name":"name", + "type":"string" + }, + { + "name":"version", + "type":"string" + }, + { + "name":"chainId", + "type":"uint256" + }, + { + "name":"verifyingContract", + "type":"address" + } + ], + "Person":[ + { + "name":"name", + "type":"string" + }, + { + "name":"mother", + "type":"Person" + }, + { + "name":"father", + "type":"Person" + } + ] + }, + "domain":{ + "name":"Family Tree", + "version":"1", + "chainId":1, + "verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "primaryType":"Person", + "message":{ + "name":"Jon", + "mother":{ + "name":"Lyanna", + "father":{ + "name":"Rickard" + } + }, + "father":{ + "name":"Rhaegar", + "father":{ + "name":"Aeris II" + } + } + } + } + """ + + let parsedEip712TypedData = try EIP712Parser.parse(rawPayload) + + XCTAssertEqual(try parsedEip712TypedData.encodeType("Person"), "Person(string name,Person mother,Person father)") + XCTAssertEqual(try parsedEip712TypedData.typeHash("Person"), "0x7c5c8e90cb92c8da53b893b24962513be98afcf1b57b00327ae4cc14e3a64116") + + XCTAssertEqual(try parsedEip712TypedData.encodeData("Person", data: parsedEip712TypedData.message["mother"] as! [String : AnyObject]).toHexString(), + "7c5c8e90cb92c8da53b893b24962513be98afcf1b57b00327ae4cc14e3a64116afe4142a2b3e7b0503b44951e6030e0e2c5000ef83c61857e2e6003e7aef8570000000000000000000000000000000000000000000000000000000000000000088f14be0dd46a8ec608ccbff6d3923a8b4e95cdfc9648f0db6d92a99a264cb36") + XCTAssertEqual(try parsedEip712TypedData.structHash("Person", data: parsedEip712TypedData.message["mother"] as! [String : AnyObject]).toHexString(), + "9ebcfbf94f349de50bcb1e3aa4f1eb38824457c99914fefda27dcf9f99f6178b") + + XCTAssertEqual(try parsedEip712TypedData.encodeData("Person", data: parsedEip712TypedData.message["father"] as! [String : AnyObject]).toHexString(), + "7c5c8e90cb92c8da53b893b24962513be98afcf1b57b00327ae4cc14e3a64116b2a7c7faba769181e578a391a6a6811a3e84080c6a3770a0bf8a856dfa79d333000000000000000000000000000000000000000000000000000000000000000002cc7460f2c9ff107904cff671ec6fee57ba3dd7decf999fe9fe056f3fd4d56e") + XCTAssertEqual(try parsedEip712TypedData.structHash("Person", data: parsedEip712TypedData.message["father"] as! [String : AnyObject]).toHexString(), + "b852e5abfeff916a30cb940c4e24c43cfb5aeb0fa8318bdb10dd2ed15c8c70d8") + + XCTAssertEqual(try parsedEip712TypedData.encodeData(parsedEip712TypedData.primaryType, data: parsedEip712TypedData.message).toHexString(), + "7c5c8e90cb92c8da53b893b24962513be98afcf1b57b00327ae4cc14e3a64116e8d55aa98b6b411f04dbcf9b23f29247bb0e335a6bc5368220032fdcb9e5927f9ebcfbf94f349de50bcb1e3aa4f1eb38824457c99914fefda27dcf9f99f6178bb852e5abfeff916a30cb940c4e24c43cfb5aeb0fa8318bdb10dd2ed15c8c70d8") + XCTAssertEqual(try parsedEip712TypedData.structHash(parsedEip712TypedData.primaryType, data: parsedEip712TypedData.message).toHexString(), + "fdc7b6d35bbd81f7fa78708604f57569a10edff2ca329c8011373f0667821a45") + XCTAssertEqual(try parsedEip712TypedData.structHash("EIP712Domain", data: parsedEip712TypedData.domain).toHexString(), + "facb2c1888f63a780c84c216bd9a81b516fc501a19bae1fc81d82df590bbdc60") + XCTAssertEqual(try parsedEip712TypedData.signHash().toHexString(), + "807773b9faa9879d4971b43856c4d60c2da15c6f8c062bd9d33afefb756de19c") + + let privateKey = Data.fromHex("dragon".sha3(.keccak256).addHexPrefix())! + let publicKey = Utilities.privateToPublic(privateKey)! + let address = Utilities.publicToAddress(publicKey)! + XCTAssertEqual(address, EthereumAddress("0x065a687103c9f6467380bee800ecd70b17f6b72f")); + let (compressedSignature, _) = try SECP256K1.signForRecovery(hash: parsedEip712TypedData.signHash(), privateKey: privateKey) + XCTAssertEqual(compressedSignature!.toHexString(), "f2ec61e636ff7bb3ac8bc2a4cc2c8b8f635dd1b2ec8094c963128b358e79c85c5ca6dd637ed7e80f0436fe8fce39c0e5f2082c9517fe677cc2917dcd6c84ba881c") + } } From c6da90fb865e1f6c9a7fb851fbfb554918f0edf3 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Fri, 3 Nov 2023 16:45:55 +0200 Subject: [PATCH 13/23] chore: make parsing errors more granular --- .../Utils/EIP/EIP712/EIP712Parser.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index 90426ce8f..04d87495d 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -102,14 +102,18 @@ public class EIP712Parser { public static func parse(_ rawJson: Data) throws -> EIP712TypedData { let decoder = JSONDecoder() let types = try decoder.decode(EIP712TypeArray.self, from: rawJson).types - guard let json = try rawJson.asJsonDictionary(), - let primaryType = json["primaryType"] as? String, - let domain = json["domain"] as? [String : AnyObject], - let message = json["message"] as? [String : AnyObject] - else { - throw Web3Error.inputError(desc: "EIP712Parser: cannot decode EIP712TypedData object. Failed to parse one of primaryType, domain or message fields. Is any field missing?") + guard let json = try rawJson.asJsonDictionary() else { + throw Web3Error.inputError(desc: "EIP712Parser. Cannot decode given JSON as it cannot be represented as a Dictionary. Is it valid JSON?") + } + guard let primaryType = json["primaryType"] as? String else { + throw Web3Error.inputError(desc: "EIP712Parser. Top-level string field 'primaryType' missing.") + } + guard let domain = json["domain"] as? [String : AnyObject] else { + throw Web3Error.inputError(desc: "EIP712Parser. Top-level object field 'domain' missing.") + } + guard let message = json["message"] as? [String : AnyObject] else { + throw Web3Error.inputError(desc: "EIP712Parser. Top-level object field 'message' missing.") } - return try EIP712TypedData(types: types, primaryType: primaryType, domain: domain, message: message) } } From e40c3f8e23139382953637dc1746038160e8e960 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Fri, 3 Nov 2023 16:46:40 +0200 Subject: [PATCH 14/23] fix: order custom subtypes (no primitives!) in alphabetical order when encoding --- Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index 04d87495d..154b744d2 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -187,17 +187,21 @@ public struct EIP712TypedData { internal func encodeType(_ type: String, _ typeData: [EIP712TypeProperty], typesCovered: [String] = []) throws -> String { var typesCovered = typesCovered - var encodedSubtypes: [String] = [] + var encodedSubtypes: [String : String] = [:] let parameters = try typeData.map { attributeType in if let innerTypes = types[attributeType.coreType], !typesCovered.contains(attributeType.coreType) { typesCovered.append(attributeType.coreType) if attributeType.coreType != type { - encodedSubtypes.append(try encodeType(attributeType.coreType, innerTypes)) + encodedSubtypes[attributeType.coreType] = try encodeType(attributeType.coreType, innerTypes) } } return "\(attributeType.type) \(attributeType.name)" } - return type + "(" + parameters.joined(separator: ",") + ")" + encodedSubtypes.joined(separator: "") + return type + "(" + parameters.joined(separator: ",") + ")" + encodedSubtypes.sorted { lhs, rhs in + return lhs.key < rhs.key + } + .map { $0.value } + .joined(separator: "") } /// Convenience function for ``encodeData(_:data:)`` that uses ``primaryType`` and ``message`` as values. From 499008446fcf9eaecd43868f6333afd9bf4d46d7 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Fri, 3 Nov 2023 16:47:10 +0200 Subject: [PATCH 15/23] chore: added new test for Invalid Order Signature error --- .../EIP712TypedDataPayloadTests.swift | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift index 6786c1c58..89a2f5cd1 100644 --- a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -360,4 +360,103 @@ class EIP712TypedDataPayloadTests: XCTestCase { let (compressedSignature, _) = try SECP256K1.signForRecovery(hash: parsedEip712TypedData.signHash(), privateKey: privateKey) XCTAssertEqual(compressedSignature!.toHexString(), "f2ec61e636ff7bb3ac8bc2a4cc2c8b8f635dd1b2ec8094c963128b358e79c85c5ca6dd637ed7e80f0436fe8fce39c0e5f2082c9517fe677cc2917dcd6c84ba881c") } + + /// This test makes sure that custom types are alphabetically ordered when encoded + /// This test is built on thje following example: https://github.com/trustwallet/wallet-core/pull/2325/files + /// Link to the GitHub issue https://github.com/trustwallet/wallet-core/issues/2323 + /// > According to the description of the issues it fixes (see the link above): + /// > The type string is different from `metamask/eth-sig-util` + /// > `type: OrderComponents(...)OfferItem(...)ConsiderationItem(...)` + /// > `ConsiderationItem` should be in front of `OfferItem` + func testEIP712OpenseaInvalidOrderSignature() throws { + let rawPayload = """ + { + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "OrderComponents": [ + { "name": "offerer", "type": "address" }, + { "name": "zone", "type": "address" }, + { "name": "offer", "type": "OfferItem[]" }, + { "name": "consideration", "type": "ConsiderationItem[]" }, + { "name": "orderType", "type": "uint8" }, + { "name": "startTime", "type": "uint256" }, + { "name": "endTime", "type": "uint256" }, + { "name": "zoneHash", "type": "bytes32" }, + { "name": "salt", "type": "uint256" }, + { "name": "conduitKey", "type": "bytes32" }, + { "name": "counter", "type": "uint256" } + ], + "OfferItem": [ + { "name": "itemType", "type": "uint8" }, + { "name": "token", "type": "address" }, + { "name": "identifierOrCriteria", "type": "uint256" }, + { "name": "startAmount", "type": "uint256" }, + { "name": "endAmount", "type": "uint256" } + ], + "ConsiderationItem": [ + { "name": "itemType", "type": "uint8" }, + { "name": "token", "type": "address" }, + { "name": "identifierOrCriteria", "type": "uint256" }, + { "name": "startAmount", "type": "uint256" }, + { "name": "endAmount", "type": "uint256" }, + { "name": "recipient", "type": "address" } + ] + }, + "primaryType": "OrderComponents", + "domain": { + "name": "Seaport", + "version": "1.1", + "chainId": "1", + "verifyingContract": "0x00000000006c3852cbEf3e08E8dF289169EdE581" + }, + "message": { + "offerer": "0x7d8bf18C7cE84b3E175b339c4Ca93aEd1dD166F1", + "offer": [ + { + "itemType": "2", + "token": "0x3F53082981815Ed8142384EDB1311025cA750Ef1", + "identifierOrCriteria": "134", + "startAmount": "1", + "endAmount": "1" + } + ], + "orderType": "2", + "consideration": [ + { + "itemType": "0", + "token": "0x0000000000000000000000000000000000000000", + "identifierOrCriteria": "0", + "startAmount": "975000000000000000", + "endAmount": "975000000000000000", + "recipient": "0x7d8bf18C7cE84b3E175b339c4Ca93aEd1dD166F1" + }, + { + "itemType": "0", + "token": "0x0000000000000000000000000000000000000000", + "identifierOrCriteria": "0", + "startAmount": "25000000000000000", + "endAmount": "25000000000000000", + "recipient": "0x8De9C5A032463C561423387a9648c5C7BCC5BC90" + } + ], + "startTime": "1655450129", + "endTime": "1658042129", + "zone": "0x004C00500000aD104D7DBd00e3ae0A5C00560C00", + "zoneHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "salt": "795459960395409", + "conduitKey": "0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000", + "totalOriginalConsiderationItems": "2", + "counter": "0" + } + } + """ + + let parsedPayload = try EIP712Parser.parse(rawPayload) + try XCTAssertEqual(parsedPayload.signHash().toHexString(), "54140d99a864932cbc40fd8a2d1d1706c3923a79c183a3b151e929ac468064db") + } } From d69d594a2ab6aac258eed1fe1a4468dcae495fa8 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Fri, 3 Nov 2023 23:44:47 +0200 Subject: [PATCH 16/23] fix: updated test case to match the expected result --- .../localTests/EIP712TypedDataPayloadTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift index 89a2f5cd1..1a31b2b8d 100644 --- a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -69,7 +69,7 @@ class EIP712TypedDataPayloadTests: XCTestCase { {"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"OrderComponents":[{"name":"offerer","type":"address"},{"name":"zone","type":"address"},{"name":"offer","type":"OfferItem[]"},{"name":"consideration","type":"ConsiderationItem[]"},{"name":"orderType","type":"uint8"},{"name":"startTime","type":"uint256"},{"name":"endTime","type":"uint256"},{"name":"zoneHash","type":"bytes32"},{"name":"salt","type":"uint256"},{"name":"conduitKey","type":"bytes32"},{"name":"counter","type":"uint256"}],"OfferItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"}],"ConsiderationItem":[{"name":"itemType","type":"uint8"},{"name":"token","type":"address"},{"name":"identifierOrCriteria","type":"uint256"},{"name":"startAmount","type":"uint256"},{"name":"endAmount","type":"uint256"},{"name":"recipient","type":"address"}]},"primaryType":"OrderComponents","domain":{"name":"Seaport","version":"1.5","chainId":"5","verifyingContract":"0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC"},"message":{"offerer":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC","offer":[{"itemType":"2","token":"0xE84a7676aAe742770A179dd7431073429a88c7B8","identifierOrCriteria":"44","startAmount":"1","endAmount":"1"}],"consideration":[{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"950000000000000000","endAmount":"950000000000000000","recipient":"0xD0727E8a578DE9Dd19BcED635B1aa43576E638bC"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0x0000a26b00c1F0DF003000390027140000fAa719"},{"itemType":"0","token":"0x0000000000000000000000000000000000000000","identifierOrCriteria":"0","startAmount":"25000000000000000","endAmount":"25000000000000000","recipient":"0xbDEf201FB5BE36579b6B66971d40A6e162b92B80"}],"startTime":"1698665491","endTime":"1701343891","orderType":"0","zone":"0x004C00500000aD104D7DBd00e3ae0A5C00560C00","zoneHash":"0x0000000000000000000000000000000000000000000000000000000000000000","salt":"24446860302761739304752683030156737591518664810215442929808784621098726351597","conduitKey":"0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000","totalOriginalConsiderationItems":"3","counter":"0"}} """ let eip712Payload = try EIP712Parser.parse(problematicTypeExample) - XCTAssertEqual(try eip712Payload.encodeType("OrderComponents"), "OrderComponents(address offerer,address zone,OfferItem[] offer,ConsiderationItem[] consideration,uint8 orderType,uint256 startTime,uint256 endTime,bytes32 zoneHash,uint256 salt,bytes32 conduitKey,uint256 counter)OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)") + XCTAssertEqual(try eip712Payload.encodeType("OrderComponents"), "OrderComponents(address offerer,address zone,OfferItem[] offer,ConsiderationItem[] consideration,uint8 orderType,uint256 startTime,uint256 endTime,bytes32 zoneHash,uint256 salt,bytes32 conduitKey,uint256 counter)ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)") XCTAssertEqual(try eip712Payload.encodeType("OfferItem"), "OfferItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount)") XCTAssertEqual(try eip712Payload.encodeType("ConsiderationItem"), "ConsiderationItem(uint8 itemType,address token,uint256 identifierOrCriteria,uint256 startAmount,uint256 endAmount,address recipient)") XCTAssertNoThrow(try eip712Payload.signHash()) @@ -368,7 +368,9 @@ class EIP712TypedDataPayloadTests: XCTestCase { /// > The type string is different from `metamask/eth-sig-util` /// > `type: OrderComponents(...)OfferItem(...)ConsiderationItem(...)` /// > `ConsiderationItem` should be in front of `OfferItem` - func testEIP712OpenseaInvalidOrderSignature() throws { + /// + /// The `InvalidOrderSignature` error is thrown when hash created for signing is invalid, thus, resulting in invalid signature. + func testEIP712NoInvalidOrderSignature() throws { let rawPayload = """ { "types": { From 40a9a3d9cbf8aacc73219a0342565bef3bbb870d Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu <36865532+JeneaVranceanu@users.noreply.github.com> Date: Fri, 3 Nov 2023 23:54:36 +0200 Subject: [PATCH 17/23] chore: updated error message for entropyOf --- Sources/Web3Core/KeystoreManager/BIP39.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Web3Core/KeystoreManager/BIP39.swift b/Sources/Web3Core/KeystoreManager/BIP39.swift index ce0b3ee28..7e7314316 100755 --- a/Sources/Web3Core/KeystoreManager/BIP39.swift +++ b/Sources/Web3Core/KeystoreManager/BIP39.swift @@ -101,7 +101,7 @@ public class BIP39 { isCorrectSize, let entropy = Data.randomBytes(length: randomBytesCount) else { - throw AbstractKeystoreError.noEntropyError("BIP39. \(!isCorrectSize ? "Requested entropy of wrong bits size \(size)." : "Failed to generated \(randomBytesCount) of random bytes.")") + throw AbstractKeystoreError.noEntropyError("BIP39. \(!isCorrectSize ? "Requested entropy of wrong bits size: \(size). Expected: 128 <= size <= 256, size % 32 == 0." : "Failed to generate \(randomBytesCount) of random bytes.")") } return entropy } From cfd8ee948649c7d4ea84c87e751920d0a5917390 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Sat, 4 Nov 2023 00:01:59 +0200 Subject: [PATCH 18/23] chore: updated error messages for EIP712Parser --- .../Utils/EIP/EIP712/EIP712Parser.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index 154b744d2..c95f2b6e0 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -90,7 +90,7 @@ public class EIP712Parser { static func toData(_ json: String) throws -> Data { guard let json = json.data(using: .utf8) else { - throw Web3Error.inputError(desc: "Failed to parse EIP712 payload. Given string is not valid UTF8 string. \(json)") + throw Web3Error.inputError(desc: "EIP712Parser. Failed to parse EIP712 payload. Given string is not valid UTF8 string. \(json)") } return json } @@ -176,7 +176,7 @@ public struct EIP712TypedData { public func encodeType(_ type: String) throws -> String { guard let typeData = types[type] else { - throw Web3Error.processingError(desc: "EIP712. Attempting to encode type that doesn't exist in this payload. Given type: \(type). Available types: \(types.values).") + throw Web3Error.processingError(desc: "EIP712Parser. Attempting to encode type that doesn't exist in this payload. Given type: \(type). Available types: \(types.values).") } return try encodeType(type, typeData) } @@ -216,7 +216,7 @@ public struct EIP712TypedData { var encValues: [Any] = [try typeHash(type)] guard let typeData = types[type] else { - 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).") + throw Web3Error.processingError(desc: "EIP712Parser. Attempting to encode data for type that doesn't exist in this payload. Given type: \(type). Available types: \(types.values).") } func encodeField(_ field: EIP712TypeProperty, @@ -225,19 +225,19 @@ public struct EIP712TypedData { var encValues: [Any] = [] if field.type == "string" { guard let value = value as? String else { - throw Web3Error.processingError(desc: "EIP712. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to String.") + throw Web3Error.processingError(desc: "EIP712Parser. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to String.") } encTypes.append(.bytes(length: 32)) encValues.append(value.sha3(.keccak256).addHexPrefix()) } else if field.type == "bytes"{ guard let value = value as? Data else { - throw Web3Error.processingError(desc: "EIP712. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to Data.") + throw Web3Error.processingError(desc: "EIP712Parser. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to Data.") } encTypes.append(.bytes(length: 32)) encValues.append(value.sha3(.keccak256)) } else if field.isArray { guard let values = value as? [AnyObject] else { - throw Web3Error.processingError(desc: "EIP712. Custom type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to [AnyObject].") + throw Web3Error.processingError(desc: "EIP712Parser. Custom type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to [AnyObject].") } encTypes.append(.bytes(length: 32)) let subField = EIP712TypeProperty(name: field.name, type: field.coreType) @@ -250,7 +250,7 @@ public struct EIP712TypedData { } guard let encodedValue = ABIEncoder.encode(types: encodedSubTypes, values: encodedSubValues) else { - throw Web3Error.processingError(desc: "EIP712. Failed to encode an array of custom type. Field: '\(field)'; value: '\(String(describing: value))'.") + throw Web3Error.processingError(desc: "EIP712Parser. Failed to encode an array of custom type. Field: '\(field)'; value: '\(String(describing: value))'.") } encValues.append(encodedValue.sha3(.keccak256)) @@ -276,7 +276,7 @@ public struct EIP712TypedData { } guard let encodedData = ABIEncoder.encode(types: encTypes, values: encValues) else { - throw Web3Error.processingError(desc: "EIP712. ABIEncoder.encode failed with the following types and values: \(encTypes); \(encValues)") + throw Web3Error.processingError(desc: "EIP712Parser. ABIEncoder.encode failed with the following types and values: \(encTypes); \(encValues)") } return encodedData } From 0b0b3fae4f9b16de708c98b3f6e25d77f34a3a86 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Sat, 4 Nov 2023 00:04:21 +0200 Subject: [PATCH 19/23] chore: error message fix --- Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index c95f2b6e0..42934c140 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -176,7 +176,7 @@ public struct EIP712TypedData { public func encodeType(_ type: String) throws -> String { guard let typeData = types[type] else { - throw Web3Error.processingError(desc: "EIP712Parser. Attempting to encode type that doesn't exist in this payload. Given type: \(type). Available types: \(types.values).") + throw Web3Error.processingError(desc: "EIP712Parser. Attempting to encode type that doesn't exist in this payload. Given type: \(type). Available types: \(types.keys).") } return try encodeType(type, typeData) } @@ -216,7 +216,7 @@ public struct EIP712TypedData { var encValues: [Any] = [try typeHash(type)] guard let typeData = types[type] else { - throw Web3Error.processingError(desc: "EIP712Parser. Attempting to encode data for type that doesn't exist in this payload. Given type: \(type). Available types: \(types.values).") + throw Web3Error.processingError(desc: "EIP712Parser. Attempting to encode data for type that doesn't exist in this payload. Given type: \(type). Available types: \(types.keys).") } func encodeField(_ field: EIP712TypeProperty, From 68d845700ebd56403fb09a6c88c429383cb5b533 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu <36865532+JeneaVranceanu@users.noreply.github.com> Date: Sun, 5 Nov 2023 01:23:33 +0200 Subject: [PATCH 20/23] chore: updated error message --- Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index 42934c140..68d9b3a95 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -90,7 +90,7 @@ public class EIP712Parser { static func toData(_ json: String) throws -> Data { guard let json = json.data(using: .utf8) else { - throw Web3Error.inputError(desc: "EIP712Parser. Failed to parse EIP712 payload. Given string is not valid UTF8 string. \(json)") + throw Web3Error.inputError(desc: "EIP712Parser. Failed to parse EIP712 payload. Given string is not valid UTF8 string.") } return json } From 111d33e62c461a30cbd08bcf0f54e9780a73320b Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu <36865532+JeneaVranceanu@users.noreply.github.com> Date: Sun, 5 Nov 2023 01:26:58 +0200 Subject: [PATCH 21/23] chore: docs update --- Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index 68d9b3a95..2f7e77e6a 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -125,9 +125,9 @@ internal struct EIP712TypeArray: Codable { public struct EIP712TypeProperty: Codable { /// Property name. An arbitrary string. public let name: String - /// Property type. A type that's ABI encodable. + /// Property type. A type that's ABI encodable or a custom type from ``EIP712TypedData/types``. public let type: String - /// Strips brackets (e.g. [] - denoting an array) and other characters augmenting the type. + /// Stripped of brackets ([] - denoting an array). /// If ``type`` is an array of then ``coreType`` will return the type of the array. public let coreType: String From e77af0ab310ed73d6203c1f26a6124158c8e007d Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Tue, 7 Nov 2023 10:48:32 +0200 Subject: [PATCH 22/23] fix: EIP712 - change type to AbstractKeystore from BIP32Keystore --- Sources/web3swift/Web3/Web3+Signing.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/web3swift/Web3/Web3+Signing.swift b/Sources/web3swift/Web3/Web3+Signing.swift index 295d1a497..e3d50cb64 100755 --- a/Sources/web3swift/Web3/Web3+Signing.swift +++ b/Sources/web3swift/Web3/Web3+Signing.swift @@ -41,7 +41,7 @@ public struct Web3Signer { } public static func signEIP712(_ eip712TypedDataPayload: EIP712TypedData, - keystore: BIP32Keystore, + keystore: AbstractKeystore, account: EthereumAddress, password: String? = nil) throws -> Data { let hash = try eip712TypedDataPayload.signHash() @@ -57,7 +57,7 @@ public struct Web3Signer { } public static func signEIP712(_ eip712Hashable: EIP712Hashable, - keystore: BIP32Keystore, + keystore: AbstractKeystore, verifyingContract: EthereumAddress, account: EthereumAddress, password: String? = nil, From 6cc06db50613a805d2c59db4ceb766bd9c6a0cc3 Mon Sep 17 00:00:00 2001 From: Jenea Vranceanu Date: Tue, 21 Nov 2023 01:33:36 +0200 Subject: [PATCH 23/23] fix: parsing and encoding of "bytes" --- .../Utils/EIP/EIP712/EIP712Parser.swift | 17 ++- .../EIP712TypedDataPayloadTests.swift | 129 ++++++++++++++++++ 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift index 2f7e77e6a..ad0f80561 100644 --- a/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -225,19 +225,26 @@ public struct EIP712TypedData { var encValues: [Any] = [] if field.type == "string" { guard let value = value as? String else { - throw Web3Error.processingError(desc: "EIP712Parser. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to String.") + throw Web3Error.processingError(desc: "EIP712Parser. Type metadata of '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to String. Parent object type: \(type).") } encTypes.append(.bytes(length: 32)) encValues.append(value.sha3(.keccak256).addHexPrefix()) } else if field.type == "bytes"{ - guard let value = value as? Data else { - throw Web3Error.processingError(desc: "EIP712Parser. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to Data.") + let _value: Data? + if let value = value as? String, + let data = Data.fromHex(value) { + _value = data + } else { + _value = value as? Data + } + guard let value = _value else { + throw Web3Error.processingError(desc: "EIP712Parser. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast/parse value to Data. Parent object type: \(type).") } encTypes.append(.bytes(length: 32)) encValues.append(value.sha3(.keccak256)) } else if field.isArray { guard let values = value as? [AnyObject] else { - throw Web3Error.processingError(desc: "EIP712Parser. Custom type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to [AnyObject].") + throw Web3Error.processingError(desc: "EIP712Parser. Custom type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to [AnyObject]. Parent object type: \(type)") } encTypes.append(.bytes(length: 32)) let subField = EIP712TypeProperty(name: field.name, type: field.coreType) @@ -250,7 +257,7 @@ public struct EIP712TypedData { } guard let encodedValue = ABIEncoder.encode(types: encodedSubTypes, values: encodedSubValues) else { - throw Web3Error.processingError(desc: "EIP712Parser. Failed to encode an array of custom type. Field: '\(field)'; value: '\(String(describing: value))'.") + throw Web3Error.processingError(desc: "EIP712Parser. Failed to encode an array of custom type. Field: '\(field)'; value: '\(String(describing: value))'. Parent object type: \(type)") } encValues.append(encodedValue.sha3(.keccak256)) diff --git a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift index 1a31b2b8d..e188370f8 100644 --- a/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -461,4 +461,133 @@ class EIP712TypedDataPayloadTests: XCTestCase { let parsedPayload = try EIP712Parser.parse(rawPayload) try XCTAssertEqual(parsedPayload.signHash().toHexString(), "54140d99a864932cbc40fd8a2d1d1706c3923a79c183a3b151e929ac468064db") } + + /// A test to check payload encoding, specifically parsing and encoding of fields with "bytes" type. + /// Given raw payload was failing with the following error: + /// ``` + /// EIP712Parser. + /// Type metadata 'EIP712TypeProperty(name: "data", type: "bytes", coreType: "bytes", isArray: false)' + /// and actual value + /// 'Optional(0x000000000000000000000000e84a7676aae742770a179dd7431073429a88c7b8000000000000000000000000000000000000000000000000000000000000002c)' + /// type doesn't match. + /// Cannot cast value to Data. + /// + /// ``` + func testEIP712BytesEncoding() throws { + let rawPayload = """ + { + "message":{ + "takeAsset":{ + "assetType":{ + "assetClass":"0xaaaebeba", + "data":"0x" + }, + "value":"2000000000000000000" + }, + "data":"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d6ffd79b52a587a0a9941a61f4e6cb0d386d54580000000000000000000000000000000000000000000000000000000000000064", + "dataType":"0x23d235ef", + "maker":"0xd0727e8a578de9dd19bced635b1aa43576e638bc", + "taker":"0x0000000000000000000000000000000000000000", + "salt":"0x8f9761e56ed73b34d0cb184a2c5530d86c355c63c1cde8db1e0d2557d93f10d7", + "end":1703058225, + "makeAsset":{ + "value":"1", + "assetType":{ + "data":"0x000000000000000000000000e84a7676aae742770a179dd7431073429a88c7b8000000000000000000000000000000000000000000000000000000000000002c", + "assetClass":"0x73ad2146" + } + }, + "start":0 + }, + "domain":{ + "verifyingContract":"0x02afbd43cad367fcb71305a2dfb9a3928218f0c1", + "version":"2", + "chainId":5, + "name":"Exchange" + }, + "primaryType":"Order", + "types":{ + "Order":[ + { + "type":"address", + "name":"maker" + }, + { + "type":"Asset", + "name":"makeAsset" + }, + { + "name":"taker", + "type":"address" + }, + { + "name":"takeAsset", + "type":"Asset" + }, + { + "name":"salt", + "type":"uint256" + }, + { + "name":"start", + "type":"uint256" + }, + { + "type":"uint256", + "name":"end" + }, + { + "type":"bytes4", + "name":"dataType" + }, + { + "type":"bytes", + "name":"data" + } + ], + "EIP712Domain":[ + { + "name":"name", + "type":"string" + }, + { + "type":"string", + "name":"version" + }, + { + "name":"chainId", + "type":"uint256" + }, + { + "name":"verifyingContract", + "type":"address" + } + ], + "Asset":[ + { + "name":"assetType", + "type":"AssetType" + }, + { + "type":"uint256", + "name":"value" + } + ], + "AssetType":[ + { + "type":"bytes4", + "name":"assetClass" + }, + { + "name":"data", + "type":"bytes" + } + ] + } + } + """ + + let parsedPayload = try EIP712Parser.parse(rawPayload) + try XCTAssertEqual(parsedPayload.signHash().toHexString(), "95625b9843950aa6cdd50c703e2bf0bdaa5ddeef9842d5839a81d927b7159637") + } }