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..7e7314316 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). Expected: 128 <= size <= 256, size % 32 == 0." : "Failed to generate \(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/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 83% rename from Sources/web3swift/Utils/EIP/EIP712.swift rename to Sources/web3swift/Utils/EIP/EIP712/EIP712.swift index e3a4bcb87..21458618b 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,6 +60,8 @@ 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: result = try hashable.hash() default: @@ -64,16 +72,19 @@ 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(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..ad0f80561 --- /dev/null +++ b/Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift @@ -0,0 +1,304 @@ +// +// 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 ready for signing. +/// 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!" +/// } +/// } +/// ``` +/// +/// 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: "EIP712Parser. Failed to parse EIP712 payload. Given string is not valid UTF8 string.") + } + 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() 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) + } +} + +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 or a custom type from ``EIP712TypedData/types``. + public let type: String + /// 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 + + public let isArray: Bool + + public init(name: String, type: String) { + 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) + } +} + +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]) throws { + self.types = types + self.primaryType = primaryType.trimmingCharacters(in: .whitespacesAndNewlines) + self.domain = domain + self.message = message + } + + 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.keys).") + } + 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 : 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[attributeType.coreType] = try encodeType(attributeType.coreType, innerTypes) + } + } + return "\(attributeType.type) \(attributeType.name)" + } + 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. + /// - 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: "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, + 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: "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"{ + 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]. Parent object type: \(type)") + } + encTypes.append(.bytes(length: 32)) + 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: "EIP712Parser. Failed to encode an array of custom type. Field: '\(field)'; value: '\(String(describing: value))'. Parent object type: \(type)") + } + + 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 { + throw Web3Error.processingError(desc: "EIP712Parser. 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/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+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. diff --git a/Sources/web3swift/Web3/Web3+Signing.swift b/Sources/web3swift/Web3/Web3+Signing.swift index 46fe01ce5..e3d50cb64 100755 --- a/Sources/web3swift/Web3/Web3+Signing.swift +++ b/Sources/web3swift/Web3/Web3+Signing.swift @@ -40,19 +40,36 @@ public struct Web3Signer { return compressedSignature } + public static func signEIP712(_ eip712TypedDataPayload: EIP712TypedData, + keystore: AbstractKeystore, + account: EthereumAddress, + password: String? = nil) throws -> Data { + let hash = try eip712TypedDataPayload.signHash() + guard let signature = try Web3Signer.signPersonalMessage(hash, + keystore: keystore, + account: account, + password: password ?? "", + useHash: false) + else { + throw Web3Error.dataError + } + return signature + } + public static func signEIP712(_ eip712Hashable: EIP712Hashable, - keystore: BIP32Keystore, + keystore: AbstractKeystore, verifyingContract: EthereumAddress, account: EthereumAddress, password: String? = nil, 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, - password: password ?? "") + password: password ?? "", + useHash: false) else { throw Web3Error.dataError } 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 3f527d2cf..3473dbf2b 100644 --- a/Tests/web3swiftTests/localTests/EIP712Tests.swift +++ b/Tests/web3swiftTests/localTests/EIP712Tests.swift @@ -2,7 +2,8 @@ import XCTest import Web3Core @testable import web3swift -class EIP712Tests: LocalTestCase { +class EIP712Tests: XCTestCase { + func testWithoutChainId() throws { let to = EthereumAddress("0x3F06bAAdA68bB997daB03d91DBD0B73e196c5A4d")! let value = EIP712.UInt256(0) @@ -51,7 +52,7 @@ class EIP712Tests: LocalTestCase { account: account!, password: password, chainId: chainId) - XCTAssertEqual(signature.toHexString(), "c0567b120d3de6b3042ae3de1aa346e167454c675e1eaf40ea2f9de89e6a95c2783c1aa6c96aa1e0aaead4ae8901052fa9fd7abe4acb331adafd61610e93c3f01c") + XCTAssertEqual(signature.toHexString(), "39e48b17008344acd58c86fba540ce65a9a4dad048e0d4d10efced291e02174c7267c9749cd2c1f9738ba1267f6fb8caadd054497daa20e2eaaee6472e7fde4e1b") } func testWithChainId() throws { @@ -102,6 +103,18 @@ class EIP712Tests: LocalTestCase { account: account!, password: password, chainId: chainId) - XCTAssertEqual(signature.toHexString(), "9ee2aadf14739e1cafc3bc1a0b48457c12419d5b480a8ffa86eb7df538c82d0753ca2a6f8024dea576b383cbcbe5e2b181b087e489298674bf6512756cabc5b01b") + XCTAssertEqual(signature.toHexString(), "e5ebc20f5794b756f01adb271db9e535df74751dfce4328b2f5bae4740d6e5ef392626b95ae0c0975a91b99033b079e6e0ccd41cb6fa70dd5f8833d78af4282f1c") + } + + 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 new file mode 100644 index 000000000..e188370f8 --- /dev/null +++ b/Tests/web3swiftTests/localTests/EIP712TypedDataPayloadTests.swift @@ -0,0 +1,593 @@ +// +// EIP712TypedDataPayloadTests.swift +// +// Created by JeneaVranceanu on 18.10.2023. +// + +import Foundation +import XCTest +import web3swift +@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 { + func testEIP712Parser() throws { + let parsedEip712TypedData = try EIP712Parser.parse(EIP712TestData.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!") + } + + 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)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()) + } + + func testEIP712EncodeType() throws { + 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(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(EIP712TestData.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(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(EIP712TestData.testTypedDataPayload) + XCTAssertEqual(try parsedEip712TypedData.signHash().toHexString(), "be609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2") + } + + func testEIP712Signing() throws { + 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)! + 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 โ€– structHash(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") + } + + 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") + } + + /// 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` + /// + /// The `InvalidOrderSignature` error is thrown when hash created for signing is invalid, thus, resulting in invalid signature. + func testEIP712NoInvalidOrderSignature() 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") + } + + /// 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") + } +}