diff --git a/README.md b/README.md index d468f2b8b..8e16493f5 100755 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ [![Platform](https://img.shields.io/cocoapods/p/web3swift?style=flat)](http://cocoapods.org/pods/web3swift) [![CocoaPods Compatible](https://img.shields.io/cocoapods/v/web3swift?style=flat)](http://cocoapods.org/pods/web3swift) [![License](https://img.shields.io/cocoapods/l/web3swift.svg?style=flat)](https://github.com/web3swift-team/web3swift/blob/master/LICENSE.md) -[![support](https://brianmacdonald.github.io/Ethonate/svg/eth-support-blue.svg)](https://brianmacdonald.github.io/Ethonate/address#0xe22b8979739d724343bd002f9f432f5990879901) [![Stackoverflow](https://img.shields.io/badge/stackoverflow-ask-blue.svg)](https://stackoverflow.com/questions/tagged/web3swift) --- diff --git a/Sources/Web3Core/Contract/ContractProtocol.swift b/Sources/Web3Core/Contract/ContractProtocol.swift index 43a4c1543..5a2ab8863 100755 --- a/Sources/Web3Core/Contract/ContractProtocol.swift +++ b/Sources/Web3Core/Contract/ContractProtocol.swift @@ -143,8 +143,12 @@ public protocol ContractProtocol { /// - name with arguments:`myFunction(uint256)`. /// - method signature (with or without `0x` prefix, case insensitive): `0xFFffFFff`; /// - data: non empty bytes to decode; - /// - Returns: dictionary with decoded values. `nil` if decoding failed. - func decodeReturnData(_ method: String, data: Data) -> [String: Any]? + /// - Returns: dictionary with decoded values. + /// - Throws: + /// - `Web3Error.revert(String, String?)` when function call aborted by `revert(string)` and `require(expression, string)`. + /// - `Web3Error.revertCustom(String, Dictionary)` when function call aborted by `revert CustomError()`. + @discardableResult + func decodeReturnData(_ method: String, data: Data) throws -> [String: Any] /// Decode input arguments of a function. /// - Parameters: @@ -280,6 +284,13 @@ extension DefaultContractProtocol { return encodedData } + public func event(_ event: String, parameters: [Any]) -> [EventFilterParameters.Topic?] { + guard let event = events[event] else { + return [] + } + return event.encodeParameters(parameters) + } + public func parseEvent(_ eventLog: EventLog) -> (eventName: String?, eventData: [String: Any]?) { for (eName, ev) in self.events { if !ev.anonymous { @@ -313,13 +324,40 @@ extension DefaultContractProtocol { return bloom.test(topic: event.topic) } - public func decodeReturnData(_ method: String, data: Data) -> [String: Any]? { + @discardableResult + public func decodeReturnData(_ method: String, data: Data) throws -> [String: Any] { if method == "fallback" { - return [String: Any]() + return [:] + } + + guard let function = methods[method]?.first else { + throw Web3Error.inputError(desc: "Make sure ABI you use contains '\(method)' method.") + } + + switch data.count % 32 { + case 0: + return try function.decodeReturnData(data) + case 4: + let selector = data[0..<4] + if selector.toHexString() == "08c379a0", let reason = ABI.Element.EthError.decodeStringError(data[4...]) { + throw Web3Error.revert("revert(string)` or `require(expression, string)` was executed. reason: \(reason)", reason: reason) + } + else if selector.toHexString() == "4e487b71", let reason = ABI.Element.EthError.decodePanicError(data[4...]) { + let panicCode = String(format: "%02X", Int(reason)).addHexPrefix() + throw Web3Error.revert("Error: call revert exception; VM Exception while processing transaction: reverted with panic code \(panicCode)", reason: panicCode) + } + else if let customError = errors[selector.toHexString().addHexPrefix().lowercased()] { + if let errorArgs = customError.decodeEthError(data[4...]) { + throw Web3Error.revertCustom(customError.signature, errorArgs) + } else { + throw Web3Error.inputError(desc: "Signature matches \(customError.errorDeclaration) but failed to be decoded.") + } + } else { + throw Web3Error.inputError(desc: "Make sure ABI you use contains error that can match signature: 0x\(selector.toHexString())") + } + default: + throw Web3Error.inputError(desc: "Given data has invalid bytes count.") } - return methods[method]?.compactMap({ function in - return function.decodeReturnData(data) - }).first } public func decodeInputData(_ method: String, data: Data) -> [String: Any]? { @@ -339,8 +377,32 @@ extension DefaultContractProtocol { return function.decodeInputData(Data(data[data.startIndex + 4 ..< data.startIndex + data.count])) } + public func decodeEthError(_ data: Data) -> [String: Any]? { + guard data.count >= 4, + let err = errors.first(where: { $0.value.methodEncoding == data[0..<4] })?.value else { + return nil + } + return err.decodeEthError(data[4...]) + } + public func getFunctionCalled(_ data: Data) -> ABI.Element.Function? { guard data.count >= 4 else { return nil } return methods[data[data.startIndex ..< data.startIndex + 4].toHexString().addHexPrefix()]?.first } } + +extension DefaultContractProtocol { + @discardableResult + public func callStatic(_ method: String, parameters: [Any], provider: Web3Provider) async throws -> [String: Any] { + guard let address = address else { + throw Web3Error.inputError(desc: "RPC failed: contract is missing an address.") + } + guard let data = self.method(method, parameters: parameters, extraData: nil) else { + throw Web3Error.dataError + } + let transaction = CodableTransaction(to: address, data: data) + + let result: Data = try await APIRequest.sendRequest(with: provider, for: .call(transaction, .latest)).result + return try decodeReturnData(method, data: result) + } +} diff --git a/Sources/Web3Core/EthereumABI/ABIElements.swift b/Sources/Web3Core/EthereumABI/ABIElements.swift index 5dca0b331..4c58f08e7 100755 --- a/Sources/Web3Core/EthereumABI/ABIElements.swift +++ b/Sources/Web3Core/EthereumABI/ABIElements.swift @@ -202,7 +202,7 @@ extension ABI.Element.Constructor { extension ABI.Element.Function { /// Encode parameters of a given contract method - /// - Parameter parameters: Parameters to pass to Ethereum contract + /// - Parameters: Parameters to pass to Ethereum contract /// - Returns: Encoded data public func encodeParameters(_ parameters: [Any]) -> Data? { guard parameters.count == inputs.count, @@ -211,13 +211,123 @@ extension ABI.Element.Function { } } -// MARK: - Event logs decoding +// MARK: - Event logs decoding & encoding extension ABI.Element.Event { public func decodeReturnedLogs(eventLogTopics: [Data], eventLogData: Data) -> [String: Any]? { guard let eventContent = ABIDecoder.decodeLog(event: self, eventLogTopics: eventLogTopics, eventLogData: eventLogData) else { return nil } return eventContent } + + public static func encodeTopic(input: ABI.Element.Event.Input, value: Any) -> EventFilterParameters.Topic? { + switch input.type { + case .string: + guard let string = value as? String else { + return nil + } + return .string(string.sha3(.keccak256).addHexPrefix()) + case .dynamicBytes: + guard let data = ABIEncoder.convertToData(value) else { + return nil + } + return .string(data.sha3(.keccak256).toHexString().addHexPrefix()) + case .bytes(length: _): + guard let data = ABIEncoder.convertToData(value), let data = data.setLengthLeft(32) else { + return nil + } + return .string(data.toHexString().addHexPrefix()) + case .address, .uint(bits: _), .int(bits: _), .bool: + guard let encoded = ABIEncoder.encodeSingleType(type: input.type, value: value) else { + return nil + } + return .string(encoded.toHexString().addHexPrefix()) + default: + guard let data = try? ABIEncoder.abiEncode(value).setLengthLeft(32) else { + return nil + } + return .string(data.toHexString().addHexPrefix()) + } + } + + public func encodeParameters(_ parameters: [Any?]) -> [EventFilterParameters.Topic?] { + guard parameters.count <= inputs.count else { + // too many arguments for fragment + return [] + } + var topics: [EventFilterParameters.Topic?] = [] + + if !anonymous { + topics.append(.string(topic.toHexString().addHexPrefix())) + } + + for (i, p) in parameters.enumerated() { + let input = inputs[i] + if !input.indexed { + // cannot filter non-indexed parameters; must be null + return [] + } + if p == nil { + topics.append(nil) + } else if input.type.isArray || input.type.isTuple { + // filtering with tuples or arrays not supported + return [] + } else if let p = p as? Array { + topics.append(.strings(p.map { Self.encodeTopic(input: input, value: $0) })) + } else { + topics.append(Self.encodeTopic(input: input, value: p!)) + } + } + + // Trim off trailing nulls + while let last = topics.last { + if last == nil { + topics.removeLast() + } else if case .string(let string) = last, string == nil { + topics.removeLast() + } else { + break + } + } + return topics + } +} + +// MARK: - Decode custom error + +extension ABI.Element.EthError { + /// Decodes `revert CustomError(_)` calls. + /// - Parameters: + /// - data: bytes returned by a function call that stripped error signature hash. + /// - Returns: a dictionary containing decoded data mappend to indices and names of returned values or nil if decoding failed. + public func decodeEthError(_ data: Data) -> [String: Any]? { + guard inputs.count * 32 <= data.count, + let decoded = ABIDecoder.decode(types: inputs, data: data) else { + return nil + } + + var result = [String: Any]() + for (index, out) in inputs.enumerated() { + result["\(index)"] = decoded[index] + if !out.name.isEmpty { + result[out.name] = decoded[index] + } + } + return result + } + + /// Decodes `revert(string)` or `require(expression, string)` calls. + /// These calls are decomposed as `Error(string)` error. + public static func decodeStringError(_ data: Data) -> String? { + let decoded = ABIDecoder.decode(types: [.init(name: "", type: .string)], data: data) + return decoded?.first as? String + } + + /// Decodes `Panic(uint256)` errors. + /// See more about panic code explain at: https://docs.soliditylang.org/en/v0.8.21/control-structures.html#panic-via-assert-and-error-via-require + public static func decodePanicError(_ data: Data) -> BigUInt? { + let decoded = ABIDecoder.decode(types: [.init(name: "", type: .uint(bits: 256))], data: data) + return decoded?.first as? BigUInt + } } // MARK: - Function input/output decoding @@ -232,7 +342,7 @@ extension ABI.Element { case .fallback: return nil case .function(let function): - return function.decodeReturnData(data) + return try? function.decodeReturnData(data) case .receive: return nil case .error: @@ -265,74 +375,38 @@ extension ABI.Element.Function { return ABIDecoder.decodeInputData(rawData, methodEncoding: methodEncoding, inputs: inputs) } - /// Decodes data returned by a function call. Able to decode `revert(string)`, `revert CustomError(...)` and `require(expression, string)` calls. + /// Decodes data returned by a function call. /// - Parameters: /// - data: bytes returned by a function call; - /// - errors: optional dictionary of known errors that could be returned by the function you called. Used to decode the error information. /// - Returns: a dictionary containing decoded data mappend to indices and names of returned values if these are not `nil`. - /// If `data` is an error response returns dictionary containing all available information about that specific error. Read more for details. + /// - Throws: + /// - `Web3Error.processingError(desc: String)` when decode process failed. /// /// Return cases: - /// - when no `outputs` declared and `data` is not an error response: + /// - when no `outputs` declared: /// ```swift - /// ["_success": true] + /// [:] /// ``` /// - when `outputs` declared and decoding completed successfully: /// ```swift - /// ["_success": true, "0": value_1, "1": value_2, ...] + /// ["0": value_1, "1": value_2, ...] /// ``` /// Additionally this dictionary will have mappings to output names if these names are specified in the ABI; - /// - function call was aborted using `revert(message)` or `require(expression, message)`: - /// ```swift - /// ["_success": false, "_abortedByRevertOrRequire": true, "_errorMessage": message]` - /// ``` - /// - function call was aborted using `revert CustomMessage()` and `errors` argument contains the ABI of that custom error type: - /// ```swift - /// ["_success": false, - /// "_abortedByRevertOrRequire": true, - /// "_error": error_name_and_types, // e.g. `MyCustomError(uint256, address senderAddress)` - /// "0": error_arg1, - /// "1": error_arg2, - /// ..., - /// "error_arg1_name": error_arg1, // Only named arguments will be mapped to their names, e.g. `"senderAddress": EthereumAddress` - /// "error_arg2_name": error_arg2, // Otherwise, you can query them by position index. - /// ...] - /// ``` - /// - in case of any error: - /// ```swift - /// ["_success": false, "_failureReason": String] - /// ``` - /// Error reasons include: - /// - `outputs` declared but at least one value failed to be decoded; - /// - `data.count` is less than `outputs.count * 32`; - /// - `outputs` defined and `data` is empty; - /// - `data` represent reverted transaction - /// - /// How `revert(string)` and `require(expression, string)` return value is decomposed: - /// - `08C379A0` function selector for `Error(string)`; - /// - next 32 bytes are the data offset; - /// - next 32 bytes are the error message length; - /// - the next N bytes, where N >= 32, are the message bytes - /// - the rest are 0 bytes padding. - public func decodeReturnData(_ data: Data, errors: [String: ABI.Element.EthError]? = nil) -> [String: Any] { - if let decodedError = decodeErrorResponse(data, errors: errors) { - return decodedError - } - + public func decodeReturnData(_ data: Data) throws -> [String: Any] { guard !outputs.isEmpty else { NSLog("Function doesn't have any output types to decode given data.") - return ["_success": true] + return [:] } guard outputs.count * 32 <= data.count else { - return ["_success": false, "_failureReason": "Bytes count must be at least \(outputs.count * 32). Given \(data.count). Decoding will fail."] + throw Web3Error.processingError(desc: "Bytes count must be at least \(outputs.count * 32). Given \(data.count). Decoding will fail.") } // TODO: need improvement - we should be able to tell which value failed to be decoded guard let values = ABIDecoder.decode(types: outputs, data: data) else { - return ["_success": false, "_failureReason": "Failed to decode at least one value."] + throw Web3Error.processingError(desc: "Failed to decode at least one value.") } - var returnArray: [String: Any] = ["_success": true] + var returnArray: [String: Any] = [:] for i in outputs.indices { returnArray["\(i)"] = values[i] if !outputs[i].name.isEmpty { @@ -381,6 +455,7 @@ extension ABI.Element.Function { /// // "_parsingError" is optional and is present only if decoding of custom error arguments failed /// "_parsingError": "Data matches MyCustomError(uint256, address senderAddress) but failed to be decoded."] /// ``` + @available(*, deprecated, message: "Use decode function from `ABI.Element.EthError` instead") public func decodeErrorResponse(_ data: Data, errors: [String: ABI.Element.EthError]? = nil) -> [String: Any]? { /// If data is empty and outputs are expected it is treated as a `require(expression)` or `revert()` call with no message. /// In solidity `require(false)` and `revert()` calls return empty error response. diff --git a/Sources/Web3Core/EthereumABI/ABIParameterTypes.swift b/Sources/Web3Core/EthereumABI/ABIParameterTypes.swift index eb536237e..7abd04ed1 100755 --- a/Sources/Web3Core/EthereumABI/ABIParameterTypes.swift +++ b/Sources/Web3Core/EthereumABI/ABIParameterTypes.swift @@ -168,31 +168,79 @@ extension ABI.Element.ParameterType: Equatable { } extension ABI.Element.Function { + /// String representation of a function, e.g. `transfer(address,uint256)`. public var signature: String { return "\(name ?? "")(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))" } + /// Function selector, e.g. `"cafe1234"`. Without hex prefix `0x`. + @available(*, deprecated, renamed: "selector", message: "Please, use 'selector' property instead.") public var methodString: String { + return selector + } + + /// Function selector, e.g. `"cafe1234"`. Without hex prefix `0x`. + public var selector: String { return String(signature.sha3(.keccak256).prefix(8)) } + /// Function selector (e.g. `0xcafe1234`) but as raw bytes. + @available(*, deprecated, renamed: "selectorEncoded", message: "Please, use 'selectorEncoded' property instead.") public var methodEncoding: Data { - return signature.data(using: .ascii)!.sha3(.keccak256)[0...3] + return selectorEncoded + } + + /// Function selector (e.g. `0xcafe1234`) but as raw bytes. + public var selectorEncoded: Data { + return Data.fromHex(selector)! } } // MARK: - Event topic extension ABI.Element.Event { + /// String representation of an event, e.g. `ContractCreated(address)`. public var signature: String { return "\(name)(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))" } + /// Hashed signature of an event, e.g. `0xcf78cf0d6f3d8371e1075c69c492ab4ec5d8cf23a1a239b6a51a1d00be7ca312`. public var topic: Data { return signature.data(using: .ascii)!.sha3(.keccak256) } } +extension ABI.Element.EthError { + /// String representation of an error, e.g. `TrasferFailed(address)`. + public var signature: String { + return "\(name)(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))" + } + + /// Error selector, e.g. `"cafe1234"`. Without hex prefix `0x`. + @available(*, deprecated, renamed: "selector", message: "Please, use 'selector' property instead.") + public var methodString: String { + return selector + } + + /// Error selector, e.g. `"cafe1234"`. Without hex prefix `0x`. + public var selector: String { + return String(signature.sha3(.keccak256).prefix(8)) + } + + /// Error selector (e.g. `0xcafe1234`) but as raw bytes. + @available(*, deprecated, renamed: "selectorEncoded", message: "Please, use 'selectorEncoded' property instead.") + public var methodEncoding: Data { + return selectorEncoded + } + + /// Error selector (e.g. `0xcafe1234`) but as raw bytes. + public var selectorEncoded: Data { + return Data.fromHex(selector)! + } +} + extension ABI.Element.ParameterType: ABIEncoding { + + /// Returns a valid solidity type like `address`, `uint128` or any other built-in type from Solidity. public var abiRepresentation: String { switch self { case .uint(let bits): 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/EthereumABI/Sequence+ABIExtension.swift b/Sources/Web3Core/EthereumABI/Sequence+ABIExtension.swift index 1d2f32744..390b928ee 100644 --- a/Sources/Web3Core/EthereumABI/Sequence+ABIExtension.swift +++ b/Sources/Web3Core/EthereumABI/Sequence+ABIExtension.swift @@ -56,6 +56,8 @@ public extension Sequence where Element == ABI.Element { var errors = [String: ABI.Element.EthError]() for case let .error(error) in self { errors[error.name] = error + errors[error.signature] = error + errors[error.methodString.addHexPrefix().lowercased()] = error } return errors } diff --git a/Sources/Web3Core/EthereumNetwork/Request/APIRequest+ComputedProperties.swift b/Sources/Web3Core/EthereumNetwork/Request/APIRequest+ComputedProperties.swift index cfa7f5190..fc711ce4b 100644 --- a/Sources/Web3Core/EthereumNetwork/Request/APIRequest+ComputedProperties.swift +++ b/Sources/Web3Core/EthereumNetwork/Request/APIRequest+ComputedProperties.swift @@ -8,15 +8,12 @@ import Foundation extension APIRequest { - var method: REST { + public var method: REST { .POST } - public var encodedBody: Data { - let request = RequestBody(method: call, params: parameters) - // this is safe to force try this here - // Because request must failed to compile if it not conformable with `Encodable` protocol - return try! JSONEncoder().encode(request) + public var encodedBody: Data { + RequestBody(method: call, params: parameters).encodedBody } var parameters: [RequestParameter] { diff --git a/Sources/Web3Core/EthereumNetwork/Request/APIRequest+Methods.swift b/Sources/Web3Core/EthereumNetwork/Request/APIRequest+Methods.swift index 58e13aa0f..c25ee496d 100644 --- a/Sources/Web3Core/EthereumNetwork/Request/APIRequest+Methods.swift +++ b/Sources/Web3Core/EthereumNetwork/Request/APIRequest+Methods.swift @@ -8,22 +8,116 @@ import Foundation import BigInt +/// TODO: should we do more error explain like ethers.js? +/// https://github.com/ethers-io/ethers.js/blob/0bfa7f497dc5793b66df7adfb42c6b846c51d794/packages/providers/src.ts/json-rpc-provider.ts#L55 +func checkError(method: String, error: JsonRpcErrorObject.RpcError) throws -> String { + if method == "eth_call" { + if let result = spelunkData(value: error) { + return result.data + } + throw Web3Error.nodeError(desc: "Error data decoding failed: missing revert data in exception; Transaction reverted without a reason string.") + } + + throw Web3Error.nodeError(desc: error.message) +} + +func spelunkData(value: Any?) -> (message: String, data: String)? { + if (value == nil) { + return nil + } + + func spelunkRpcError(_ message: String, data: String) -> (message: String, data: String)? { + if message.contains("revert") && data.isHex { + return (message, data) + } else { + return nil + } + } + + if let error = value as? JsonRpcErrorObject.RpcError { + if let data = error.data as? String { + return spelunkRpcError(error.message, data: data) + } else { + return spelunkData(value: error.data) + } + } + + // Spelunk further... + if let object = value as? [String: Any] { + if let message = object["message"] as? String, + let data = object["data"] as? String { + return spelunkRpcError(message, data: data) + } + + for value in object.values { + if let result = spelunkData(value: value) { + return result + } + return nil + } + } + if let array = value as? [Any] { + for e in array { + if let result = spelunkData(value: e) { + return result + } + return nil + } + } + + // Might be a JSON string we can further descend... + if let string = value as? String, let data = string.data(using: .utf8) { + let json = try? JSONSerialization.jsonObject(with: data) + return spelunkData(value: json) + } + + return nil +} + extension APIRequest { public static func sendRequest(with provider: Web3Provider, for call: APIRequest) async throws -> APIResponse { - let request = setupRequest(for: call, with: provider) - return try await APIRequest.send(uRLRequest: request, with: provider.session) + try await send(call.call, parameters: call.parameters, with: provider) } - static func setupRequest(for call: APIRequest, with provider: Web3Provider) -> URLRequest { + static func setupRequest(for body: RequestBody, with provider: Web3Provider) -> URLRequest { var urlRequest = URLRequest(url: provider.url, cachePolicy: .reloadIgnoringCacheData) urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") - urlRequest.httpMethod = call.method.rawValue - urlRequest.httpBody = call.encodedBody + urlRequest.httpMethod = "POST" + urlRequest.httpBody = body.encodedBody return urlRequest } - public static func send(uRLRequest: URLRequest, with session: URLSession) async throws -> APIResponse { + public static func send(_ method: String, parameters: [Encodable], with provider: Web3Provider) async throws -> APIResponse { + let body = RequestBody(method: method, params: parameters) + let uRLRequest = setupRequest(for: body, with: provider) + + let data: Data + do { + data = try await send(uRLRequest: uRLRequest, with: provider.session) + } catch Web3Error.rpcError(let error) { + let responseAsString = try checkError(method: method, error: error) + guard let LiteralType = Result.self as? LiteralInitiableFromString.Type, + let literalValue = LiteralType.init(from: responseAsString), + let result = literalValue as? Result else { + throw Web3Error.dataError + } + return APIResponse(id: 2, result: result) + } + + /// Checks if `Result` type can be initialized from HEX-encoded bytes. + /// If it can - we attempt initializing a value of `Result` type. + if let LiteralType = Result.self as? LiteralInitiableFromString.Type { + guard let responseAsString = try? JSONDecoder().decode(APIResponse.self, from: data) else { throw Web3Error.dataError } + guard let literalValue = LiteralType.init(from: responseAsString.result) else { throw Web3Error.dataError } + /// `literalValue` conforms `LiteralInitiableFromString` (which conforms to an `APIResponseType` type) so it never fails. + guard let result = literalValue as? Result else { throw Web3Error.typeError } + return APIResponse(id: responseAsString.id, jsonrpc: responseAsString.jsonrpc, result: result) + } + return try JSONDecoder().decode(APIResponse.self, from: data) + } + + public static func send(uRLRequest: URLRequest, with session: URLSession) async throws -> Data { let (data, response) = try await session.data(for: uRLRequest) guard 200 ..< 400 ~= response.statusCode else { @@ -34,9 +128,9 @@ extension APIRequest { } } - if let error = (try? JSONDecoder().decode(JsonRpcErrorObject.self, from: data))?.error { + if let error = JsonRpcErrorObject.init(from: data)?.error { guard let parsedErrorCode = error.parsedErrorCode else { - throw Web3Error.nodeError(desc: "\(error.message)\nError code: \(error.code)") + throw Web3Error.rpcError(error) } let description = "\(parsedErrorCode.errorName). Error code: \(error.code). \(error.message)" switch parsedErrorCode { @@ -49,35 +143,51 @@ extension APIRequest { } } - /// This bit of code is purposed to work with literal types that comes in ``Response`` in hexString type. - /// Currently it's just `Data` and any kind of Integers `(U)Int`, `Big(U)Int`. - if let LiteralType = Result.self as? LiteralInitiableFromString.Type { - guard let responseAsString = try? JSONDecoder().decode(APIResponse.self, from: data) else { throw Web3Error.dataError } - guard let literalValue = LiteralType.init(from: responseAsString.result) else { throw Web3Error.dataError } - /// `literalValue` conforms `LiteralInitiableFromString`, that conforming to an `APIResponseType` type, so it's never fails. - guard let result = literalValue as? Result else { throw Web3Error.typeError } - return APIResponse(id: responseAsString.id, jsonrpc: responseAsString.jsonrpc, result: result) - } - return try JSONDecoder().decode(APIResponse.self, from: data) + return data } } /// JSON RPC Error object. See official specification https://www.jsonrpc.org/specification#error_object -private struct JsonRpcErrorObject: Decodable { +public struct JsonRpcErrorObject { public let error: RpcError? - class RpcError: Decodable { - let message: String - let code: Int + public class RpcError { + public let message: String + public let code: Int + public let data: Any? + + init(message: String, code: Int, data: Any?) { + self.message = message + self.code = code + self.data = data + } + var parsedErrorCode: JsonRpcErrorCode? { JsonRpcErrorCode.from(code) } } + + init?(from data: Data) { + guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return nil + } + if let error = root["error"] as? [String: Any], + let message = error["message"] as? String, + let code = error["code"] as? Int { + guard let errorData = error["data"] else { + self.error = RpcError(message: message, code: code, data: nil) + return + } + self.error = RpcError(message: message, code: code, data: errorData) + } else { + self.error = nil + } + } } /// For error codes specification see chapter `5.1 Error object` /// https://www.jsonrpc.org/specification#error_object -private enum JsonRpcErrorCode { +enum JsonRpcErrorCode { /// -32700 /// Invalid JSON was received by the server. An error occurred on the server while parsing the JSON case parseError diff --git a/Sources/Web3Core/EthereumNetwork/Request/APIRequest+UtilityTypes.swift b/Sources/Web3Core/EthereumNetwork/Request/APIRequest+UtilityTypes.swift index ca55d622f..1e40e53d5 100644 --- a/Sources/Web3Core/EthereumNetwork/Request/APIRequest+UtilityTypes.swift +++ b/Sources/Web3Core/EthereumNetwork/Request/APIRequest+UtilityTypes.swift @@ -14,7 +14,7 @@ public struct APIResponse: Decodable where Result: APIResultType { public var result: Result } -enum REST: String { +public enum REST: String { case POST case GET } @@ -24,5 +24,30 @@ struct RequestBody: Encodable { var id = Counter.increment() var method: String - var params: [RequestParameter] + var params: [Encodable] + + enum CodingKeys: String, CodingKey { + case jsonrpc + case id + case method + case params + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(jsonrpc, forKey: .jsonrpc) + try container.encode(id, forKey: .id) + try container.encode(method, forKey: .method) + + var paramsContainer = container.superEncoder(forKey: .params).unkeyedContainer() + try params.forEach { a in + try paramsContainer.encode(a) + } + } + + public var encodedBody: Data { + // Safe to use force-try because request will fail to + // compile if it's not conforming to the `Encodable` protocol. + return try! JSONEncoder().encode(self) + } } diff --git a/Sources/Web3Core/EthereumNetwork/Utility/HexDecodable+Extensions.swift b/Sources/Web3Core/EthereumNetwork/Utility/HexDecodable+Extensions.swift index d64bb9a09..739442fca 100644 --- a/Sources/Web3Core/EthereumNetwork/Utility/HexDecodable+Extensions.swift +++ b/Sources/Web3Core/EthereumNetwork/Utility/HexDecodable+Extensions.swift @@ -17,16 +17,17 @@ extension BigInt: LiteralInitiableFromString { } extension BigUInt: LiteralInitiableFromString { } extension Data: LiteralInitiableFromString { + /// Converts hexadecimal string representation of some bytes into actual bytes. + /// Notes: + /// - empty string will return `nil`; + /// - empty hex string, meaning it's equal to `"0x"`, will return empty `Data` object. + /// - Parameter hex: bytes represented as string. + /// - Returns: optional raw bytes. public static func fromHex(_ hex: String) -> Data? { - let string = hex.lowercased().stripHexPrefix() - let array = [UInt8](hex: string) - if array.count == 0 { - if hex == "0x" || hex == "" { - return Data() - } else { - return nil - } - } - return Data(array) + let hex = hex.lowercased().trim() + guard !hex.isEmpty else { return nil } + guard hex != "0x" else { return Data() } + let bytes = [UInt8](hex: hex.stripHexPrefix()) + return bytes.isEmpty ? nil : Data(bytes) } } 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+WordLists.swift b/Sources/Web3Core/KeystoreManager/BIP39+WordLists.swift index f5cbf4e13..534b3f62d 100755 --- a/Sources/Web3Core/KeystoreManager/BIP39+WordLists.swift +++ b/Sources/Web3Core/KeystoreManager/BIP39+WordLists.swift @@ -30,4 +30,7 @@ extension BIP39Language { var spanishWords: [String] { return "ábaco abdomen abeja abierto abogado abono aborto abrazo abrir abuelo abuso acabar academia acceso acción aceite acelga acento aceptar ácido aclarar acné acoger acoso activo acto actriz actuar acudir acuerdo acusar adicto admitir adoptar adorno aduana adulto aéreo afectar afición afinar afirmar ágil agitar agonía agosto agotar agregar agrio agua agudo águila aguja ahogo ahorro aire aislar ajedrez ajeno ajuste alacrán alambre alarma alba álbum alcalde aldea alegre alejar alerta aleta alfiler alga algodón aliado aliento alivio alma almeja almíbar altar alteza altivo alto altura alumno alzar amable amante amapola amargo amasar ámbar ámbito ameno amigo amistad amor amparo amplio ancho anciano ancla andar andén anemia ángulo anillo ánimo anís anotar antena antiguo antojo anual anular anuncio añadir añejo año apagar aparato apetito apio aplicar apodo aporte apoyo aprender aprobar apuesta apuro arado araña arar árbitro árbol arbusto archivo arco arder ardilla arduo área árido aries armonía arnés aroma arpa arpón arreglo arroz arruga arte artista asa asado asalto ascenso asegurar aseo asesor asiento asilo asistir asno asombro áspero astilla astro astuto asumir asunto atajo ataque atar atento ateo ático atleta átomo atraer atroz atún audaz audio auge aula aumento ausente autor aval avance avaro ave avellana avena avestruz avión aviso ayer ayuda ayuno azafrán azar azote azúcar azufre azul baba babor bache bahía baile bajar balanza balcón balde bambú banco banda baño barba barco barniz barro báscula bastón basura batalla batería batir batuta baúl bazar bebé bebida bello besar beso bestia bicho bien bingo blanco bloque blusa boa bobina bobo boca bocina boda bodega boina bola bolero bolsa bomba bondad bonito bono bonsái borde borrar bosque bote botín bóveda bozal bravo brazo brecha breve brillo brinco brisa broca broma bronce brote bruja brusco bruto buceo bucle bueno buey bufanda bufón búho buitre bulto burbuja burla burro buscar butaca buzón caballo cabeza cabina cabra cacao cadáver cadena caer café caída caimán caja cajón cal calamar calcio caldo calidad calle calma calor calvo cama cambio camello camino campo cáncer candil canela canguro canica canto caña cañón caoba caos capaz capitán capote captar capucha cara carbón cárcel careta carga cariño carne carpeta carro carta casa casco casero caspa castor catorce catre caudal causa cazo cebolla ceder cedro celda célebre celoso célula cemento ceniza centro cerca cerdo cereza cero cerrar certeza césped cetro chacal chaleco champú chancla chapa charla chico chiste chivo choque choza chuleta chupar ciclón ciego cielo cien cierto cifra cigarro cima cinco cine cinta ciprés circo ciruela cisne cita ciudad clamor clan claro clase clave cliente clima clínica cobre cocción cochino cocina coco código codo cofre coger cohete cojín cojo cola colcha colegio colgar colina collar colmo columna combate comer comida cómodo compra conde conejo conga conocer consejo contar copa copia corazón corbata corcho cordón corona correr coser cosmos costa cráneo cráter crear crecer creído crema cría crimen cripta crisis cromo crónica croqueta crudo cruz cuadro cuarto cuatro cubo cubrir cuchara cuello cuento cuerda cuesta cueva cuidar culebra culpa culto cumbre cumplir cuna cuneta cuota cupón cúpula curar curioso curso curva cutis dama danza dar dardo dátil deber débil década decir dedo defensa definir dejar delfín delgado delito demora denso dental deporte derecho derrota desayuno deseo desfile desnudo destino desvío detalle detener deuda día diablo diadema diamante diana diario dibujo dictar diente dieta diez difícil digno dilema diluir dinero directo dirigir disco diseño disfraz diva divino doble doce dolor domingo don donar dorado dormir dorso dos dosis dragón droga ducha duda duelo dueño dulce dúo duque durar dureza duro ébano ebrio echar eco ecuador edad edición edificio editor educar efecto eficaz eje ejemplo elefante elegir elemento elevar elipse élite elixir elogio eludir embudo emitir emoción empate empeño empleo empresa enano encargo enchufe encía enemigo enero enfado enfermo engaño enigma enlace enorme enredo ensayo enseñar entero entrar envase envío época equipo erizo escala escena escolar escribir escudo esencia esfera esfuerzo espada espejo espía esposa espuma esquí estar este estilo estufa etapa eterno ética etnia evadir evaluar evento evitar exacto examen exceso excusa exento exigir exilio existir éxito experto explicar exponer extremo fábrica fábula fachada fácil factor faena faja falda fallo falso faltar fama familia famoso faraón farmacia farol farsa fase fatiga fauna favor fax febrero fecha feliz feo feria feroz fértil fervor festín fiable fianza fiar fibra ficción ficha fideo fiebre fiel fiera fiesta figura fijar fijo fila filete filial filtro fin finca fingir finito firma flaco flauta flecha flor flota fluir flujo flúor fobia foca fogata fogón folio folleto fondo forma forro fortuna forzar fosa foto fracaso frágil franja frase fraude freír freno fresa frío frito fruta fuego fuente fuerza fuga fumar función funda furgón furia fusil fútbol futuro gacela gafas gaita gajo gala galería gallo gamba ganar gancho ganga ganso garaje garza gasolina gastar gato gavilán gemelo gemir gen género genio gente geranio gerente germen gesto gigante gimnasio girar giro glaciar globo gloria gol golfo goloso golpe goma gordo gorila gorra gota goteo gozar grada gráfico grano grasa gratis grave grieta grillo gripe gris grito grosor grúa grueso grumo grupo guante guapo guardia guerra guía guiño guion guiso guitarra gusano gustar haber hábil hablar hacer hacha hada hallar hamaca harina haz hazaña hebilla hebra hecho helado helio hembra herir hermano héroe hervir hielo hierro hígado higiene hijo himno historia hocico hogar hoguera hoja hombre hongo honor honra hora hormiga horno hostil hoyo hueco huelga huerta hueso huevo huida huir humano húmedo humilde humo hundir huracán hurto icono ideal idioma ídolo iglesia iglú igual ilegal ilusión imagen imán imitar impar imperio imponer impulso incapaz índice inerte infiel informe ingenio inicio inmenso inmune innato insecto instante interés íntimo intuir inútil invierno ira iris ironía isla islote jabalí jabón jamón jarabe jardín jarra jaula jazmín jefe jeringa jinete jornada joroba joven joya juerga jueves juez jugador jugo juguete juicio junco jungla junio juntar júpiter jurar justo juvenil juzgar kilo koala labio lacio lacra lado ladrón lagarto lágrima laguna laico lamer lámina lámpara lana lancha langosta lanza lápiz largo larva lástima lata látex latir laurel lavar lazo leal lección leche lector leer legión legumbre lejano lengua lento leña león leopardo lesión letal letra leve leyenda libertad libro licor líder lidiar lienzo liga ligero lima límite limón limpio lince lindo línea lingote lino linterna líquido liso lista litera litio litro llaga llama llanto llave llegar llenar llevar llorar llover lluvia lobo loción loco locura lógica logro lombriz lomo lonja lote lucha lucir lugar lujo luna lunes lupa lustro luto luz maceta macho madera madre maduro maestro mafia magia mago maíz maldad maleta malla malo mamá mambo mamut manco mando manejar manga maniquí manjar mano manso manta mañana mapa máquina mar marco marea marfil margen marido mármol marrón martes marzo masa máscara masivo matar materia matiz matriz máximo mayor mazorca mecha medalla medio médula mejilla mejor melena melón memoria menor mensaje mente menú mercado merengue mérito mes mesón meta meter método metro mezcla miedo miel miembro miga mil milagro militar millón mimo mina minero mínimo minuto miope mirar misa miseria misil mismo mitad mito mochila moción moda modelo moho mojar molde moler molino momento momia monarca moneda monja monto moño morada morder moreno morir morro morsa mortal mosca mostrar motivo mover móvil mozo mucho mudar mueble muela muerte muestra mugre mujer mula muleta multa mundo muñeca mural muro músculo museo musgo música muslo nácar nación nadar naipe naranja nariz narrar nasal natal nativo natural náusea naval nave navidad necio néctar negar negocio negro neón nervio neto neutro nevar nevera nicho nido niebla nieto niñez niño nítido nivel nobleza noche nómina noria norma norte nota noticia novato novela novio nube nuca núcleo nudillo nudo nuera nueve nuez nulo número nutria oasis obeso obispo objeto obra obrero observar obtener obvio oca ocaso océano ochenta ocho ocio ocre octavo octubre oculto ocupar ocurrir odiar odio odisea oeste ofensa oferta oficio ofrecer ogro oído oír ojo ola oleada olfato olivo olla olmo olor olvido ombligo onda onza opaco opción ópera opinar oponer optar óptica opuesto oración orador oral órbita orca orden oreja órgano orgía orgullo oriente origen orilla oro orquesta oruga osadía oscuro osezno oso ostra otoño otro oveja óvulo óxido oxígeno oyente ozono pacto padre paella página pago país pájaro palabra palco paleta pálido palma paloma palpar pan panal pánico pantera pañuelo papá papel papilla paquete parar parcela pared parir paro párpado parque párrafo parte pasar paseo pasión paso pasta pata patio patria pausa pauta pavo payaso peatón pecado pecera pecho pedal pedir pegar peine pelar peldaño pelea peligro pellejo pelo peluca pena pensar peñón peón peor pepino pequeño pera percha perder pereza perfil perico perla permiso perro persona pesa pesca pésimo pestaña pétalo petróleo pez pezuña picar pichón pie piedra pierna pieza pijama pilar piloto pimienta pino pintor pinza piña piojo pipa pirata pisar piscina piso pista pitón pizca placa plan plata playa plaza pleito pleno plomo pluma plural pobre poco poder podio poema poesía poeta polen policía pollo polvo pomada pomelo pomo pompa poner porción portal posada poseer posible poste potencia potro pozo prado precoz pregunta premio prensa preso previo primo príncipe prisión privar proa probar proceso producto proeza profesor programa prole promesa pronto propio próximo prueba público puchero pudor pueblo puerta puesto pulga pulir pulmón pulpo pulso puma punto puñal puño pupa pupila puré quedar queja quemar querer queso quieto química quince quitar rábano rabia rabo ración radical raíz rama rampa rancho rango rapaz rápido rapto rasgo raspa rato rayo raza razón reacción realidad rebaño rebote recaer receta rechazo recoger recreo recto recurso red redondo reducir reflejo reforma refrán refugio regalo regir regla regreso rehén reino reír reja relato relevo relieve relleno reloj remar remedio remo rencor rendir renta reparto repetir reposo reptil res rescate resina respeto resto resumen retiro retorno retrato reunir revés revista rey rezar rico riego rienda riesgo rifa rígido rigor rincón riñón río riqueza risa ritmo rito rizo roble roce rociar rodar rodeo rodilla roer rojizo rojo romero romper ron ronco ronda ropa ropero rosa rosca rostro rotar rubí rubor rudo rueda rugir ruido ruina ruleta rulo rumbo rumor ruptura ruta rutina sábado saber sabio sable sacar sagaz sagrado sala saldo salero salir salmón salón salsa salto salud salvar samba sanción sandía sanear sangre sanidad sano santo sapo saque sardina sartén sastre satán sauna saxofón sección seco secreto secta sed seguir seis sello selva semana semilla senda sensor señal señor separar sepia sequía ser serie sermón servir sesenta sesión seta setenta severo sexo sexto sidra siesta siete siglo signo sílaba silbar silencio silla símbolo simio sirena sistema sitio situar sobre socio sodio sol solapa soldado soledad sólido soltar solución sombra sondeo sonido sonoro sonrisa sopa soplar soporte sordo sorpresa sorteo sostén sótano suave subir suceso sudor suegra suelo sueño suerte sufrir sujeto sultán sumar superar suplir suponer supremo sur surco sureño surgir susto sutil tabaco tabique tabla tabú taco tacto tajo talar talco talento talla talón tamaño tambor tango tanque tapa tapete tapia tapón taquilla tarde tarea tarifa tarjeta tarot tarro tarta tatuaje tauro taza tazón teatro techo tecla técnica tejado tejer tejido tela teléfono tema temor templo tenaz tender tener tenis tenso teoría terapia terco término ternura terror tesis tesoro testigo tetera texto tez tibio tiburón tiempo tienda tierra tieso tigre tijera tilde timbre tímido timo tinta tío típico tipo tira tirón titán títere título tiza toalla tobillo tocar tocino todo toga toldo tomar tono tonto topar tope toque tórax torero tormenta torneo toro torpedo torre torso tortuga tos tosco toser tóxico trabajo tractor traer tráfico trago traje tramo trance trato trauma trazar trébol tregua treinta tren trepar tres tribu trigo tripa triste triunfo trofeo trompa tronco tropa trote trozo truco trueno trufa tubería tubo tuerto tumba tumor túnel túnica turbina turismo turno tutor ubicar úlcera umbral unidad unir universo uno untar uña urbano urbe urgente urna usar usuario útil utopía uva vaca vacío vacuna vagar vago vaina vajilla vale válido valle valor válvula vampiro vara variar varón vaso vecino vector vehículo veinte vejez vela velero veloz vena vencer venda veneno vengar venir venta venus ver verano verbo verde vereda verja verso verter vía viaje vibrar vicio víctima vida vídeo vidrio viejo viernes vigor vil villa vinagre vino viñedo violín viral virgo virtud visor víspera vista vitamina viudo vivaz vivero vivir vivo volcán volumen volver voraz votar voto voz vuelo vulgar yacer yate yegua yema yerno yeso yodo yoga yogur zafiro zanja zapato zarza zona zorro zumo zurdo".components(separatedBy: " ") } + var portugueseWords: [String] { + return "abacate abaixo abalar abater abduzir abelha aberto abismo abotoar abranger abreviar abrigar abrupto absinto absoluto absurdo abutre acabado acalmar acampar acanhar acaso aceitar acelerar acenar acervo acessar acetona achatar acidez acima acionado acirrar aclamar aclive acolhida acomodar acoplar acordar acumular acusador adaptar adega adentro adepto adequar aderente adesivo adeus adiante aditivo adjetivo adjunto admirar adorar adquirir adubo adverso advogado aeronave afastar aferir afetivo afinador afivelar aflito afluente afrontar agachar agarrar agasalho agenciar agilizar agiota agitado agora agradar agreste agrupar aguardar agulha ajoelhar ajudar ajustar alameda alarme alastrar alavanca albergue albino alcatra aldeia alecrim alegria alertar alface alfinete algum alheio aliar alicate alienar alinhar aliviar almofada alocar alpiste alterar altitude alucinar alugar aluno alusivo alvo amaciar amador amarelo amassar ambas ambiente ameixa amenizar amido amistoso amizade amolador amontoar amoroso amostra amparar ampliar ampola anagrama analisar anarquia anatomia andaime anel anexo angular animar anjo anomalia anotado ansioso anterior anuidade anunciar anzol apagador apalpar apanhado apego apelido apertada apesar apetite apito aplauso aplicada apoio apontar aposta aprendiz aprovar aquecer arame aranha arara arcada ardente areia arejar arenito aresta argiloso argola arma arquivo arraial arrebate arriscar arroba arrumar arsenal arterial artigo arvoredo asfaltar asilado aspirar assador assinar assoalho assunto astral atacado atadura atalho atarefar atear atender aterro ateu atingir atirador ativo atoleiro atracar atrevido atriz atual atum auditor aumentar aura aurora autismo autoria autuar avaliar avante avaria avental avesso aviador avisar avulso axila azarar azedo azeite azulejo babar babosa bacalhau bacharel bacia bagagem baiano bailar baioneta bairro baixista bajular baleia baliza balsa banal bandeira banho banir banquete barato barbado baronesa barraca barulho baseado bastante batata batedor batida batom batucar baunilha beber beijo beirada beisebol beldade beleza belga beliscar bendito bengala benzer berimbau berlinda berro besouro bexiga bezerro bico bicudo bienal bifocal bifurcar bigorna bilhete bimestre bimotor biologia biombo biosfera bipolar birrento biscoito bisneto bispo bissexto bitola bizarro blindado bloco bloquear boato bobagem bocado bocejo bochecha boicotar bolada boletim bolha bolo bombeiro bonde boneco bonita borbulha borda boreal borracha bovino boxeador branco brasa braveza breu briga brilho brincar broa brochura bronzear broto bruxo bucha budismo bufar bule buraco busca busto buzina cabana cabelo cabide cabo cabrito cacau cacetada cachorro cacique cadastro cadeado cafezal caiaque caipira caixote cajado caju calafrio calcular caldeira calibrar calmante calota camada cambista camisa camomila campanha camuflar canavial cancelar caneta canguru canhoto canivete canoa cansado cantar canudo capacho capela capinar capotar capricho captador capuz caracol carbono cardeal careca carimbar carneiro carpete carreira cartaz carvalho casaco casca casebre castelo casulo catarata cativar caule causador cautelar cavalo caverna cebola cedilha cegonha celebrar celular cenoura censo centeio cercar cerrado certeiro cerveja cetim cevada chacota chaleira chamado chapada charme chatice chave chefe chegada cheiro cheque chicote chifre chinelo chocalho chover chumbo chutar chuva cicatriz ciclone cidade cidreira ciente cigana cimento cinto cinza ciranda circuito cirurgia citar clareza clero clicar clone clube coado coagir cobaia cobertor cobrar cocada coelho coentro coeso cogumelo coibir coifa coiote colar coleira colher colidir colmeia colono coluna comando combinar comentar comitiva comover complexo comum concha condor conectar confuso congelar conhecer conjugar consumir contrato convite cooperar copeiro copiador copo coquetel coragem cordial corneta coronha corporal correio cortejo coruja corvo cosseno costela cotonete couro couve covil cozinha cratera cravo creche credor creme crer crespo criada criminal crioulo crise criticar crosta crua cruzeiro cubano cueca cuidado cujo culatra culminar culpar cultura cumprir cunhado cupido curativo curral cursar curto cuspir custear cutelo damasco datar debater debitar deboche debulhar decalque decimal declive decote decretar dedal dedicado deduzir defesa defumar degelo degrau degustar deitado deixar delator delegado delinear delonga demanda demitir demolido dentista depenado depilar depois depressa depurar deriva derramar desafio desbotar descanso desenho desfiado desgaste desigual deslize desmamar desova despesa destaque desviar detalhar detentor detonar detrito deusa dever devido devotado dezena diagrama dialeto didata difuso digitar dilatado diluente diminuir dinastia dinheiro diocese direto discreta disfarce disparo disquete dissipar distante ditador diurno diverso divisor divulgar dizer dobrador dolorido domador dominado donativo donzela dormente dorsal dosagem dourado doutor drenagem drible drogaria duelar duende dueto duplo duquesa durante duvidoso eclodir ecoar ecologia edificar edital educado efeito efetivar ejetar elaborar eleger eleitor elenco elevador eliminar elogiar embargo embolado embrulho embutido emenda emergir emissor empatia empenho empinado empolgar emprego empurrar emulador encaixe encenado enchente encontro endeusar endossar enfaixar enfeite enfim engajado engenho englobar engomado engraxar enguia enjoar enlatar enquanto enraizar enrolado enrugar ensaio enseada ensino ensopado entanto enteado entidade entortar entrada entulho envergar enviado envolver enxame enxerto enxofre enxuto epiderme equipar ereto erguido errata erva ervilha esbanjar esbelto escama escola escrita escuta esfinge esfolar esfregar esfumado esgrima esmalte espanto espelho espiga esponja espreita espumar esquerda estaca esteira esticar estofado estrela estudo esvaziar etanol etiqueta euforia europeu evacuar evaporar evasivo eventual evidente evoluir exagero exalar examinar exato exausto excesso excitar exclamar executar exemplo exibir exigente exonerar expandir expelir expirar explanar exposto expresso expulsar externo extinto extrato fabricar fabuloso faceta facial fada fadiga faixa falar falta familiar fandango fanfarra fantoche fardado farelo farinha farofa farpa fartura fatia fator favorita faxina fazenda fechado feijoada feirante felino feminino fenda feno fera feriado ferrugem ferver festejar fetal feudal fiapo fibrose ficar ficheiro figurado fileira filho filme filtrar firmeza fisgada fissura fita fivela fixador fixo flacidez flamingo flanela flechada flora flutuar fluxo focal focinho fofocar fogo foguete foice folgado folheto forjar formiga forno forte fosco fossa fragata fralda frango frasco fraterno freira frente fretar frieza friso fritura fronha frustrar fruteira fugir fulano fuligem fundar fungo funil furador furioso futebol gabarito gabinete gado gaiato gaiola gaivota galega galho galinha galocha ganhar garagem garfo gargalo garimpo garoupa garrafa gasoduto gasto gata gatilho gaveta gazela gelado geleia gelo gemada gemer gemido generoso gengiva genial genoma genro geologia gerador germinar gesso gestor ginasta gincana gingado girafa girino glacial glicose global glorioso goela goiaba golfe golpear gordura gorjeta gorro gostoso goteira governar gracejo gradual grafite gralha grampo granada gratuito graveto graxa grego grelhar greve grilo grisalho gritaria grosso grotesco grudado grunhido gruta guache guarani guaxinim guerrear guiar guincho guisado gula guloso guru habitar harmonia haste haver hectare herdar heresia hesitar hiato hibernar hidratar hiena hino hipismo hipnose hipoteca hoje holofote homem honesto honrado hormonal hospedar humorado iate ideia idoso ignorado igreja iguana ileso ilha iludido iluminar ilustrar imagem imediato imenso imersivo iminente imitador imortal impacto impedir implante impor imprensa impune imunizar inalador inapto inativo incenso inchar incidir incluir incolor indeciso indireto indutor ineficaz inerente infantil infestar infinito inflamar informal infrator ingerir inibido inicial inimigo injetar inocente inodoro inovador inox inquieto inscrito inseto insistir inspetor instalar insulto intacto integral intimar intocado intriga invasor inverno invicto invocar iogurte iraniano ironizar irreal irritado isca isento isolado isqueiro italiano janeiro jangada janta jararaca jardim jarro jasmim jato javali jazida jejum joaninha joelhada jogador joia jornal jorrar jovem juba judeu judoca juiz julgador julho jurado jurista juro justa labareda laboral lacre lactante ladrilho lagarta lagoa laje lamber lamentar laminar lampejo lanche lapidar lapso laranja lareira largura lasanha lastro lateral latido lavanda lavoura lavrador laxante lazer lealdade lebre legado legendar legista leigo leiloar leitura lembrete leme lenhador lentilha leoa lesma leste letivo letreiro levar leveza levitar liberal libido liderar ligar ligeiro limitar limoeiro limpador linda linear linhagem liquidez listagem lisura litoral livro lixa lixeira locador locutor lojista lombo lona longe lontra lorde lotado loteria loucura lousa louvar luar lucidez lucro luneta lustre lutador luva macaco macete machado macio madeira madrinha magnata magreza maior mais malandro malha malote maluco mamilo mamoeiro mamute manada mancha mandato manequim manhoso manivela manobrar mansa manter manusear mapeado maquinar marcador maresia marfim margem marinho marmita maroto marquise marreco martelo marujo mascote masmorra massagem mastigar matagal materno matinal matutar maxilar medalha medida medusa megafone meiga melancia melhor membro memorial menino menos mensagem mental merecer mergulho mesada mesclar mesmo mesquita mestre metade meteoro metragem mexer mexicano micro migalha migrar milagre milenar milhar mimado minerar minhoca ministro minoria miolo mirante mirtilo misturar mocidade moderno modular moeda moer moinho moita moldura moleza molho molinete molusco montanha moqueca morango morcego mordomo morena mosaico mosquete mostarda motel motim moto motriz muda muito mulata mulher multar mundial munido muralha murcho muscular museu musical nacional nadador naja namoro narina narrado nascer nativa natureza navalha navegar navio neblina nebuloso negativa negociar negrito nervoso neta neural nevasca nevoeiro ninar ninho nitidez nivelar nobreza noite noiva nomear nominal nordeste nortear notar noticiar noturno novelo novilho novo nublado nudez numeral nupcial nutrir nuvem obcecado obedecer objetivo obrigado obscuro obstetra obter obturar ocidente ocioso ocorrer oculista ocupado ofegante ofensiva oferenda oficina ofuscado ogiva olaria oleoso olhar oliveira ombro omelete omisso omitir ondulado oneroso ontem opcional operador oponente oportuno oposto orar orbitar ordem ordinal orfanato orgasmo orgulho oriental origem oriundo orla ortodoxo orvalho oscilar ossada osso ostentar otimismo ousadia outono outubro ouvido ovelha ovular oxidar oxigenar pacato paciente pacote pactuar padaria padrinho pagar pagode painel pairar paisagem palavra palestra palheta palito palmada palpitar pancada panela panfleto panqueca pantanal papagaio papelada papiro parafina parcial pardal parede partida pasmo passado pastel patamar patente patinar patrono paulada pausar peculiar pedalar pedestre pediatra pedra pegada peitoral peixe pele pelicano penca pendurar peneira penhasco pensador pente perceber perfeito pergunta perito permitir perna perplexo persiana pertence peruca pescado pesquisa pessoa petiscar piada picado piedade pigmento pilastra pilhado pilotar pimenta pincel pinguim pinha pinote pintar pioneiro pipoca piquete piranha pires pirueta piscar pistola pitanga pivete planta plaqueta platina plebeu plumagem pluvial pneu poda poeira poetisa polegada policiar poluente polvilho pomar pomba ponderar pontaria populoso porta possuir postal pote poupar pouso povoar praia prancha prato praxe prece predador prefeito premiar prensar preparar presilha pretexto prevenir prezar primata princesa prisma privado processo produto profeta proibido projeto prometer propagar prosa protetor provador publicar pudim pular pulmonar pulseira punhal punir pupilo pureza puxador quadra quantia quarto quase quebrar queda queijo quente querido quimono quina quiosque rabanada rabisco rachar racionar radial raiar rainha raio raiva rajada ralado ramal ranger ranhura rapadura rapel rapidez raposa raquete raridade rasante rascunho rasgar raspador rasteira rasurar ratazana ratoeira realeza reanimar reaver rebaixar rebelde rebolar recado recente recheio recibo recordar recrutar recuar rede redimir redonda reduzida reenvio refinar refletir refogar refresco refugiar regalia regime regra reinado reitor rejeitar relativo remador remendo remorso renovado reparo repelir repleto repolho represa repudiar requerer resenha resfriar resgatar residir resolver respeito ressaca restante resumir retalho reter retirar retomada retratar revelar revisor revolta riacho rica rigidez rigoroso rimar ringue risada risco risonho robalo rochedo rodada rodeio rodovia roedor roleta romano roncar rosado roseira rosto rota roteiro rotina rotular rouco roupa roxo rubro rugido rugoso ruivo rumo rupestre russo sabor saciar sacola sacudir sadio safira saga sagrada saibro salada saleiro salgado saliva salpicar salsicha saltar salvador sambar samurai sanar sanfona sangue sanidade sapato sarda sargento sarjeta saturar saudade saxofone sazonal secar secular seda sedento sediado sedoso sedutor segmento segredo segundo seiva seleto selvagem semanal semente senador senhor sensual sentado separado sereia seringa serra servo setembro setor sigilo silhueta silicone simetria simpatia simular sinal sincero singular sinopse sintonia sirene siri situado soberano sobra socorro sogro soja solda soletrar solteiro sombrio sonata sondar sonegar sonhador sono soprano soquete sorrir sorteio sossego sotaque soterrar sovado sozinho suavizar subida submerso subsolo subtrair sucata sucesso suco sudeste sufixo sugador sugerir sujeito sulfato sumir suor superior suplicar suposto suprimir surdina surfista surpresa surreal surtir suspiro sustento tabela tablete tabuada tacho tagarela talher talo talvez tamanho tamborim tampa tangente tanto tapar tapioca tardio tarefa tarja tarraxa tatuagem taurino taxativo taxista teatral tecer tecido teclado tedioso teia teimar telefone telhado tempero tenente tensor tentar termal terno terreno tese tesoura testado teto textura texugo tiara tigela tijolo timbrar timidez tingido tinteiro tiragem titular toalha tocha tolerar tolice tomada tomilho tonel tontura topete tora torcido torneio torque torrada torto tostar touca toupeira toxina trabalho tracejar tradutor trafegar trajeto trama trancar trapo traseiro tratador travar treino tremer trepidar trevo triagem tribo triciclo tridente trilogia trindade triplo triturar triunfal trocar trombeta trova trunfo truque tubular tucano tudo tulipa tupi turbo turma turquesa tutelar tutorial uivar umbigo unha unidade uniforme urologia urso urtiga urubu usado usina usufruir vacina vadiar vagaroso vaidoso vala valente validade valores vantagem vaqueiro varanda vareta varrer vascular vasilha vassoura vazar vazio veado vedar vegetar veicular veleiro velhice veludo vencedor vendaval venerar ventre verbal verdade vereador vergonha vermelho verniz versar vertente vespa vestido vetorial viaduto viagem viajar viatura vibrador videira vidraria viela viga vigente vigiar vigorar vilarejo vinco vinheta vinil violeta virada virtude visitar visto vitral viveiro vizinho voador voar vogal volante voleibol voltagem volumoso vontade vulto vuvuzela xadrez xarope xeque xeretar xerife xingar zangado zarpar zebu zelador zombar zoologia zumbido".components(separatedBy: " ") + } } diff --git a/Sources/Web3Core/KeystoreManager/BIP39.swift b/Sources/Web3Core/KeystoreManager/BIP39.swift index e9965ef5d..7deaf83d5 100755 --- a/Sources/Web3Core/KeystoreManager/BIP39.swift +++ b/Sources/Web3Core/KeystoreManager/BIP39.swift @@ -6,7 +6,7 @@ import Foundation import CryptoSwift -public enum BIP39Language { +public enum BIP39Language: CaseIterable { case english case chinese_simplified case chinese_traditional @@ -15,6 +15,7 @@ public enum BIP39Language { case french case italian case spanish + case portuguese public var words: [String] { switch self { @@ -34,9 +35,16 @@ public enum BIP39Language { return italianWords case .spanish: return spanishWords + case .portuguese: + return portugueseWords } } + public var separator: String { + return String(separatorCharacter) + } + + public var separatorCharacter: Character { switch self { case .japanese: return "\u{3000}" @@ -63,6 +71,8 @@ public enum BIP39Language { self = .italian case "spanish": self = .spanish + case "portuguese": + self = .portuguese default: return nil } @@ -95,11 +105,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 } @@ -122,7 +134,7 @@ public class BIP39 { public static func generateMnemonicsFromEntropy(entropy: Data, language: BIP39Language = .english) -> String? { guard entropy.count >= 16, entropy.count & 4 == 0 else { return nil } let separator = language.separator - let wordList = generateMnemonicsFrom(entropy: entropy) + let wordList = generateMnemonicsFrom(entropy: entropy, language: language) return wordList.joined(separator: separator) } 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/Transaction/EventfilterParameters.swift b/Sources/Web3Core/Transaction/EventfilterParameters.swift index 9850feb72..eb3c9342d 100755 --- a/Sources/Web3Core/Transaction/EventfilterParameters.swift +++ b/Sources/Web3Core/Transaction/EventfilterParameters.swift @@ -50,8 +50,8 @@ extension EventFilterParameters { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(fromBlock.description, forKey: .fromBlock) try container.encode(toBlock.description, forKey: .toBlock) - try container.encode(address.description, forKey: .address) - try container.encode(topics.textRepresentation, forKey: .topics) + try container.encode(address, forKey: .address) + try container.encode(topics, forKey: .topics) } } @@ -96,6 +96,17 @@ extension EventFilterParameters { case string(String?) case strings([Topic?]?) + public func encode(to encoder: Encoder) throws { + switch self { + case let .string(s): + var container = encoder.singleValueContainer() + try container.encode(s) + case let .strings(ss): + var container = encoder.unkeyedContainer() + try container.encode(contentsOf: ss ?? []) + } + } + var rawValue: String { switch self { case let .string(string): diff --git a/Sources/Web3Core/Utility/Data+Extension.swift b/Sources/Web3Core/Utility/Data+Extension.swift index 448728f1a..3a8185d0e 100755 --- a/Sources/Web3Core/Utility/Data+Extension.swift +++ b/Sources/Web3Core/Utility/Data+Extension.swift @@ -5,7 +5,8 @@ import Foundation -extension Data { +public extension Data { + init(fromArray values: [T]) { let values = values let ptrUB = values.withUnsafeBufferPointer { (ptr: UnsafeBufferPointer) in return ptr } @@ -33,32 +34,34 @@ extension Data { return difference == UInt8(0x00) } - public static func zero(_ data: inout Data) { + static func zero(_ data: inout Data) { let count = data.count data.withUnsafeMutableBytes { (body: UnsafeMutableRawBufferPointer) in body.baseAddress?.assumingMemoryBound(to: UInt8.self).initialize(repeating: 0, count: count) } } - public static func randomBytes(length: Int) -> Data? { - for _ in 0...1024 { - var data = Data(repeating: 0, count: length) - let result = data.withUnsafeMutableBytes { (body: UnsafeMutableRawBufferPointer) -> Int32? in - if let bodyAddress = body.baseAddress, body.count > 0 { - let pointer = bodyAddress.assumingMemoryBound(to: UInt8.self) - return SecRandomCopyBytes(kSecRandomDefault, length, pointer) - } else { - return nil - } - } - if let notNilResult = result, notNilResult == errSecSuccess { - return data - } + /** + Generates an array of random bytes of the specified length. + This function uses `SecRandomCopyBytes` to generate random bytes returning it as a `Data` object. + If an error occurs during random bytes generation, the function returns `nil`. + Error occurs only if `SecRandomCopyBytes` returns status that is not `errSecSuccess`. + See [all status codes](https://developer.apple.com/documentation/security/1542001-security_framework_result_codes) for possible error reasons. + Note: in v4 of web3swift this function will be deprecated and a new implementation will be provided that will throw occurred error. + - Parameter length: The number of random bytes to generate. + + - Returns: optional `Data` object containing the generated random bytes, or `nil` if an error occurred during generation. + */ + static func randomBytes(length: Int) -> Data? { + var entropyBytes = [UInt8](repeating: 0, count: length) + let status = SecRandomCopyBytes(kSecRandomDefault, entropyBytes.count, &entropyBytes) + guard status == errSecSuccess else { + return nil } - return nil + return Data(entropyBytes) } - public func bitsInRange(_ startingBit: Int, _ length: Int) -> UInt64? { // return max of 8 bytes for simplicity, non-public + func bitsInRange(_ startingBit: Int, _ length: Int) -> UInt64? { // return max of 8 bytes for simplicity, non-public if startingBit + length / 8 > self.count, length > 64, startingBit > 0, length >= 1 { return nil } let bytes = self[(startingBit/8) ..< (startingBit+length+7)/8] let padding = Data(repeating: 0, count: 8 - bytes.count) diff --git a/Sources/Web3Core/Utility/String+Extension.swift b/Sources/Web3Core/Utility/String+Extension.swift index dbe0a10ca..778558166 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 } @@ -135,6 +135,40 @@ extension String { func trim() -> String { trimmingCharacters(in: .whitespacesAndNewlines) } + + public var isHex: Bool { + var _str = self.trim() + if _str.isEmpty { + return false + } + _str = _str.stripHexPrefix() + for char in _str { + if !char.isHexDigit { + return false + } + } + return true + } + + /// Splits a string into groups of `every` n characters, grouping from left-to-right by default. If `backwards` is true, right-to-left. + public func split(every: Int, backwards: Bool = false) -> [String] { + var result = [String]() + + for i in stride(from: 0, to: self.count, by: every) { + switch backwards { + case true: + let endIndex = self.index(self.endIndex, offsetBy: -i) + let startIndex = self.index(endIndex, offsetBy: -every, limitedBy: self.startIndex) ?? self.startIndex + result.insert(String(self[startIndex.. [EventLog] { + try await APIRequest.sendRequest(with: self.provider, for: .getLogs(eventFilter)).result + } +} + public extension IEth { func send(_ transaction: CodableTransaction) async throws -> TransactionSendingResult { let request = APIRequest.sendTransaction(transaction) diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift index ddade0c00..0ce33372f 100755 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift +++ b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift @@ -25,6 +25,8 @@ public protocol IEth { func code(for address: EthereumAddress, onBlock: BlockNumber) async throws -> Hash + func getLogs(eventFilter: EventFilterParameters) async throws -> [EventLog] + func gasPrice() async throws -> BigUInt func getTransactionCount(for address: EthereumAddress, onBlock: BlockNumber) async throws -> BigUInt diff --git a/Sources/web3swift/Operations/ReadOperation.swift b/Sources/web3swift/Operations/ReadOperation.swift index 50dc30ab4..58945a32e 100755 --- a/Sources/web3swift/Operations/ReadOperation.swift +++ b/Sources/web3swift/Operations/ReadOperation.swift @@ -43,9 +43,6 @@ public class ReadOperation { let resultHex = data.toHexString().addHexPrefix() return ["result": resultHex] } - guard let decodedData = self.contract.decodeReturnData(self.method, data: data) else { - throw Web3Error.processingError(desc: "Can not decode returned parameters") - } - return decodedData + return try self.contract.decodeReturnData(self.method, data: data) } } 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..de01b10ba 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. @@ -112,5 +112,11 @@ extension Web3 { } return .init(transaction: transaction, web3: web3, contract: contract, method: method) } + + /// Combines `createReadOperation` & `callContractMethod` + @discardableResult + public func callStatic(_ method: String, parameters: [Any]) async throws -> [String: Any] { + try await contract.callStatic(method, parameters: parameters, provider: web3.provider) + } } } diff --git a/Sources/web3swift/Web3/Web3+HttpProvider.swift b/Sources/web3swift/Web3/Web3+HttpProvider.swift index a6411552d..1e75b242c 100755 --- a/Sources/web3swift/Web3/Web3+HttpProvider.swift +++ b/Sources/web3swift/Web3/Web3+HttpProvider.swift @@ -33,13 +33,21 @@ public class Web3HttpProvider: Web3Provider { if let net = net { network = net } else { - var urlRequest = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData) - urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") - urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") - urlRequest.httpMethod = APIRequest.getNetwork.call - urlRequest.httpBody = APIRequest.getNetwork.encodedBody - let response: APIResponse = try await APIRequest.send(uRLRequest: urlRequest, with: session) - self.network = Networks.fromInt(response.result) + /// chain id could be a hex string or an int value. + let response: String = try await APIRequest.send(APIRequest.getNetwork.call, parameters: [], with: self).result + let result: UInt + if response.hasHexPrefix() { + guard let num = BigUInt(response, radix: 16) else { + throw Web3Error.processingError(desc: "Get network succeeded but can't be parsed to a valid chain id.") + } + result = UInt(num) + } else { + guard let num = UInt(response) else { + throw Web3Error.processingError(desc: "Get network succeeded but can't be parsed to a valid chain id.") + } + result = num + } + self.network = Networks.fromInt(result) } attachedKeystoreManager = manager } 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/ABIDecoderSliceTests.swift b/Tests/web3swiftTests/localTests/ABIDecoderSliceTests.swift index a3fbeb43c..3e7f077d8 100644 --- a/Tests/web3swiftTests/localTests/ABIDecoderSliceTests.swift +++ b/Tests/web3swiftTests/localTests/ABIDecoderSliceTests.swift @@ -27,7 +27,7 @@ final class ABIDecoderSliceTests: XCTestCase { while startIndex < data.count { let slice = data[startIndex ..< startIndex + answerSize] startIndex += answerSize - guard let bigInt = balanceofMethod.decodeReturnData(slice)["0"] as? BigUInt else { + guard let bigInt = try balanceofMethod.decodeReturnData(slice)["0"] as? BigUInt else { throw Web3Error.processingError(desc: "Can not decode returned parameters") } let value = Utilities.formatToPrecision(bigInt, units: .wei) @@ -52,9 +52,7 @@ final class ABIDecoderSliceTests: XCTestCase { XCTAssertEqual(methods.count, 3) /// Act - guard let decodedData = multiCall2Contract.decodeReturnData("aggregate", data: data) else { - throw Web3Error.processingError(desc: "Can not decode returned parameters") - } + let decodedData = try multiCall2Contract.decodeReturnData("aggregate", data: data) guard let returnData = decodedData["returnData"] as? [Data] else { throw Web3Error.dataError @@ -63,7 +61,7 @@ final class ABIDecoderSliceTests: XCTestCase { XCTAssertEqual(returnData.count, 3) for item in methods.enumerated() { - XCTAssertNotNil(item.element.decodeReturnData(returnData[item.offset])["0"]) + XCTAssertNotNil(try item.element.decodeReturnData(returnData[item.offset])["0"]) } } @@ -74,9 +72,7 @@ final class ABIDecoderSliceTests: XCTestCase { let erc20_balanceof = try EthereumContract(Web3.Utils.erc20ABI).methods["balanceOf"]!.first! /// Act - guard let decodedData = contract.decodeReturnData("tryAggregate", data: data) else { - throw Web3Error.processingError(desc: "Can not decode returned parameters") - } + let decodedData = try contract.decodeReturnData("tryAggregate", data: data) guard let returnData = decodedData["returnData"] as? [[Any]] else { throw Web3Error.dataError @@ -84,7 +80,7 @@ final class ABIDecoderSliceTests: XCTestCase { var resultArray = [BigUInt]() for i in 0..<2 { guard let data = returnData[i][1] as? Data, - let balance = erc20_balanceof.decodeReturnData(data)["0"] as? BigUInt else { + let balance = try? erc20_balanceof.decodeReturnData(data)["0"] as? BigUInt else { resultArray.append(0) continue } diff --git a/Tests/web3swiftTests/localTests/ABIElementErrorDecodingTest.swift b/Tests/web3swiftTests/localTests/ABIElementErrorDecodingTest.swift index 181337d19..d0aa1b9d8 100644 --- a/Tests/web3swiftTests/localTests/ABIElementErrorDecodingTest.swift +++ b/Tests/web3swiftTests/localTests/ABIElementErrorDecodingTest.swift @@ -65,123 +65,140 @@ class ABIElementErrorDecodingTest: XCTestCase { XCTAssertTrue(emptyFunction.decodeErrorResponse(Data()) == nil) } - func testDecodeEmptyErrorOnOneOutputFunction() { - guard let errorData = oneOutputFunction.decodeErrorResponse(Data()) else { - XCTFail("Empty Data must be decoded as a `revert()` or `require(false)` call if function used to decode it has at least one output parameter.") - return + /// `require(expression)` and `revert()` without a message return 0 bytes, + /// we can noly catch an error when function has a return value + func testDecodeEmptyErrorOnOneOutputFunction() throws { + let contract = try EthereumContract(abi: [.function(emptyFunction)]) + do { + try contract.decodeReturnData(emptyFunction.signature, data: Data()) + } catch { + XCTFail() } - XCTAssertEqual(errorData["_success"] as? Bool, false) - XCTAssertNotNil(errorData["_failureReason"] as? String) - - let decodedOutput = oneOutputFunction.decodeReturnData(Data()) - - XCTAssertEqual(errorData["_success"] as? Bool, decodedOutput["_success"] as? Bool) - XCTAssertEqual(errorData["_failureReason"] as? String, decodedOutput["_failureReason"] as? String) + let contract2 = try EthereumContract(abi: [.function(oneOutputFunction)]) + do { + try contract2.decodeReturnData(oneOutputFunction.signature, data: Data()) + XCTFail() + } catch { + print(error) + } } /// Data is decoded as a call of `revert` or `require` with a message no matter the number of outputs configured in the ``ABI/Element/Function``. /// `revert(message)` and `require(false,message)`return at least 128 bytes. We cannot differentiate between `require` or `revert`. - func testDecodeDefaultErrorWithMessage() { + func testDecodeDefaultErrorWithMessage() throws { /// 08c379a0 - Error(string) function selector /// 0000000000000000000000000000000000000000000000000000000000000020 - Data offset /// 000000000000000000000000000000000000000000000000000000000000001a - Message length /// 4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 - Message + 0 bytes padding /// 0000... - some more 0 bytes padding to make the number of bytes match 32 bytes chunks - let errorResponse = Data.fromHex("08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a4e6f7420656e6f7567682045746865722070726f76696465642e00000000000000000000000000000000000000000000000000000000000000000000")! - guard let errorData = emptyFunction.decodeErrorResponse(errorResponse) else { - XCTFail("Data must be decoded as a `revert(\"Not enough Ether provided.\")` or `require(false, \"Not enough Ether provided.\")` but decoding failed completely.") - return + let errorResponse = Data.fromHex("08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a4e6f7420656e6f7567682045746865722070726f76696465642e0000000000000000000000000000000000000000000000000000000000000000000000000000")! + let contract = try EthereumContract(abi: [.function(emptyFunction)]) + + do { + try contract.decodeReturnData(emptyFunction.signature, data: errorResponse) + XCTFail("decode function should throw an error") + } catch Web3Error.revert(_, let reason) { + XCTAssertEqual(reason, "Not enough Ether provided.") } - XCTAssertEqual(errorData["_success"] as? Bool, false) - XCTAssertEqual(errorData["_abortedByRevertOrRequire"] as? Bool, true) - XCTAssertEqual(errorData["_errorMessage"] as? String, "Not enough Ether provided.") - XCTAssertNotNil(errorData["_failureReason"] as? String) - - let decodedOutput = oneOutputFunction.decodeReturnData(errorResponse) - - XCTAssertEqual(errorData["_success"] as? Bool, decodedOutput["_success"] as? Bool) - XCTAssertEqual(errorData["_failureReason"] as? String, decodedOutput["_failureReason"] as? String) - XCTAssertEqual(errorData["_abortedByRevertOrRequire"] as? Bool, decodedOutput["_abortedByRevertOrRequire"] as? Bool) - XCTAssertEqual(errorData["_errorMessage"] as? String, decodedOutput["_errorMessage"] as? String) - XCTAssertEqual(decodedOutput["_errorMessage"] as? String, "Not enough Ether provided.") + XCTAssertEqual(EthError.decodeStringError(errorResponse[4...]), "Not enough Ether provided.") } - /// Data is decoded as a call of `revert Unauthorized()`. Decoded only if custom error ABI is given. - func testDecodeRevertWithCustomError() { + /// Data is decoded as a call of `revert Unauthorized()` + func testDecodeRevertWithCustomError() throws { /// 82b42900 - Unauthorized() function selector /// 00000000000000000000000000000000000000000000000000000000 - padding bytes - let errorResponse = Data.fromHex("82b4290000000000000000000000000000000000000000000000000000000000")! - let errors: [String: EthError] = ["82b42900": .init(name: "Unauthorized", inputs: [])] - guard let errorData = emptyFunction.decodeErrorResponse(errorResponse, errors: errors) else { - XCTFail("Data must be decoded as a `revert(\"Not enough Ether provided.\")` or `require(false, \"Not enough Ether provided.\")` but decoding failed completely.") - return + let errorResponse = Data.fromHex("82b429000000000000000000000000000000000000000000000000000000000000000000")! + let error = ABI.Element.EthError(name: "Unauthorized", inputs: []) + let contract = try EthereumContract(abi: [.function(emptyFunction), .error(error)] ) + + do { + try contract.decodeReturnData(emptyFunction.signature, data: errorResponse) + XCTFail("decode function should throw an error") + } catch Web3Error.revertCustom(let signature, let args) { + XCTAssertEqual(signature, "Unauthorized()") + XCTAssertTrue(args.isEmpty) } - XCTAssertEqual(errorData["_success"] as? Bool, false) - XCTAssertEqual(errorData["_abortedByRevertOrRequire"] as? Bool, true) - XCTAssertEqual(errorData["_error"] as? String, "Unauthorized()") - - let decodedOutput = oneOutputFunction.decodeReturnData(errorResponse, errors: errors) - - XCTAssertEqual(errorData["_success"] as? Bool, decodedOutput["_success"] as? Bool) - XCTAssertEqual(errorData["_abortedByRevertOrRequire"] as? Bool, decodedOutput["_abortedByRevertOrRequire"] as? Bool) - XCTAssertEqual(errorData["_error"] as? String, decodedOutput["_error"] as? String) + guard let decoded = error.decodeEthError(errorResponse[4...]) else { + XCTFail("decode response failed.") + return + } + XCTAssertTrue(decoded.isEmpty) } - /// Data is decoded as a call of `revert Unauthorized()`. Decoded only if custom error ABI is given. + /// Data is decoded as a call of `revert Unauthorized(bool)`. /// Trying to decode as `Unauthorized(string)`. Must fail. - func testDecodeRevertWithCustomErrorFailed() { - /// 82b42900 - Unauthorized() function selector + func testDecodeRevertWithCustomErrorFailed() throws { + /// 5caef992 - Unauthorized(bool) function selector /// 00000000000000000000000000000000000000000000000000000000 - padding bytes - let errorResponse = Data.fromHex("82b4290000000000000000000000000000000000000000000000000000000000")! - let errors: [String: EthError] = ["82b42900": .init(name: "Unauthorized", inputs: [.init(name: "", type: .string)])] - guard let errorData = emptyFunction.decodeErrorResponse(errorResponse, errors: errors) else { - XCTFail("Data must be decoded as a `revert(\"Not enough Ether provided.\")` or `require(false, \"Not enough Ether provided.\")` but decoding failed completely.") - return + let errorResponse = Data.fromHex("5caef9920000000000000000000000000000000000000000000000000000000000000000")! + let error = ABI.Element.EthError(name: "Unauthorized", inputs: [.init(name: "", type: .bool)]) + let contract = try EthereumContract(abi: [.function(oneOutputFunction), .error(error)] ) + + do { + try contract.decodeReturnData(oneOutputFunction.signature, data: errorResponse) + XCTFail("decode function should throw an error") + } catch Web3Error.revertCustom(let signature, let args) { + XCTAssertEqual(signature, "Unauthorized(bool)") + XCTAssertEqual(args["0"] as? Bool, false) } - XCTAssertEqual(errorData["_success"] as? Bool, false) - XCTAssertEqual(errorData["_abortedByRevertOrRequire"] as? Bool, true) - XCTAssertEqual(errorData["_error"] as? String, "Unauthorized(string)") - XCTAssertEqual(errorData["_parsingError"] as? String, "Data matches Unauthorized(string) but failed to be decoded.") - - let decodedOutput = oneOutputFunction.decodeReturnData(errorResponse, errors: errors) - - XCTAssertEqual(errorData["_success"] as? Bool, decodedOutput["_success"] as? Bool) - XCTAssertEqual(errorData["_abortedByRevertOrRequire"] as? Bool, decodedOutput["_abortedByRevertOrRequire"] as? Bool) - XCTAssertEqual(errorData["_error"] as? String, decodedOutput["_error"] as? String) - XCTAssertEqual(errorData["_parsingError"] as? String, decodedOutput["_parsingError"] as? String) + guard let decoded = error.decodeEthError(errorResponse[4...]) else { + XCTFail("decode response failed.") + return + } + XCTAssertEqual(decoded["0"] as? Bool, false) } /// Data is decoded as a call of `revert Unauthorized("Reason")`. Decoded only if custom error ABI is given. /// The custom error argument must be extractable by index and name if the name is available. - func testDecodeRevertWithCustomErrorWithArguments() { + func testDecodeRevertWithCustomErrorWithArguments() throws { /// 973d02cb - `Unauthorized(string)` function selector /// 0000000000000000000000000000000000000000000000000000000000000020 - data offset /// 0000000000000000000000000000000000000000000000000000000000000006 - first custom argument length /// 526561736f6e0000000000000000000000000000000000000000000000000000 - first custom argument bytes + 0 bytes padding /// 0000... - some more 0 bytes padding to make the number of bytes match 32 bytes chunks - let errorResponse = Data.fromHex("973d02cb00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006526561736f6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")! - let errors: [String: EthError] = ["973d02cb": .init(name: "Unauthorized", inputs: [.init(name: "message_arg", type: .string)])] - guard let errorData = emptyFunction.decodeErrorResponse(errorResponse, errors: errors) else { - XCTFail("Data must be decoded as a `revert(\"Not enough Ether provided.\")` or `require(false, \"Not enough Ether provided.\")` but decoding failed completely.") - return + let errorResponse = Data.fromHex("973d02cb00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006526561736f6e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")! + let error = ABI.Element.EthError(name: "Unauthorized", inputs: [.init(name: "message_arg", type: .string)]) + let contract = try EthereumContract(abi: [.function(emptyFunction), .error(error)]) + + do { + try contract.decodeReturnData(emptyFunction.signature, data: errorResponse) + XCTFail("decode function should throw an error") + } catch Web3Error.revertCustom(let signature, let args) { + XCTAssertEqual(signature, "Unauthorized(string)") + XCTAssertEqual(args["0"] as? String, "Reason") + XCTAssertEqual(args["message_arg"] as? String, "Reason") } - XCTAssertEqual(errorData["_success"] as? Bool, false) - XCTAssertEqual(errorData["_abortedByRevertOrRequire"] as? Bool, true) - XCTAssertEqual(errorData["_error"] as? String, "Unauthorized(string message_arg)") - XCTAssertEqual(errorData["0"] as? String, "Reason") - XCTAssertEqual(errorData["0"] as? String, errorData["message_arg"] as? String) + guard let decoded = error.decodeEthError(errorResponse[4...]) else { + XCTFail("decode response failed.") + return + } + XCTAssertEqual(decoded["0"] as? String, "Reason") + XCTAssertEqual(decoded["message_arg"] as? String, "Reason") + } - let decodedOutput = oneOutputFunction.decodeReturnData(errorResponse, errors: errors) + /// Data is decoded as a panic exception is generated. + /// Example: + /// ``` solidity + /// function panicError() public { + /// assert(false); + /// } + /// ``` + func testDecodePanicError() throws { + let errorResponse = Data(hex: "4e487b710000000000000000000000000000000000000000000000000000000000000001") + let contract = try EthereumContract(abi: [.function(emptyFunction)]) + + do { + try contract.decodeReturnData(emptyFunction.signature, data: errorResponse) + } catch Web3Error.revert(let message, let code) { + XCTAssertTrue(message.contains("reverted with panic code 0x01")) + XCTAssertEqual(code, "0x01") + } - XCTAssertEqual(errorData["_success"] as? Bool, decodedOutput["_success"] as? Bool) - XCTAssertEqual(errorData["_abortedByRevertOrRequire"] as? Bool, decodedOutput["_abortedByRevertOrRequire"] as? Bool) - XCTAssertEqual(errorData["_error"] as? String, decodedOutput["_error"] as? String) - XCTAssertEqual(errorData["0"] as? String, decodedOutput["0"] as? String) - XCTAssertEqual(errorData["message_arg"] as? String, decodedOutput["message_arg"] as? String) + XCTAssertEqual(EthError.decodePanicError(errorResponse[4...]), 1) } } diff --git a/Tests/web3swiftTests/localTests/ABIElementsTests.swift b/Tests/web3swiftTests/localTests/ABIElementsTests.swift new file mode 100644 index 000000000..a94fa3d9d --- /dev/null +++ b/Tests/web3swiftTests/localTests/ABIElementsTests.swift @@ -0,0 +1,29 @@ +// +// ABIElementsTests.swift +// +// +// Created by JeneaVranceanu on 09.01.2024. +// + +import Foundation +import XCTest +@testable import web3swift +@testable import Web3Core + +class ABIElementsTests: XCTestCase { + + func testABIElementFunction() { + let test1Function = ABI.Element.Function(name: "Test1", + inputs: [], + outputs: [], + constant: true, + payable: false) + + XCTAssertEqual(test1Function.name, "Test1") + XCTAssertEqual(test1Function.selector, String("Test1()".sha3(.keccak256).prefix(8))) + XCTAssertEqual(test1Function.selectorEncoded, + Data.fromHex("Test1()".sha3(.keccak256))?.prefix(4)) + + } + +} diff --git a/Tests/web3swiftTests/localTests/AdvancedABIv2Tests.swift b/Tests/web3swiftTests/localTests/AdvancedABIv2Tests.swift index 066a1b7a8..e3e9f8cc1 100755 --- a/Tests/web3swiftTests/localTests/AdvancedABIv2Tests.swift +++ b/Tests/web3swiftTests/localTests/AdvancedABIv2Tests.swift @@ -17,20 +17,7 @@ class AdvancedABIv2Tests: LocalTestCase { let bytecode = Data.fromHex("6080604052341561000f57600080fd5b610cb18061001e6000396000f30060806040526004361061006d576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063189533931461007257806338163ff51461009b57806388be6c65146100c4578063e7c1a47c146100ed578063f376e01314610116575b600080fd5b341561007d57600080fd5b61008561013f565b6040516100929190610a9f565b60405180910390f35b34156100a657600080fd5b6100ae610220565b6040516100bb9190610a7d565b60405180910390f35b34156100cf57600080fd5b6100d76102c5565b6040516100e49190610ae3565b60405180910390f35b34156100f857600080fd5b610100610350565b60405161010d9190610ac1565b60405180910390f35b341561012157600080fd5b610129610399565b6040516101369190610b05565b60405180910390f35b6060600260405190808252806020026020018201604052801561017657816020015b60608152602001906001900390816101615790505b5090506040805190810160405280600581526020017f48656c6c6f0000000000000000000000000000000000000000000000000000008152508160008151811015156101be57fe5b906020019060200201819052506040805190810160405280600581526020017f576f726c6400000000000000000000000000000000000000000000000000000081525081600181518110151561021057fe5b9060200190602002018190525090565b610228610546565b6040805190810160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525081600060028110151561026d57fe5b60200201819052506040805190810160405280600581526020017f576f726c640000000000000000000000000000000000000000000000000000008152508160016002811015156102ba57fe5b602002018190525090565b6060600260405190808252806020026020018201604052801561030257816020015b6102ef61056d565b8152602001906001900390816102e75790505b50905061030d610399565b81600081518110151561031c57fe5b90602001906020020181905250610331610399565b81600181518110151561034057fe5b9060200190602002018190525090565b6103586105a9565b610360610399565b81600060028110151561036f57fe5b602002018190525061037f610399565b81600160028110151561038e57fe5b602002018190525090565b6103a16105d8565b60606103ab610614565b6103b3610546565b60036040519080825280602002602001820160405280156103e35781602001602082028038833980820191505090505b50925060008360008151811015156103f757fe5b9060200190602002018181525050600183600181518110151561041657fe5b9060200190602002018181525050600283600281518110151561043557fe5b90602001906020020181815250506040805190810160405280600081526020016001815250915060408051908101604052806040805190810160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525081526020016040805190810160405280600581526020017f576f726c64000000000000000000000000000000000000000000000000000000815250815250905060a060405190810160405280600181526020016040805190810160405280600b81526020017f48656c6c6f20776f726c64000000000000000000000000000000000000000000815250815260200183815260200184815260200182815250935083935050505090565b60408051908101604052806002905b60608152602001906001900390816105555790505090565b60e060405190810160405280600081526020016060815260200161058f610636565b8152602001606081526020016105a3610658565b81525090565b6101c0604051908101604052806002905b6105c261056d565b8152602001906001900390816105ba5790505090565b60e06040519081016040528060008152602001606081526020016105fa610636565b81526020016060815260200161060e610658565b81525090565b6040805190810160405280600290602082028038833980820191505090505090565b6040805190810160405280600290602082028038833980820191505090505090565b60408051908101604052806002905b60608152602001906001900390816106675790505090565b600061068a82610b81565b8360208202850161069a85610b31565b60005b848110156106d35783830388526106b5838351610930565b92506106c082610bdb565b915060208801975060018101905061069d565b508196508694505050505092915050565b60006106ef82610b76565b836020820285016106ff85610b27565b60005b8481101561073857838303885261071a838351610930565b925061072582610bce565b9150602088019750600181019050610702565b508196508694505050505092915050565b600061075482610b8c565b8084526020840193508360208202850161076d85610b3b565b60005b848110156107a6578383038852610788838351610930565b925061079382610be8565b9150602088019750600181019050610770565b508196508694505050505092915050565b60006107c282610b97565b836020820285016107d285610b48565b60005b8481101561080b5783830388526107ed8383516109ea565b92506107f882610bf5565b91506020880197506001810190506107d5565b508196508694505050505092915050565b600061082782610ba2565b8084526020840193508360208202850161084085610b52565b60005b8481101561087957838303885261085b8383516109ea565b925061086682610c02565b9150602088019750600181019050610843565b508196508694505050505092915050565b61089381610bad565b61089c82610b5f565b60005b828110156108ce576108b2858351610a6e565b6108bb82610c0f565b915060208501945060018101905061089f565b5050505050565b60006108e082610bb8565b8084526020840193506108f283610b69565b60005b8281101561092457610908868351610a6e565b61091182610c1c565b91506020860195506001810190506108f5565b50849250505092915050565b600061093b82610bc3565b80845261094f816020860160208601610c33565b61095881610c66565b602085010191505092915050565b600060c08301600083015161097e6000860182610a6e565b50602083015184820360208601526109968282610930565b91505060408301516109ab604086018261088a565b50606083015184820360808601526109c382826108d5565b915050608083015184820360a08601526109dd82826106e4565b9150508091505092915050565b600060c083016000830151610a026000860182610a6e565b5060208301518482036020860152610a1a8282610930565b9150506040830151610a2f604086018261088a565b5060608301518482036080860152610a4782826108d5565b915050608083015184820360a0860152610a6182826106e4565b9150508091505092915050565b610a7781610c29565b82525050565b60006020820190508181036000830152610a97818461067f565b905092915050565b60006020820190508181036000830152610ab98184610749565b905092915050565b60006020820190508181036000830152610adb81846107b7565b905092915050565b60006020820190508181036000830152610afd818461081c565b905092915050565b60006020820190508181036000830152610b1f8184610966565b905092915050565b6000819050919050565b6000819050919050565b6000602082019050919050565b6000819050919050565b6000602082019050919050565b6000819050919050565b6000602082019050919050565b600060029050919050565b600060029050919050565b600081519050919050565b600060029050919050565b600081519050919050565b600060029050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000819050919050565b60005b83811015610c51578082015181840152602081019050610c36565b83811115610c60576000848401525b50505050565b6000601f19601f83011690509190505600a265627a7a72305820fdaf8ce6fe282a46498c8066d5b5ac382b69969eee38e1ad03c0d501b27f65366c6578706572696d656e74616cf50037")! let web3 = try await Web3.new(LocalTestCase.url) - let allAddresses = try await web3.eth.ownedAccounts() - var contract = web3.contract(abiString, at: nil, abiVersion: 2)! - - // MARK: Writing Data flow - let deployTx = contract.prepareDeploy(bytecode: bytecode)! - deployTx.transaction.from = allAddresses[0] - // MARK: Sending Data flow - let policies = Policies(gasLimitPolicy: .manual(3000000)) - let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) - let txHash = result.hash.stripHexPrefix() - - Thread.sleep(forTimeInterval: 1.0) - - let receipt = try await web3.eth.transactionReceipt(Data.fromHex(txHash)!) + let receipt = try await deployContract(bytecode: bytecode, abiString: abiString) switch receipt.status { case .notYetProcessed: @@ -39,7 +26,7 @@ class AdvancedABIv2Tests: LocalTestCase { break } - contract = web3.contract(abiString, at: receipt.contractAddress, abiVersion: 2)! + let contract = web3.contract(abiString, at: receipt.contractAddress, abiVersion: 2)! // MARK: Read data from ABI flow // MARK: - Encoding ABI Data flow let tx = contract.createReadOperation("testSingle") @@ -51,20 +38,7 @@ class AdvancedABIv2Tests: LocalTestCase { let bytecode = Data.fromHex("6080604052341561000f57600080fd5b610cb18061001e6000396000f30060806040526004361061006d576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063189533931461007257806338163ff51461009b57806388be6c65146100c4578063e7c1a47c146100ed578063f376e01314610116575b600080fd5b341561007d57600080fd5b61008561013f565b6040516100929190610a9f565b60405180910390f35b34156100a657600080fd5b6100ae610220565b6040516100bb9190610a7d565b60405180910390f35b34156100cf57600080fd5b6100d76102c5565b6040516100e49190610ae3565b60405180910390f35b34156100f857600080fd5b610100610350565b60405161010d9190610ac1565b60405180910390f35b341561012157600080fd5b610129610399565b6040516101369190610b05565b60405180910390f35b6060600260405190808252806020026020018201604052801561017657816020015b60608152602001906001900390816101615790505b5090506040805190810160405280600581526020017f48656c6c6f0000000000000000000000000000000000000000000000000000008152508160008151811015156101be57fe5b906020019060200201819052506040805190810160405280600581526020017f576f726c6400000000000000000000000000000000000000000000000000000081525081600181518110151561021057fe5b9060200190602002018190525090565b610228610546565b6040805190810160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525081600060028110151561026d57fe5b60200201819052506040805190810160405280600581526020017f576f726c640000000000000000000000000000000000000000000000000000008152508160016002811015156102ba57fe5b602002018190525090565b6060600260405190808252806020026020018201604052801561030257816020015b6102ef61056d565b8152602001906001900390816102e75790505b50905061030d610399565b81600081518110151561031c57fe5b90602001906020020181905250610331610399565b81600181518110151561034057fe5b9060200190602002018190525090565b6103586105a9565b610360610399565b81600060028110151561036f57fe5b602002018190525061037f610399565b81600160028110151561038e57fe5b602002018190525090565b6103a16105d8565b60606103ab610614565b6103b3610546565b60036040519080825280602002602001820160405280156103e35781602001602082028038833980820191505090505b50925060008360008151811015156103f757fe5b9060200190602002018181525050600183600181518110151561041657fe5b9060200190602002018181525050600283600281518110151561043557fe5b90602001906020020181815250506040805190810160405280600081526020016001815250915060408051908101604052806040805190810160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525081526020016040805190810160405280600581526020017f576f726c64000000000000000000000000000000000000000000000000000000815250815250905060a060405190810160405280600181526020016040805190810160405280600b81526020017f48656c6c6f20776f726c64000000000000000000000000000000000000000000815250815260200183815260200184815260200182815250935083935050505090565b60408051908101604052806002905b60608152602001906001900390816105555790505090565b60e060405190810160405280600081526020016060815260200161058f610636565b8152602001606081526020016105a3610658565b81525090565b6101c0604051908101604052806002905b6105c261056d565b8152602001906001900390816105ba5790505090565b60e06040519081016040528060008152602001606081526020016105fa610636565b81526020016060815260200161060e610658565b81525090565b6040805190810160405280600290602082028038833980820191505090505090565b6040805190810160405280600290602082028038833980820191505090505090565b60408051908101604052806002905b60608152602001906001900390816106675790505090565b600061068a82610b81565b8360208202850161069a85610b31565b60005b848110156106d35783830388526106b5838351610930565b92506106c082610bdb565b915060208801975060018101905061069d565b508196508694505050505092915050565b60006106ef82610b76565b836020820285016106ff85610b27565b60005b8481101561073857838303885261071a838351610930565b925061072582610bce565b9150602088019750600181019050610702565b508196508694505050505092915050565b600061075482610b8c565b8084526020840193508360208202850161076d85610b3b565b60005b848110156107a6578383038852610788838351610930565b925061079382610be8565b9150602088019750600181019050610770565b508196508694505050505092915050565b60006107c282610b97565b836020820285016107d285610b48565b60005b8481101561080b5783830388526107ed8383516109ea565b92506107f882610bf5565b91506020880197506001810190506107d5565b508196508694505050505092915050565b600061082782610ba2565b8084526020840193508360208202850161084085610b52565b60005b8481101561087957838303885261085b8383516109ea565b925061086682610c02565b9150602088019750600181019050610843565b508196508694505050505092915050565b61089381610bad565b61089c82610b5f565b60005b828110156108ce576108b2858351610a6e565b6108bb82610c0f565b915060208501945060018101905061089f565b5050505050565b60006108e082610bb8565b8084526020840193506108f283610b69565b60005b8281101561092457610908868351610a6e565b61091182610c1c565b91506020860195506001810190506108f5565b50849250505092915050565b600061093b82610bc3565b80845261094f816020860160208601610c33565b61095881610c66565b602085010191505092915050565b600060c08301600083015161097e6000860182610a6e565b50602083015184820360208601526109968282610930565b91505060408301516109ab604086018261088a565b50606083015184820360808601526109c382826108d5565b915050608083015184820360a08601526109dd82826106e4565b9150508091505092915050565b600060c083016000830151610a026000860182610a6e565b5060208301518482036020860152610a1a8282610930565b9150506040830151610a2f604086018261088a565b5060608301518482036080860152610a4782826108d5565b915050608083015184820360a0860152610a6182826106e4565b9150508091505092915050565b610a7781610c29565b82525050565b60006020820190508181036000830152610a97818461067f565b905092915050565b60006020820190508181036000830152610ab98184610749565b905092915050565b60006020820190508181036000830152610adb81846107b7565b905092915050565b60006020820190508181036000830152610afd818461081c565b905092915050565b60006020820190508181036000830152610b1f8184610966565b905092915050565b6000819050919050565b6000819050919050565b6000602082019050919050565b6000819050919050565b6000602082019050919050565b6000819050919050565b6000602082019050919050565b600060029050919050565b600060029050919050565b600081519050919050565b600060029050919050565b600081519050919050565b600060029050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000819050919050565b60005b83811015610c51578082015181840152602081019050610c36565b83811115610c60576000848401525b50505050565b6000601f19601f83011690509190505600a265627a7a72305820fdaf8ce6fe282a46498c8066d5b5ac382b69969eee38e1ad03c0d501b27f65366c6578706572696d656e74616cf50037")! let web3 = try await Web3.new(LocalTestCase.url) - let allAddresses = try await web3.eth.ownedAccounts() - var contract = web3.contract(abiString, at: nil, abiVersion: 2)! - - let parameters: [Any] = [] - // MARK: Writing Data flow - let deployTx = contract.prepareDeploy(bytecode: bytecode, parameters: parameters)! - deployTx.transaction.from = allAddresses[0] - let policies = Policies(gasLimitPolicy: .manual(3000000)) - let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) - let txHash = result.hash.stripHexPrefix() - - Thread.sleep(forTimeInterval: 1.0) - - let receipt = try await web3.eth.transactionReceipt(Data.fromHex(txHash)!) + let receipt = try await deployContract(bytecode: bytecode, abiString: abiString) switch receipt.status { case .notYetProcessed: @@ -73,7 +47,7 @@ class AdvancedABIv2Tests: LocalTestCase { break } - contract = web3.contract(abiString, at: receipt.contractAddress, abiVersion: 2)! + let contract = web3.contract(abiString, at: receipt.contractAddress, abiVersion: 2)! // MARK: Read data from ABI flow // MARK: - Encoding ABI Data flow let tx = contract.createReadOperation("testStaticArray") @@ -85,20 +59,7 @@ class AdvancedABIv2Tests: LocalTestCase { let bytecode = Data.fromHex("6080604052341561000f57600080fd5b610cb18061001e6000396000f30060806040526004361061006d576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063189533931461007257806338163ff51461009b57806388be6c65146100c4578063e7c1a47c146100ed578063f376e01314610116575b600080fd5b341561007d57600080fd5b61008561013f565b6040516100929190610a9f565b60405180910390f35b34156100a657600080fd5b6100ae610220565b6040516100bb9190610a7d565b60405180910390f35b34156100cf57600080fd5b6100d76102c5565b6040516100e49190610ae3565b60405180910390f35b34156100f857600080fd5b610100610350565b60405161010d9190610ac1565b60405180910390f35b341561012157600080fd5b610129610399565b6040516101369190610b05565b60405180910390f35b6060600260405190808252806020026020018201604052801561017657816020015b60608152602001906001900390816101615790505b5090506040805190810160405280600581526020017f48656c6c6f0000000000000000000000000000000000000000000000000000008152508160008151811015156101be57fe5b906020019060200201819052506040805190810160405280600581526020017f576f726c6400000000000000000000000000000000000000000000000000000081525081600181518110151561021057fe5b9060200190602002018190525090565b610228610546565b6040805190810160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525081600060028110151561026d57fe5b60200201819052506040805190810160405280600581526020017f576f726c640000000000000000000000000000000000000000000000000000008152508160016002811015156102ba57fe5b602002018190525090565b6060600260405190808252806020026020018201604052801561030257816020015b6102ef61056d565b8152602001906001900390816102e75790505b50905061030d610399565b81600081518110151561031c57fe5b90602001906020020181905250610331610399565b81600181518110151561034057fe5b9060200190602002018190525090565b6103586105a9565b610360610399565b81600060028110151561036f57fe5b602002018190525061037f610399565b81600160028110151561038e57fe5b602002018190525090565b6103a16105d8565b60606103ab610614565b6103b3610546565b60036040519080825280602002602001820160405280156103e35781602001602082028038833980820191505090505b50925060008360008151811015156103f757fe5b9060200190602002018181525050600183600181518110151561041657fe5b9060200190602002018181525050600283600281518110151561043557fe5b90602001906020020181815250506040805190810160405280600081526020016001815250915060408051908101604052806040805190810160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525081526020016040805190810160405280600581526020017f576f726c64000000000000000000000000000000000000000000000000000000815250815250905060a060405190810160405280600181526020016040805190810160405280600b81526020017f48656c6c6f20776f726c64000000000000000000000000000000000000000000815250815260200183815260200184815260200182815250935083935050505090565b60408051908101604052806002905b60608152602001906001900390816105555790505090565b60e060405190810160405280600081526020016060815260200161058f610636565b8152602001606081526020016105a3610658565b81525090565b6101c0604051908101604052806002905b6105c261056d565b8152602001906001900390816105ba5790505090565b60e06040519081016040528060008152602001606081526020016105fa610636565b81526020016060815260200161060e610658565b81525090565b6040805190810160405280600290602082028038833980820191505090505090565b6040805190810160405280600290602082028038833980820191505090505090565b60408051908101604052806002905b60608152602001906001900390816106675790505090565b600061068a82610b81565b8360208202850161069a85610b31565b60005b848110156106d35783830388526106b5838351610930565b92506106c082610bdb565b915060208801975060018101905061069d565b508196508694505050505092915050565b60006106ef82610b76565b836020820285016106ff85610b27565b60005b8481101561073857838303885261071a838351610930565b925061072582610bce565b9150602088019750600181019050610702565b508196508694505050505092915050565b600061075482610b8c565b8084526020840193508360208202850161076d85610b3b565b60005b848110156107a6578383038852610788838351610930565b925061079382610be8565b9150602088019750600181019050610770565b508196508694505050505092915050565b60006107c282610b97565b836020820285016107d285610b48565b60005b8481101561080b5783830388526107ed8383516109ea565b92506107f882610bf5565b91506020880197506001810190506107d5565b508196508694505050505092915050565b600061082782610ba2565b8084526020840193508360208202850161084085610b52565b60005b8481101561087957838303885261085b8383516109ea565b925061086682610c02565b9150602088019750600181019050610843565b508196508694505050505092915050565b61089381610bad565b61089c82610b5f565b60005b828110156108ce576108b2858351610a6e565b6108bb82610c0f565b915060208501945060018101905061089f565b5050505050565b60006108e082610bb8565b8084526020840193506108f283610b69565b60005b8281101561092457610908868351610a6e565b61091182610c1c565b91506020860195506001810190506108f5565b50849250505092915050565b600061093b82610bc3565b80845261094f816020860160208601610c33565b61095881610c66565b602085010191505092915050565b600060c08301600083015161097e6000860182610a6e565b50602083015184820360208601526109968282610930565b91505060408301516109ab604086018261088a565b50606083015184820360808601526109c382826108d5565b915050608083015184820360a08601526109dd82826106e4565b9150508091505092915050565b600060c083016000830151610a026000860182610a6e565b5060208301518482036020860152610a1a8282610930565b9150506040830151610a2f604086018261088a565b5060608301518482036080860152610a4782826108d5565b915050608083015184820360a0860152610a6182826106e4565b9150508091505092915050565b610a7781610c29565b82525050565b60006020820190508181036000830152610a97818461067f565b905092915050565b60006020820190508181036000830152610ab98184610749565b905092915050565b60006020820190508181036000830152610adb81846107b7565b905092915050565b60006020820190508181036000830152610afd818461081c565b905092915050565b60006020820190508181036000830152610b1f8184610966565b905092915050565b6000819050919050565b6000819050919050565b6000602082019050919050565b6000819050919050565b6000602082019050919050565b6000819050919050565b6000602082019050919050565b600060029050919050565b600060029050919050565b600081519050919050565b600060029050919050565b600081519050919050565b600060029050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000819050919050565b60005b83811015610c51578082015181840152602081019050610c36565b83811115610c60576000848401525b50505050565b6000601f19601f83011690509190505600a265627a7a72305820fdaf8ce6fe282a46498c8066d5b5ac382b69969eee38e1ad03c0d501b27f65366c6578706572696d656e74616cf50037")! let web3 = try await Web3.new(LocalTestCase.url) - let allAddresses = try await web3.eth.ownedAccounts() - var contract = web3.contract(abiString, at: nil, abiVersion: 2)! - - let parameters: [Any] = [] - // MARK: Writing Data flow - let deployTx = contract.prepareDeploy(bytecode: bytecode, parameters: parameters)! - deployTx.transaction.from = allAddresses[0] - let policies = Policies(gasLimitPolicy: .manual(3000000)) - let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) - let txHash = result.hash.stripHexPrefix() - - Thread.sleep(forTimeInterval: 1.0) - - let receipt = try await web3.eth.transactionReceipt(Data.fromHex(txHash)!) + let receipt = try await deployContract(bytecode: bytecode, abiString: abiString) switch receipt.status { case .notYetProcessed: @@ -107,7 +68,7 @@ class AdvancedABIv2Tests: LocalTestCase { break } - contract = web3.contract(abiString, at: receipt.contractAddress, abiVersion: 2)! + let contract = web3.contract(abiString, at: receipt.contractAddress, abiVersion: 2)! let tx = contract.createReadOperation("testDynArray") _ = try await tx!.callContractMethod() @@ -118,20 +79,7 @@ class AdvancedABIv2Tests: LocalTestCase { let bytecode = Data.fromHex("6080604052341561000f57600080fd5b610cb18061001e6000396000f30060806040526004361061006d576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063189533931461007257806338163ff51461009b57806388be6c65146100c4578063e7c1a47c146100ed578063f376e01314610116575b600080fd5b341561007d57600080fd5b61008561013f565b6040516100929190610a9f565b60405180910390f35b34156100a657600080fd5b6100ae610220565b6040516100bb9190610a7d565b60405180910390f35b34156100cf57600080fd5b6100d76102c5565b6040516100e49190610ae3565b60405180910390f35b34156100f857600080fd5b610100610350565b60405161010d9190610ac1565b60405180910390f35b341561012157600080fd5b610129610399565b6040516101369190610b05565b60405180910390f35b6060600260405190808252806020026020018201604052801561017657816020015b60608152602001906001900390816101615790505b5090506040805190810160405280600581526020017f48656c6c6f0000000000000000000000000000000000000000000000000000008152508160008151811015156101be57fe5b906020019060200201819052506040805190810160405280600581526020017f576f726c6400000000000000000000000000000000000000000000000000000081525081600181518110151561021057fe5b9060200190602002018190525090565b610228610546565b6040805190810160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525081600060028110151561026d57fe5b60200201819052506040805190810160405280600581526020017f576f726c640000000000000000000000000000000000000000000000000000008152508160016002811015156102ba57fe5b602002018190525090565b6060600260405190808252806020026020018201604052801561030257816020015b6102ef61056d565b8152602001906001900390816102e75790505b50905061030d610399565b81600081518110151561031c57fe5b90602001906020020181905250610331610399565b81600181518110151561034057fe5b9060200190602002018190525090565b6103586105a9565b610360610399565b81600060028110151561036f57fe5b602002018190525061037f610399565b81600160028110151561038e57fe5b602002018190525090565b6103a16105d8565b60606103ab610614565b6103b3610546565b60036040519080825280602002602001820160405280156103e35781602001602082028038833980820191505090505b50925060008360008151811015156103f757fe5b9060200190602002018181525050600183600181518110151561041657fe5b9060200190602002018181525050600283600281518110151561043557fe5b90602001906020020181815250506040805190810160405280600081526020016001815250915060408051908101604052806040805190810160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525081526020016040805190810160405280600581526020017f576f726c64000000000000000000000000000000000000000000000000000000815250815250905060a060405190810160405280600181526020016040805190810160405280600b81526020017f48656c6c6f20776f726c64000000000000000000000000000000000000000000815250815260200183815260200184815260200182815250935083935050505090565b60408051908101604052806002905b60608152602001906001900390816105555790505090565b60e060405190810160405280600081526020016060815260200161058f610636565b8152602001606081526020016105a3610658565b81525090565b6101c0604051908101604052806002905b6105c261056d565b8152602001906001900390816105ba5790505090565b60e06040519081016040528060008152602001606081526020016105fa610636565b81526020016060815260200161060e610658565b81525090565b6040805190810160405280600290602082028038833980820191505090505090565b6040805190810160405280600290602082028038833980820191505090505090565b60408051908101604052806002905b60608152602001906001900390816106675790505090565b600061068a82610b81565b8360208202850161069a85610b31565b60005b848110156106d35783830388526106b5838351610930565b92506106c082610bdb565b915060208801975060018101905061069d565b508196508694505050505092915050565b60006106ef82610b76565b836020820285016106ff85610b27565b60005b8481101561073857838303885261071a838351610930565b925061072582610bce565b9150602088019750600181019050610702565b508196508694505050505092915050565b600061075482610b8c565b8084526020840193508360208202850161076d85610b3b565b60005b848110156107a6578383038852610788838351610930565b925061079382610be8565b9150602088019750600181019050610770565b508196508694505050505092915050565b60006107c282610b97565b836020820285016107d285610b48565b60005b8481101561080b5783830388526107ed8383516109ea565b92506107f882610bf5565b91506020880197506001810190506107d5565b508196508694505050505092915050565b600061082782610ba2565b8084526020840193508360208202850161084085610b52565b60005b8481101561087957838303885261085b8383516109ea565b925061086682610c02565b9150602088019750600181019050610843565b508196508694505050505092915050565b61089381610bad565b61089c82610b5f565b60005b828110156108ce576108b2858351610a6e565b6108bb82610c0f565b915060208501945060018101905061089f565b5050505050565b60006108e082610bb8565b8084526020840193506108f283610b69565b60005b8281101561092457610908868351610a6e565b61091182610c1c565b91506020860195506001810190506108f5565b50849250505092915050565b600061093b82610bc3565b80845261094f816020860160208601610c33565b61095881610c66565b602085010191505092915050565b600060c08301600083015161097e6000860182610a6e565b50602083015184820360208601526109968282610930565b91505060408301516109ab604086018261088a565b50606083015184820360808601526109c382826108d5565b915050608083015184820360a08601526109dd82826106e4565b9150508091505092915050565b600060c083016000830151610a026000860182610a6e565b5060208301518482036020860152610a1a8282610930565b9150506040830151610a2f604086018261088a565b5060608301518482036080860152610a4782826108d5565b915050608083015184820360a0860152610a6182826106e4565b9150508091505092915050565b610a7781610c29565b82525050565b60006020820190508181036000830152610a97818461067f565b905092915050565b60006020820190508181036000830152610ab98184610749565b905092915050565b60006020820190508181036000830152610adb81846107b7565b905092915050565b60006020820190508181036000830152610afd818461081c565b905092915050565b60006020820190508181036000830152610b1f8184610966565b905092915050565b6000819050919050565b6000819050919050565b6000602082019050919050565b6000819050919050565b6000602082019050919050565b6000819050919050565b6000602082019050919050565b600060029050919050565b600060029050919050565b600081519050919050565b600060029050919050565b600081519050919050565b600060029050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000819050919050565b60005b83811015610c51578082015181840152602081019050610c36565b83811115610c60576000848401525b50505050565b6000601f19601f83011690509190505600a265627a7a72305820fdaf8ce6fe282a46498c8066d5b5ac382b69969eee38e1ad03c0d501b27f65366c6578706572696d656e74616cf50037")! let web3 = try await Web3.new(LocalTestCase.url) - let allAddresses = try await web3.eth.ownedAccounts() - var contract = web3.contract(abiString, at: nil, abiVersion: 2)! - - let parameters: [Any] = [] - // MARK: Writing Data flow - let deployTx = contract.prepareDeploy(bytecode: bytecode, parameters: parameters)! - deployTx.transaction.from = allAddresses[0] - let policies = Policies(gasLimitPolicy: .manual(3000000)) - let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) - let txHash = result.hash.stripHexPrefix() - - Thread.sleep(forTimeInterval: 1.0) - - let receipt = try await web3.eth.transactionReceipt(Data.fromHex(txHash)!) + let receipt = try await deployContract(bytecode: bytecode, abiString: abiString) switch receipt.status { case .notYetProcessed: @@ -140,7 +88,7 @@ class AdvancedABIv2Tests: LocalTestCase { break } - contract = web3.contract(abiString, at: receipt.contractAddress, abiVersion: 2)! + let contract = web3.contract(abiString, at: receipt.contractAddress, abiVersion: 2)! // MARK: Read data from ABI flow // MARK: - Encoding ABI Data flow let tx = contract.createReadOperation("testDynOfDyn") @@ -152,20 +100,7 @@ class AdvancedABIv2Tests: LocalTestCase { let bytecode = Data.fromHex("6080604052341561000f57600080fd5b610cb18061001e6000396000f30060806040526004361061006d576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063189533931461007257806338163ff51461009b57806388be6c65146100c4578063e7c1a47c146100ed578063f376e01314610116575b600080fd5b341561007d57600080fd5b61008561013f565b6040516100929190610a9f565b60405180910390f35b34156100a657600080fd5b6100ae610220565b6040516100bb9190610a7d565b60405180910390f35b34156100cf57600080fd5b6100d76102c5565b6040516100e49190610ae3565b60405180910390f35b34156100f857600080fd5b610100610350565b60405161010d9190610ac1565b60405180910390f35b341561012157600080fd5b610129610399565b6040516101369190610b05565b60405180910390f35b6060600260405190808252806020026020018201604052801561017657816020015b60608152602001906001900390816101615790505b5090506040805190810160405280600581526020017f48656c6c6f0000000000000000000000000000000000000000000000000000008152508160008151811015156101be57fe5b906020019060200201819052506040805190810160405280600581526020017f576f726c6400000000000000000000000000000000000000000000000000000081525081600181518110151561021057fe5b9060200190602002018190525090565b610228610546565b6040805190810160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525081600060028110151561026d57fe5b60200201819052506040805190810160405280600581526020017f576f726c640000000000000000000000000000000000000000000000000000008152508160016002811015156102ba57fe5b602002018190525090565b6060600260405190808252806020026020018201604052801561030257816020015b6102ef61056d565b8152602001906001900390816102e75790505b50905061030d610399565b81600081518110151561031c57fe5b90602001906020020181905250610331610399565b81600181518110151561034057fe5b9060200190602002018190525090565b6103586105a9565b610360610399565b81600060028110151561036f57fe5b602002018190525061037f610399565b81600160028110151561038e57fe5b602002018190525090565b6103a16105d8565b60606103ab610614565b6103b3610546565b60036040519080825280602002602001820160405280156103e35781602001602082028038833980820191505090505b50925060008360008151811015156103f757fe5b9060200190602002018181525050600183600181518110151561041657fe5b9060200190602002018181525050600283600281518110151561043557fe5b90602001906020020181815250506040805190810160405280600081526020016001815250915060408051908101604052806040805190810160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525081526020016040805190810160405280600581526020017f576f726c64000000000000000000000000000000000000000000000000000000815250815250905060a060405190810160405280600181526020016040805190810160405280600b81526020017f48656c6c6f20776f726c64000000000000000000000000000000000000000000815250815260200183815260200184815260200182815250935083935050505090565b60408051908101604052806002905b60608152602001906001900390816105555790505090565b60e060405190810160405280600081526020016060815260200161058f610636565b8152602001606081526020016105a3610658565b81525090565b6101c0604051908101604052806002905b6105c261056d565b8152602001906001900390816105ba5790505090565b60e06040519081016040528060008152602001606081526020016105fa610636565b81526020016060815260200161060e610658565b81525090565b6040805190810160405280600290602082028038833980820191505090505090565b6040805190810160405280600290602082028038833980820191505090505090565b60408051908101604052806002905b60608152602001906001900390816106675790505090565b600061068a82610b81565b8360208202850161069a85610b31565b60005b848110156106d35783830388526106b5838351610930565b92506106c082610bdb565b915060208801975060018101905061069d565b508196508694505050505092915050565b60006106ef82610b76565b836020820285016106ff85610b27565b60005b8481101561073857838303885261071a838351610930565b925061072582610bce565b9150602088019750600181019050610702565b508196508694505050505092915050565b600061075482610b8c565b8084526020840193508360208202850161076d85610b3b565b60005b848110156107a6578383038852610788838351610930565b925061079382610be8565b9150602088019750600181019050610770565b508196508694505050505092915050565b60006107c282610b97565b836020820285016107d285610b48565b60005b8481101561080b5783830388526107ed8383516109ea565b92506107f882610bf5565b91506020880197506001810190506107d5565b508196508694505050505092915050565b600061082782610ba2565b8084526020840193508360208202850161084085610b52565b60005b8481101561087957838303885261085b8383516109ea565b925061086682610c02565b9150602088019750600181019050610843565b508196508694505050505092915050565b61089381610bad565b61089c82610b5f565b60005b828110156108ce576108b2858351610a6e565b6108bb82610c0f565b915060208501945060018101905061089f565b5050505050565b60006108e082610bb8565b8084526020840193506108f283610b69565b60005b8281101561092457610908868351610a6e565b61091182610c1c565b91506020860195506001810190506108f5565b50849250505092915050565b600061093b82610bc3565b80845261094f816020860160208601610c33565b61095881610c66565b602085010191505092915050565b600060c08301600083015161097e6000860182610a6e565b50602083015184820360208601526109968282610930565b91505060408301516109ab604086018261088a565b50606083015184820360808601526109c382826108d5565b915050608083015184820360a08601526109dd82826106e4565b9150508091505092915050565b600060c083016000830151610a026000860182610a6e565b5060208301518482036020860152610a1a8282610930565b9150506040830151610a2f604086018261088a565b5060608301518482036080860152610a4782826108d5565b915050608083015184820360a0860152610a6182826106e4565b9150508091505092915050565b610a7781610c29565b82525050565b60006020820190508181036000830152610a97818461067f565b905092915050565b60006020820190508181036000830152610ab98184610749565b905092915050565b60006020820190508181036000830152610adb81846107b7565b905092915050565b60006020820190508181036000830152610afd818461081c565b905092915050565b60006020820190508181036000830152610b1f8184610966565b905092915050565b6000819050919050565b6000819050919050565b6000602082019050919050565b6000819050919050565b6000602082019050919050565b6000819050919050565b6000602082019050919050565b600060029050919050565b600060029050919050565b600081519050919050565b600060029050919050565b600081519050919050565b600060029050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000602082019050919050565b6000819050919050565b60005b83811015610c51578082015181840152602081019050610c36565b83811115610c60576000848401525b50505050565b6000601f19601f83011690509190505600a265627a7a72305820fdaf8ce6fe282a46498c8066d5b5ac382b69969eee38e1ad03c0d501b27f65366c6578706572696d656e74616cf50037")! let web3 = try await Web3.new(LocalTestCase.url) - let allAddresses = try await web3.eth.ownedAccounts() - var contract = web3.contract(abiString, at: nil, abiVersion: 2)! - - let parameters: [Any] = [] - // MARK: Writing Data flow - let deployTx = contract.prepareDeploy(bytecode: bytecode, parameters: parameters)! - deployTx.transaction.from = allAddresses[0] - let policies = Policies(gasLimitPolicy: .manual(3000000)) - let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) - let txHash = result.hash.stripHexPrefix() - - Thread.sleep(forTimeInterval: 1.0) - - let receipt = try await web3.eth.transactionReceipt(Data.fromHex(txHash)!) + let receipt = try await deployContract(bytecode: bytecode, abiString: abiString) switch receipt.status { case .notYetProcessed: @@ -174,7 +109,7 @@ class AdvancedABIv2Tests: LocalTestCase { break } - contract = web3.contract(abiString, at: receipt.contractAddress, abiVersion: 2)! + let contract = web3.contract(abiString, at: receipt.contractAddress, abiVersion: 2)! // MARK: Read data from ABI flow // MARK: - Encoding ABI Data flow let tx = contract.createReadOperation("testStOfDyn") @@ -182,8 +117,10 @@ class AdvancedABIv2Tests: LocalTestCase { } func testEmptyArrayDecoding() async throws { + let bytecode = Data(hex: "608060405234801561001057600080fd5b5061027b806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063f2a75fe414610030575b600080fd5b61003861004e565b60405161004591906101e0565b60405180910390f35b60606000600267ffffffffffffffff811115610093577f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040519080825280602002602001820160405280156100c15781602001602082028036833780820191505090505b509050600181600081518110610100577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b602002602001018181525050600281600181518110610148577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b6020026020010181815250508091505090565b600061016783836101d1565b60208301905092915050565b600061017e82610212565b610188818561022a565b935061019383610202565b8060005b838110156101c45781516101ab888261015b565b97506101b68361021d565b925050600181019050610197565b5085935050505092915050565b6101da8161023b565b82525050565b600060208201905081810360008301526101fa8184610173565b905092915050565b6000819050602082019050919050565b600081519050919050565b6000602082019050919050565b600082825260208201905092915050565b600081905091905056fea2646970667358221220d1b52dfe3238df01604ccfbb6c6cee01edbaa74fcffd8a57c4041b0b19e6887664736f6c63430008040033") let abiString = "[{\"inputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"constant\":true,\"inputs\":[],\"name\":\"empty\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256[]\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]" - let contractAddress = EthereumAddress("0x200eb5ccda1c35b0f5bf82552fdd65a8aee98e79")! + let receipt = try await deployContract(bytecode: bytecode, abiString: abiString) + let contractAddress = receipt.contractAddress! let web3 = try await Web3.new(LocalTestCase.url) let contract = web3.contract(abiString, at: contractAddress, abiVersion: 2) XCTAssert(contract != nil) @@ -191,20 +128,31 @@ class AdvancedABIv2Tests: LocalTestCase { // MARK: - Encoding ABI Data flow let tx = contract?.createReadOperation("empty") XCTAssertNotNil(tx) - _ = try await tx!.callContractMethod() + let result = try await tx!.callContractMethod() + XCTAssertEqual(result.count, 1) + XCTAssertEqual((result["0"] as? Array)?[0], 1) + XCTAssertEqual((result["0"] as? Array)?[1], 2) } func testUserCase() async throws { + let bytecode = Data(hex: "0x608060405234801561001057600080fd5b50610484806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063a16e94bf1461003b578063a46b5b6b14610059575b600080fd5b610043610075565b60405161005091906102a6565b60405180910390f35b610073600480360381019061006e919061022c565b610107565b005b6060600080546100849061037c565b80601f01602080910402602001604051908101604052809291908181526020018280546100b09061037c565b80156100fd5780601f106100d2576101008083540402835291602001916100fd565b820191906000526020600020905b8154815290600101906020018083116100e057829003601f168201915b5050505050905090565b806000908051906020019061011d929190610121565b5050565b82805461012d9061037c565b90600052602060002090601f01602090048101928261014f5760008555610196565b82601f1061016857805160ff1916838001178555610196565b82800160010185558215610196579182015b8281111561019557825182559160200191906001019061017a565b5b5090506101a391906101a7565b5090565b5b808211156101c05760008160009055506001016101a8565b5090565b60006101d76101d2846102ed565b6102c8565b9050828152602081018484840111156101ef57600080fd5b6101fa84828561033a565b509392505050565b600082601f83011261021357600080fd5b81356102238482602086016101c4565b91505092915050565b60006020828403121561023e57600080fd5b600082013567ffffffffffffffff81111561025857600080fd5b61026484828501610202565b91505092915050565b60006102788261031e565b6102828185610329565b9350610292818560208601610349565b61029b8161043d565b840191505092915050565b600060208201905081810360008301526102c0818461026d565b905092915050565b60006102d26102e3565b90506102de82826103ae565b919050565b6000604051905090565b600067ffffffffffffffff8211156103085761030761040e565b5b6103118261043d565b9050602081019050919050565b600081519050919050565b600082825260208201905092915050565b82818337600083830152505050565b60005b8381101561036757808201518184015260208101905061034c565b83811115610376576000848401525b50505050565b6000600282049050600182168061039457607f821691505b602082108114156103a8576103a76103df565b5b50919050565b6103b78261043d565b810181811067ffffffffffffffff821117156103d6576103d561040e565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6000601f19601f830116905091905056fea2646970667358221220264496be069122017e0e6bd1c3b06e335df83ac7d058c0063a81e9227786693564736f6c63430008040033") let abiString = "[{\"constant\":true,\"inputs\":[],\"name\":\"getFlagData\",\"outputs\":[{\"name\":\"data\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"data\",\"type\":\"string\"}],\"name\":\"setFlagData\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]" - let contractAddress = EthereumAddress("0x811411e3cdfd4750cdd3552feb3b89a46ddb612e") - let web3 = try await Web3.new(LocalTestCase.url) + let receipt = try await deployContract(bytecode: bytecode, abiString: abiString) + let contractAddress = receipt.contractAddress! + let web3 = try await Web3(provider: Web3HttpProvider(url: LocalTestCase.url, network: nil)) let contract = web3.contract(abiString, at: contractAddress, abiVersion: 2) XCTAssert(contract != nil) + let allAddresses = try await web3.eth.ownedAccounts() + let balance = try await web3.eth.getBalance(for: allAddresses[0]) + let writeOperation = contract?.createWriteOperation("setFlagData", parameters: ["abcdefg"]) + writeOperation?.transaction.from = allAddresses[0] + try await writeOperation?.writeToChain(password: "web3swift", policies: Policies(gasLimitPolicy: .manual(3000000)), sendRaw: false) // MARK: Read data from ABI flow // MARK: - Encoding ABI Data flow let tx = contract?.createReadOperation("getFlagData") XCTAssertNotNil(tx) - _ = try await tx!.callContractMethod() + let result = try await tx!.callContractMethod() + XCTAssertEqual(result["0"] as? String, "abcdefg") } } diff --git a/Tests/web3swiftTests/localTests/BIP39Tests.swift b/Tests/web3swiftTests/localTests/BIP39Tests.swift index e9ccf6410..55b053c9d 100644 --- a/Tests/web3swiftTests/localTests/BIP39Tests.swift +++ b/Tests/web3swiftTests/localTests/BIP39Tests.swift @@ -151,6 +151,7 @@ final class BIP39Tests: XCTestCase { XCTAssert(keystore1?.addresses?.first == keystore2?.addresses?.first) } + /// It's expected for the entropy bits count to be [128, 256] and (bits mod 32) must return 0. func testWrongBitsOfEntropyMustThrow() throws { XCTAssertThrowsError(try BIP39.generateMnemonics(entropy: 127)) XCTAssertThrowsError(try BIP39.generateMnemonics(entropy: 255)) @@ -166,4 +167,33 @@ final class BIP39Tests: XCTestCase { XCTAssertFalse(try BIP39.generateMnemonics(entropy: 256).isEmpty) } + func testBip39CorrectWordsCount() throws { + XCTAssertEqual(try BIP39.generateMnemonics(entropy: 128).count, 12) + XCTAssertEqual(try BIP39.generateMnemonics(entropy: 160).count, 15) + XCTAssertEqual(try BIP39.generateMnemonics(entropy: 192).count, 18) + XCTAssertEqual(try BIP39.generateMnemonics(entropy: 224).count, 21) + XCTAssertEqual(try BIP39.generateMnemonics(entropy: 256).count, 24) + } + + func testAllLanguageMnemonics() throws { + for language in BIP39Language.allCases { + let mnemonicPhrase = try BIP39.generateMnemonics(entropy: 128, language: language) + for word in mnemonicPhrase { + guard language.words.contains(word) else { + XCTFail("Given word is not contained in the list of words of selected language available for mnemonics generation: \(word); \(language)") + return + } + } + } + } + + func testBip39MnemonicSeparatorUse() throws { + for language in BIP39Language.allCases { + guard let mnemonicPhrase = try BIP39.generateMnemonics(bitsOfEntropy: 128, language: language) else { + XCTFail("Failed to generate BIP39 mnemonics phrase with 128 bits of entropy using language: \(language)") + return + } + XCTAssertEqual(mnemonicPhrase.split(whereSeparator: { $0 == language.separatorCharacter }).count, 12) + } + } } 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") + } +} diff --git a/Tests/web3swiftTests/localTests/EventTests.swift b/Tests/web3swiftTests/localTests/EventTests.swift new file mode 100644 index 000000000..7c1b4187b --- /dev/null +++ b/Tests/web3swiftTests/localTests/EventTests.swift @@ -0,0 +1,90 @@ +// +// EventTests.swift +// +// +// Created by liugang zhang on 2023/8/24. +// + +import XCTest +import Web3Core +import BigInt + +@testable import web3swift + +class EventTests: XCTestCase { + + /// Solidity event allows up to 3 indexed field, this is just for test. + let testEvent = """ + [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"string","name":"a","type":"string"},{"indexed":true,"internalType":"bool","name":"b","type":"bool"},{"indexed":true,"internalType":"bytes","name":"c","type":"bytes"},{"indexed":true,"internalType":"uint256","name":"d","type":"uint256"}],"name":"UserOperationEvent","type":"event"}] + """ + + func testEncodeTopicToJSON() throws { + let encoder = JSONEncoder() + let t1: [EventFilterParameters.Topic] = [] + let t2: [EventFilterParameters.Topic] = [.string(nil)] + let t3: [EventFilterParameters.Topic] = [.strings([.string(nil), .string("1")])] + let t4: [EventFilterParameters.Topic] = [.strings([nil, .string("1")])] + XCTAssertNoThrow(try encoder.encode(t1)) + XCTAssertNoThrow(try encoder.encode(t2)) + XCTAssertNoThrow(try encoder.encode(t3)) + XCTAssertNoThrow(try encoder.encode(t4)) + + let topics: [EventFilterParameters.Topic] = [ + .string("1"), + .strings([ + .string("2"), + .string("3"), + ] + )] + let encoded = try encoder.encode(topics) + let json = try JSONSerialization.jsonObject(with: encoded) + XCTAssertEqual(json as? NSArray, ["1", ["2", "3"]]) + } + + func testEncodeLogs() throws { + let contract = try EthereumContract(testEvent) + let topic = contract.events["UserOperationEvent"]!.topic + let logs = contract.events["UserOperationEvent"]!.encodeParameters( + [ + "0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042", + "0x581074D2d9e50913eB37665b07CAFa9bFFdd1640", + "hello,world", + true, + "0x02c16c07e1c68d50", + nil + ] + ) + XCTAssertEqual(logs.count, 6) + + XCTAssertTrue(logs[0] == topic.toHexString().addHexPrefix()) + XCTAssertTrue(logs[1] == "0x2c16c07e1c68d502e9c7ad05f0402b365671a0e6517cb807b2de4edd95657042") + XCTAssertTrue(logs[2] == "0x000000000000000000000000581074d2d9e50913eb37665b07cafa9bffdd1640") + XCTAssertTrue(logs[3] == "0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7") + XCTAssertTrue(logs[4] == "0x0000000000000000000000000000000000000000000000000000000000000001") + XCTAssertTrue(logs[5] == "0x56f5a6cba57d26b32db8dc756fda960dcd3687770a300575a5f8107591eff63f") + } + + func testEncodeTopic() throws { + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .string, indexed: true), value: "hello,world") == "0xab036729af8b8f9b610af4e11b14fa30c348f40c2c230cce92ef6ef37726fee7") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .address, indexed: true), value: "0x003e36550908907c2a2da960fd19a419b9a774b7") == "0x000000000000000000000000003e36550908907c2a2da960fd19a419b9a774b7") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .address, indexed: true), value: EthereumAddress("0x003e36550908907c2a2da960fd19a419b9a774b7")!) == "0x000000000000000000000000003e36550908907c2a2da960fd19a419b9a774b7") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bool, indexed: true), value: true) == "0x0000000000000000000000000000000000000000000000000000000000000001") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bool, indexed: true), value: false) == "0x0000000000000000000000000000000000000000000000000000000000000000") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .uint(bits: 256), indexed: true), value: BigUInt("dbe20a", radix: 16)!) == "0x0000000000000000000000000000000000000000000000000000000000dbe20a") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .uint(bits: 256), indexed: true), value: "dbe20a") == "0x0000000000000000000000000000000000000000000000000000000000dbe20a") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .int(bits: 32), indexed: true), value: 100) == "0x0000000000000000000000000000000000000000000000000000000000000064") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .dynamicBytes, indexed: true), value: Data(hex: "6761766f66796f726b")) == "0xe0859ceea0a2fd2474deef2b2183f10f4c741ebba702e9a07d337522c0af55fb") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bytes(length: 32), indexed: true), value: Data(hex: "6761766f66796f726b")) == "0x00000000000000000000000000000000000000000000006761766f66796f726b") + XCTAssertTrue(ABI.Element.Event.encodeTopic(input: .init(name: "", type: .bytes(length: 32), indexed: true), value: "0x6761766f66796f726b") == "0x00000000000000000000000000000000000000000000006761766f66796f726b") + } +} + +private func ==(lhs: EventFilterParameters.Topic?, rhs: String?) -> Bool { + if let lhs = lhs, case .string(let string) = lhs { + return string == rhs + } + if lhs == nil && rhs == nil { + return true + } + return false +} diff --git a/Tests/web3swiftTests/localTests/LocalTestCase.swift b/Tests/web3swiftTests/localTests/LocalTestCase.swift index 7b79589c4..707fbde93 100644 --- a/Tests/web3swiftTests/localTests/LocalTestCase.swift +++ b/Tests/web3swiftTests/localTests/LocalTestCase.swift @@ -31,4 +31,21 @@ class LocalTestCase: XCTestCase { _ = try! await writeTX.writeToChain(password: "", policies: policies, sendRaw: false) } } + + func deployContract(bytecode: Data, abiString: String) async throws -> TransactionReceipt { + let web3 = try await Web3.new(LocalTestCase.url) + let allAddresses = try await web3.eth.ownedAccounts() + var contract = web3.contract(abiString, at: nil, abiVersion: 2)! + + let parameters: [Any] = [] + // MARK: Writing Data flow + let deployTx = contract.prepareDeploy(bytecode: bytecode, parameters: parameters)! + deployTx.transaction.from = allAddresses[0] + let policies = Policies(gasLimitPolicy: .manual(3000000)) + let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) + let txHash = result.hash.stripHexPrefix() + Thread.sleep(forTimeInterval: 1.0) + let receipt = try await web3.eth.transactionReceipt(Data.fromHex(txHash)!) + return receipt + } } diff --git a/Tests/web3swiftTests/localTests/String+ExtensionTests.swift.swift b/Tests/web3swiftTests/localTests/String+ExtensionTests.swift.swift new file mode 100644 index 000000000..1a6686178 --- /dev/null +++ b/Tests/web3swiftTests/localTests/String+ExtensionTests.swift.swift @@ -0,0 +1,31 @@ +// +// String+ExtensionTests.swift +// +// Created by JeneaVranceanu on 26.11.2023. +// + +import Foundation +import XCTest + +class StringExtensionsTest: XCTestCase { + + func testIsHex() throws { + XCTAssertTrue("0x".isHex) + XCTAssertTrue("0xF".isHex) + XCTAssertTrue("F".isHex) + XCTAssertTrue("0xFF".isHex) + XCTAssertTrue("0x0123456789abcdefABCDEF".isHex) + XCTAssertTrue("0123456789abcdefABCDEF".isHex) + XCTAssertTrue("0123456789abcdefABCDEF ".isHex) + XCTAssertTrue(" 0123456789abcdefABCDEF ".isHex) + XCTAssertTrue(" 0123456789abcdefABCDEF".isHex) + + XCTAssertFalse("".isHex) + XCTAssertFalse("-".isHex) + XCTAssertFalse("xyz".isHex) + XCTAssertFalse("0xCAFEQ".isHex) + XCTAssertFalse("R0123456789abcdefABCDEF ".isHex) + XCTAssertFalse(" R0123456789abcdefABCDEF ".isHex) + } + +} diff --git a/Tests/web3swiftTests/localTests/UncategorizedTests.swift b/Tests/web3swiftTests/localTests/UncategorizedTests.swift index f47ccdeec..fa6bcf4d7 100755 --- a/Tests/web3swiftTests/localTests/UncategorizedTests.swift +++ b/Tests/web3swiftTests/localTests/UncategorizedTests.swift @@ -10,7 +10,7 @@ import BigInt @testable import Web3Core @testable import web3swift -class UncategorizedTests: XCTestCase { +class UncategorizedTests: LocalTestCase { func testBitFunctions () throws { let data = Data([0xf0, 0x02, 0x03]) let firstBit = data.bitsInRange(0, 1) @@ -51,6 +51,17 @@ class UncategorizedTests: XCTestCase { XCTAssert(biguint == BigUInt("126978086000000000")) } + func testStringSplit() { + XCTAssertEqual("abcdefgh".split(every: 3), ["abc", "def", "gh"]) + XCTAssertEqual("abcdefgh".split(every: 3, backwards: true), ["ab", "cde", "fgh"]) + + XCTAssertEqual("abcdefgh".split(every: 10), ["abcdefgh"]) + XCTAssertEqual("".split(every: 3), []) + + XCTAssertEqual("abcdefgh".split(every: 1), ["a", "b", "c", "d", "e", "f", "g", "h"]) + XCTAssertEqual("abcdefgh".split(every: 1, backwards: true), ["a", "b", "c", "d", "e", "f", "g", "h"]) // should be the same as from the front + } + func testBloom() throws { let positive = [ "testtest", @@ -109,24 +120,28 @@ class UncategorizedTests: XCTestCase { // } func testPublicMappingsAccess() async throws { + let bytecode = Data(hex: "0x608060405234801561001057600080fd5b5061023d806100206000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063365b98b2146100465780635e79ab6014610076578063bff1f9e1146100a6575b600080fd5b610060600480360381019061005b919061014a565b6100c4565b60405161006d9190610182565b60405180910390f35b610090600480360381019061008b9190610121565b6100ce565b60405161009d9190610182565b60405180910390f35b6100ae6100ee565b6040516100bb9190610182565b60405180910390f35b6000819050919050565b60008173ffffffffffffffffffffffffffffffffffffffff169050919050565b60006064905090565b600081359050610106816101d9565b92915050565b60008135905061011b816101f0565b92915050565b60006020828403121561013357600080fd5b6000610141848285016100f7565b91505092915050565b60006020828403121561015c57600080fd5b600061016a8482850161010c565b91505092915050565b61017c816101cf565b82525050565b60006020820190506101976000830184610173565b92915050565b60006101a8826101af565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b6101e28161019d565b81146101ed57600080fd5b50565b6101f9816101cf565b811461020457600080fd5b5056fea26469706673582212207373b0db986284793522a82bff7bf03e30323defa94e6d25f7141e7d63e1ee0564736f6c63430008040033") let jsonString = "[{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"users\",\"outputs\":[{\"name\":\"name\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"userDeviceCount\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalUsers\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]" + let receipt = try await deployContract(bytecode: bytecode, abiString: jsonString) let web3 = try await Web3.new(LocalTestCase.url) - guard let addr = EthereumAddress("0xdef61132a0c1259464b19e4590e33666aae38574") else {return XCTFail()} - let contract = web3.contract(jsonString, at: addr, abiVersion: 2) - XCTAssert(contract != nil) - let allMethods = contract!.contract.allMethods + guard let addr = receipt.contractAddress else {return XCTFail()} + let contract = web3.contract(jsonString, at: receipt.contractAddress!, abiVersion: 2) + let userDeviceCount = try await contract! - .createReadOperation("userDeviceCount", parameters: [addr])? + .createReadOperation("userDeviceCount", parameters: [addr])! .callContractMethod() let totalUsers = try await contract! - .createReadOperation("totalUsers")? + .createReadOperation("totalUsers")! .callContractMethod() let user = try await contract! - .createReadOperation("users", parameters: [0])? + .createReadOperation("users", parameters: [0])! .callContractMethod() + XCTAssertEqual((userDeviceCount["0"] as? BigUInt)?.hexString.lowercased(), addr.address.lowercased()) + XCTAssertEqual(totalUsers["0"] as? BigUInt, 100) + XCTAssertEqual(user["0"] as? BigUInt, 0) } func testBloomFilterPerformance() throws { diff --git a/Tests/web3swiftTests/localTests/UtilitiesTests.swift b/Tests/web3swiftTests/localTests/UtilitiesTests.swift index 4780805b0..057d99be2 100644 --- a/Tests/web3swiftTests/localTests/UtilitiesTests.swift +++ b/Tests/web3swiftTests/localTests/UtilitiesTests.swift @@ -82,4 +82,9 @@ class UtilitiesTests: XCTestCase { address = Utilities.publicToAddress(Data.fromHex("0x0852972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2")!)?.address XCTAssertEqual(address, nil) } + + func testStringIsHex() { + XCTAssertTrue("1234567890abcdef".isHex) + XCTAssertFalse("ghijklmn".isHex) + } } diff --git a/Tests/web3swiftTests/remoteTests/DecodeRemoteErrorTests.swift b/Tests/web3swiftTests/remoteTests/DecodeRemoteErrorTests.swift new file mode 100644 index 000000000..ed5a48bf2 --- /dev/null +++ b/Tests/web3swiftTests/remoteTests/DecodeRemoteErrorTests.swift @@ -0,0 +1,53 @@ +// +// DecodeRemoteErrorTests.swift +// +// Created by liugang zhang on 2023/8/25. +// + +import XCTest +import Web3Core + +@testable import web3swift + +final class DecodeRemoteErrorTests: XCTestCase { + + let entryPoint = EthereumAddress("0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789")! + let factory = EthereumAddress("0x9406Cc6185a346906296840746125a0E44976454")! + let address = EthereumAddress("0x581074D2d9e50913eB37665b07CAFa9bFFdd1640")! + + func testDecodeRemoteFunc() async throws { + let web3 = try await Web3.InfuraMainnetWeb3(accessToken: Constants.infuraToken) + + let entryABI = try EthereumContract(abi: [ + .error(.init(name: "SenderAddressResult", + inputs: [.init(name: "sender", type: .address)])), + .function(.init(name: "getSenderAddress", + inputs: [.init(name: "initCode", type: .dynamicBytes)], + outputs: [], + constant: false, + payable: false)) + ], at: entryPoint) + + let factoryABI = try EthereumContract(abi: [ + .function(.init(name: "createAccount", + inputs: [ + .init(name: "owner", type: .address), + .init(name: "salt", type: .uint(bits: 256)) + ], + outputs: [], + constant: false, + payable: false)) + ]) + + let initCode = factory.addressData + factoryABI.method("createAccount", parameters: [address, 0], extraData: nil)! + + do { + try await entryABI.callStatic("getSenderAddress", parameters: [initCode], provider: web3.provider) + XCTFail() + } catch Web3Error.revertCustom(let signature, let args) { + XCTAssertEqual(signature, "SenderAddressResult(address)") + XCTAssertEqual((args["sender"] as? EthereumAddress)?.address, "0x9CF91286f22a1b799770fB5De0E66f3C4cc165d1") + XCTAssertEqual((args["0"] as? EthereumAddress)?.address, "0x9CF91286f22a1b799770fB5De0E66f3C4cc165d1") + } + } +} diff --git a/Tests/web3swiftTests/remoteTests/EventFilterTests.swift b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift new file mode 100644 index 000000000..99a29683b --- /dev/null +++ b/Tests/web3swiftTests/remoteTests/EventFilterTests.swift @@ -0,0 +1,46 @@ +// +// EventFilterTests.swift +// +// +// Created by liugang zhang on 2023/8/24. +// + +import XCTest +import Web3Core +import BigInt +import CryptoSwift +@testable import web3swift + +class EventFilerTests: XCTestCase { + + /// This test tx can be found at here: + /// https://etherscan.io/tx/0x1a1daac5b3158f16399baec9abba2c8a4b4b7ffea5992490079b6bfc4ce70004 + func testErc20Transfer() async throws { + let web3 = try await Web3.InfuraMainnetWeb3(accessToken: Constants.infuraToken) + let address = EthereumAddress("0xdac17f958d2ee523a2206206994597c13d831ec7")! + let erc20 = ERC20(web3: web3, provider: web3.provider, address: address) + + let topics = erc20.contract.contract.event("Transfer", parameters: [ + "0x003e36550908907c2a2da960fd19a419b9a774b7" + ]) + + let parameters = EventFilterParameters(fromBlock: .exact(17983395), toBlock: .exact(17983395), address: [address], topics: topics) + let result = try await web3.eth.getLogs(eventFilter: parameters) + + XCTAssertEqual(result.count, 1) + + let log = result.first! + XCTAssertEqual(log.address.address.lowercased(), "0xdac17f958d2ee523a2206206994597c13d831ec7") + XCTAssertEqual(log.transactionHash.toHexString().lowercased(), "1a1daac5b3158f16399baec9abba2c8a4b4b7ffea5992490079b6bfc4ce70004") + + let logTopics = log.topics.map { $0.toHexString() } + topics.compactMap { t -> String? in + if let t = t, case EventFilterParameters.Topic.string(let topic) = t { + return topic + } + return nil + }.forEach { t in + XCTAssertTrue(logTopics.contains(t.stripHexPrefix())) + } + } +} diff --git a/Tests/web3swiftTests/remoteTests/InfuraTests.swift b/Tests/web3swiftTests/remoteTests/InfuraTests.swift index fec9289ef..da1cdf412 100755 --- a/Tests/web3swiftTests/remoteTests/InfuraTests.swift +++ b/Tests/web3swiftTests/remoteTests/InfuraTests.swift @@ -10,7 +10,6 @@ import Web3Core // MARK: Works only with network connection class InfuraTests: XCTestCase { - func testGetBalance() async throws { let web3 = try await Web3.InfuraMainnetWeb3(accessToken: Constants.infuraToken) let address = EthereumAddress("0xd61b5ca425F8C8775882d4defefC68A6979DBbce")! diff --git a/Tests/web3swiftTests/remoteTests/Web3HttpProviderTests.swift b/Tests/web3swiftTests/remoteTests/Web3HttpProviderTests.swift new file mode 100644 index 000000000..b2a5e144b --- /dev/null +++ b/Tests/web3swiftTests/remoteTests/Web3HttpProviderTests.swift @@ -0,0 +1,33 @@ +// +// Web3HttpProviderTests.swift +// +// +// Created by liugang zhang on 2023/9/2. +// + +import XCTest +import Web3Core + +@testable import web3swift + +final class Web3HttpProviderTests: XCTestCase { + + /// if one of these rpc server lose efficacy, find a substitution from https://chainlist.org/ + func testGetNetwork() async throws { + let requestURLstring = "https://" + Networks.Mainnet.name + Constants.infuraHttpScheme + Constants.infuraToken + var web3 = try await Web3HttpProvider(url: URL(string: requestURLstring)!, network: nil) + XCTAssertEqual(web3.network?.chainID, 1) + + web3 = try await Web3HttpProvider(url: URL(string: "https://arbitrum-one.publicnode.com")!, network: nil) + XCTAssertEqual(web3.network?.chainID, 42161) + + web3 = try await Web3HttpProvider(url: URL(string: "https://rpc.ankr.com/bsc")!, network: nil) + XCTAssertEqual(web3.network?.chainID, 56) + + web3 = try await Web3HttpProvider(url: URL(string: "https://rpc-mainnet.maticvigil.com/")!, network: nil) + XCTAssertEqual(web3.network?.chainID, 137) + + web3 = try await Web3HttpProvider(url: URL(string: "https://optimism.gateway.tenderly.co")!, network: nil) + XCTAssertEqual(web3.network?.chainID, 10) + } +}