diff --git a/Sources/Core/Contract/ContractProtocol.swift b/Sources/Core/Contract/ContractProtocol.swift index 495b032f7..9302cbc8e 100755 --- a/Sources/Core/Contract/ContractProtocol.swift +++ b/Sources/Core/Contract/ContractProtocol.swift @@ -173,6 +173,11 @@ public protocol ContractProtocol { /// - Returns: `true` if event is possibly present, `false` if definitely not present and `nil` if event with given name /// is not part of the ``EthereumContract/abi``. func testBloomForEventPresence(eventName: String, bloom: EthereumBloomFilter) -> Bool? + + /// Given the transaction data searches for a match in ``ContractProtocol/methods``. + /// - Parameter data: encoded function call used in transaction data field. Must be at least 4 bytes long. + /// - Returns: function decoded from the ABI of this contract or `nil` if nothing was found. + func getFunctionCalled(_ data: Data) -> ABI.Element.Function? } // MARK: - Overloaded ContractProtocol's functions @@ -333,4 +338,9 @@ extension DefaultContractProtocol { guard let function = methods[methodSignature]?.first else { return nil } return function.decodeInputData(Data(data[4 ..< data.count])) } + + public func getFunctionCalled(_ data: Data) -> ABI.Element.Function? { + guard data.count >= 4 else { return nil } + return methods[data[0..<4].toHexString().addHexPrefix()]?.first + } } diff --git a/Sources/Core/EthereumABI/ABIElements.swift b/Sources/Core/EthereumABI/ABIElements.swift index ac111458b..21217af3d 100755 --- a/Sources/Core/EthereumABI/ABIElements.swift +++ b/Sources/Core/EthereumABI/ABIElements.swift @@ -77,7 +77,7 @@ public extension ABI { public let type: ParameterType public init(name: String, type: ParameterType) { - self.name = name + self.name = name.trim() self.type = type } } @@ -91,7 +91,7 @@ public extension ABI { public let payable: Bool public init(name: String?, inputs: [InOut], outputs: [InOut], constant: Bool, payable: Bool) { - self.name = name + self.name = name?.trim() self.inputs = inputs self.outputs = outputs self.constant = constant @@ -103,6 +103,7 @@ public extension ABI { public let inputs: [InOut] public let constant: Bool public let payable: Bool + public init(inputs: [InOut], constant: Bool, payable: Bool) { self.inputs = inputs self.constant = constant @@ -126,7 +127,7 @@ public extension ABI { public let anonymous: Bool public init(name: String, inputs: [Input], anonymous: Bool) { - self.name = name + self.name = name.trim() self.inputs = inputs self.anonymous = anonymous } @@ -137,7 +138,7 @@ public extension ABI { public let indexed: Bool public init(name: String, type: ParameterType, indexed: Bool) { - self.name = name + self.name = name.trim() self.type = type self.indexed = indexed } @@ -155,16 +156,16 @@ public extension ABI { /// Custom structured error type available since solidity 0.8.4 public struct EthError { public let name: String - public let inputs: [Input] + public let inputs: [InOut] - public struct Input { - public let name: String - public let type: ParameterType + /// e.g. `CustomError(uint32, address sender)` + public var errorDeclaration: String { + "\(name)(\(inputs.map { "\($0.type.abiRepresentation) \($0.name)".trim() }.joined(separator: ",")))" + } - public init(name: String, type: ParameterType) { - self.name = name - self.type = type - } + public init(name: String, inputs: [InOut] = []) { + self.name = name.trim() + self.inputs = inputs } } } @@ -202,7 +203,7 @@ extension ABI.Element.Function { /// Encode parameters of a given contract method /// - Parameter parameters: Parameters to pass to Ethereum contract - /// - Returns: Encoded data + /// - Returns: Encoded data public func encodeParameters(_ parameters: [AnyObject]) -> Data? { guard parameters.count == inputs.count, let data = ABIEncoder.encode(types: inputs, values: parameters) else { return nil } @@ -264,73 +265,168 @@ extension ABI.Element.Function { return Core.decodeInputData(rawData, methodEncoding: methodEncoding, inputs: inputs) } - public func decodeReturnData(_ data: Data) -> [String: Any]? { - // the response size greater than equal 100 bytes, when read function aborted by "require" statement. - // if "require" statement has no message argument, the response is empty (0 byte). - if data.bytes.count >= 100 { - let check00_31 = BigUInt("08C379A000000000000000000000000000000000000000000000000000000000", radix: 16)! - let check32_63 = BigUInt("0000002000000000000000000000000000000000000000000000000000000000", radix: 16)! - - // check data[00-31] and data[32-63] - if check00_31 == BigUInt(data[0...31]) && check32_63 == BigUInt(data[32...63]) { - // data.bytes[64-67] contains the length of require message - let len = (Int(data.bytes[64])<<24) | (Int(data.bytes[65])<<16) | (Int(data.bytes[66])<<8) | Int(data.bytes[67]) - - let message = String(bytes: data.bytes[68..<(68+len)], encoding: .utf8)! - - print("read function aborted by require statement: \(message)") - - var returnArray = [String: Any]() + /// Decodes data returned by a function call. Able to decode `revert(string)`, `revert CustomError(...)` and `require(expression, string)` calls. + /// - 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. + /// + /// Return cases: + /// - when no `outputs` declared and `data` is not an error response: + ///```swift + ///["_success": true] + ///``` + /// - when `outputs` declared and decoding completed successfully: + ///```swift + ///["_success": true, "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 + } - // set infomation - returnArray["_abortedByRequire"] = true - returnArray["_errorMessageFromRequire"] = message + guard !outputs.isEmpty else { + NSLog("Function doesn't have any output types to decode given data.") + return ["_success": true] + } - // set empty values - for i in 0 ..< outputs.count { - let name = "\(i)" - returnArray[name] = outputs[i].type.emptyValue - if outputs[i].name != "" { - returnArray[outputs[i].name] = outputs[i].type.emptyValue - } - } + 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."] + } - return returnArray + // 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."] + } + var returnArray: [String: Any] = ["_success": true] + for i in outputs.indices { + returnArray["\(i)"] = values[i] + if !outputs[i].name.isEmpty { + returnArray[outputs[i].name] = values[i] } } + return returnArray + } - var returnArray = [String: Any]() - - // the "require" statement with no message argument will be caught here - if data.count == 0 && outputs.count == 1 { - let name = "0" - let value = outputs[0].type.emptyValue - returnArray[name] = value - if outputs[0].name != "" { - returnArray[outputs[0].name] = value - } - } else { - guard outputs.count * 32 <= data.count else { return nil } - - var i = 0 - guard let values = ABIDecoder.decode(types: outputs, data: data) else { return nil } - for output in outputs { - let name = "\(i)" - returnArray[name] = values[i] - if output.name != "" { - returnArray[output.name] = values[i] - } - i = i + 1 - } - // set a flag to detect the request succeeded + /// Decodes `revert(string)`, `revert CustomError(...)` and `require(expression, string)` calls. + /// If `data` is empty and `outputs` are not empty it's considered that data is a result of `revert()` or `require(false)`. + /// - Parameters: + /// - data: returned function call data to decode; + /// - errors: optional known errors that could be thrown by the function you called. + /// - Returns: dictionary containing information about the error thrown by the function call. + /// + /// What could be returned: + /// - `nil` if data doesn't represent an error or it failed to be mapped to any of the `errors` or `Error(string)` types; + /// - `nil` is `data.isEmpty` and `outputs.isEmpty`; + /// - `data.isEmpty` and `!outputs.isEmpty`: + /// ```swift + /// ["_success": false, + /// "_failureReason": "Cannot decode empty data. X outputs are expected: [outputs_types]. Was this a result of en empty `require(false)` or `revert()` call?"] + /// ``` + /// - 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. + /// ...] + /// + /// /// or if custo error found but decoding failed + /// ["_success": false, + /// "_abortedByRevertOrRequire": true, + /// // "_error" can contain value like `MyCustomError(uint256, address senderAddress)` + /// "_error": error_name_and_types, + /// // "_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."] + /// ``` + 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. + if data.isEmpty && !outputs.isEmpty { + return ["_success": false, "_failureReason": "Cannot decode empty data. \(outputs.count) outputs are expected: \(outputs.map { $0.type.abiRepresentation }). Was this a result of en empty `require(false)` or `revert()` call?"] } - if returnArray.isEmpty { - return nil + /// Explanation of this condition: + /// When `revert(string)` or `require(false, string)` are called in soliditiy they produce + /// an error, specifically an instance of default `Error(string)` type. + /// 1) The total number of bytes returned are at least 100. + /// 2) The function selector for `Error(string)` is `08C379A0`; + /// 3) Data offset must be present. Hexadecimal value of `0000...0020` is 32 in decimal. Reasoning for `BigInt(...) == 32`. + /// 4) `messageLength` is used to determine where message bytes end to decode string correctly. + /// 5) The rest of the `data` must be 0 bytes or empty. + if data.bytes.count >= 100, + Data(data[0..<4]) == Data.fromHex("08C379A0"), + BigInt(data[4..<36]) == 32, + let messageLength = Int(Data(data[36..<68]).toHexString(), radix: 16), + let message = String(bytes: data.bytes[68..<(68+messageLength)], encoding: .utf8), + (68+messageLength == data.count || data.bytes[68+messageLength..= 4, + let errors = errors, + let customError = errors[data[0..<4].toHexString().stripHexPrefix()] { + var errorResponse: [String: Any] = ["_success": false, "_abortedByRevertOrRequire": true, "_error": customError.errorDeclaration] + + if (data.count > 32 && !customError.inputs.isEmpty), + let decodedInputs = ABIDecoder.decode(types: customError.inputs, data: Data(data[4.. ABI.Element.Receive { } private func parseError(abiRecord: ABI.Record) throws -> ABI.Element.EthError { - let inputs = try abiRecord.inputs?.map({ (input: ABI.Input) throws -> ABI.Element.EthError.Input in - let nativeInput = try input.parseForError() - return nativeInput - }) - let abiInputs = inputs ?? [] + let abiInputs = try abiRecord.inputs?.map({ input throws -> ABI.Element.InOut in + try input.parse() + }) ?? [] let name = abiRecord.name ?? "" return ABI.Element.EthError(name: name, inputs: abiInputs) } @@ -172,12 +170,6 @@ extension ABI.Input { let indexed = self.indexed == true return ABI.Element.Event.Input(name: name, type: parameterType, indexed: indexed) } - - func parseForError() throws -> ABI.Element.EthError.Input { - let name = self.name ?? "" - let parameterType = try ABITypeParser.parseTypeString(self.type) - return ABI.Element.EthError.Input(name: name, type: parameterType) - } } extension ABI.Output { diff --git a/Sources/Core/Utility/String+Extension.swift b/Sources/Core/Utility/String+Extension.swift index 8bbe1f688..cf7537663 100755 --- a/Sources/Core/Utility/String+Extension.swift +++ b/Sources/Core/Utility/String+Extension.swift @@ -130,6 +130,11 @@ extension String { return Int(s[s.startIndex].value) } } + + /// Strips whitespaces and newlines on both ends. + func trim() -> String { + trimmingCharacters(in: .whitespacesAndNewlines) + } } extension Character { diff --git a/Sources/Core/Web3Error/Web3Error.swift b/Sources/Core/Web3Error/Web3Error.swift index 251bcb069..bb21193dc 100644 --- a/Sources/Core/Web3Error/Web3Error.swift +++ b/Sources/Core/Web3Error/Web3Error.swift @@ -54,8 +54,8 @@ public enum Web3Error: LocalizedError { return "Server error: \(code)" case let .clientError(code: code): return "Client error: \(code)" - case .valueError: - return "You're passing value that doesn't supported by this method." + case .valueError(let errorDescription): + return (errorDescription?.isEmpty ?? true) ? "You're passing value that isn't supported by this method" : errorDescription! } } } diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+Call.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+Call.swift deleted file mode 100755 index c837c1ea0..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+Call.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import Core - -extension Web3.Eth { - public func callTransaction(_ transaction: CodableTransaction) async throws -> Data { - let request = APIRequest.call(transaction, transaction.callOnBlock ?? .latest) - return try await APIRequest.sendRequest(with: provider, for: request).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+EstimateGas.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+EstimateGas.swift deleted file mode 100755 index 7d1fa7578..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+EstimateGas.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - public func estimateGas(for transaction: CodableTransaction, onBlock: BlockNumber = .latest) async throws -> BigUInt { - let request = APIRequest.estimateGas(transaction, onBlock) - return try await APIRequest.sendRequest(with: provider, for: request).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+FeeHistory.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+FeeHistory.swift deleted file mode 100644 index d290140fc..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+FeeHistory.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - func feeHistory(blockCount: BigUInt, block: BlockNumber, percentiles: [Double]) async throws -> Oracle.FeeHistory { - let request = APIRequest.feeHistory(blockCount, block, percentiles) - return try await APIRequest.sendRequest(with: web3.provider, for: request).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetAccounts.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetAccounts.swift deleted file mode 100755 index f433fabe2..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetAccounts.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - public func ownedAccounts() async throws -> [EthereumAddress] { - guard web3.provider.attachedKeystoreManager == nil else { - return try web3.wallet.getAccounts() - } - return try await APIRequest.sendRequest(with: web3.provider, for: .getAccounts).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBalance.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBalance.swift deleted file mode 100755 index fb189e2fa..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBalance.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import Core -import BigInt - -extension Web3.Eth { - public func getBalance(for address: EthereumAddress, onBlock: BlockNumber = .latest) async throws -> BigUInt { - let request = APIRequest.getBalance(address.address, onBlock) - return try await APIRequest.sendRequest(with: web3.provider, for: request).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBlockByHash.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBlockByHash.swift deleted file mode 100755 index ad68b740c..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBlockByHash.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - public func block(by hash: Data, fullTransactions: Bool = false) async throws -> Block { - let request: APIRequest = .getBlockByHash(hash.toHexString().addHexPrefix(), fullTransactions) - return try await APIRequest.sendRequest(with: provider, for: request).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBlockByNumber.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBlockByNumber.swift deleted file mode 100755 index 6763df2cc..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBlockByNumber.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - public func block(by hash: Hash, fullTransactions: Bool = false) async throws -> Block { - let request = APIRequest.getBlockByHash(hash, fullTransactions) - return try await APIRequest.sendRequest(with: provider, for: request).result - } - - public func block(by number: BlockNumber, fullTransactions: Bool = false) async throws -> Block { - let request = APIRequest.getBlockByNumber(number, fullTransactions) - return try await APIRequest.sendRequest(with: provider, for: request).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBlockNumber.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBlockNumber.swift deleted file mode 100755 index b63556897..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetBlockNumber.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - public func blockNumber() async throws -> BigUInt { - try await APIRequest.sendRequest(with: web3.provider, for: .blockNumber).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetCode.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetCode.swift deleted file mode 100644 index f17eda612..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetCode.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import Core -import BigInt - -extension Web3.Eth { - public func code(for address: EthereumAddress, onBlock: BlockNumber = .latest) async throws -> Hash { - let request = APIRequest.getCode(address.address, onBlock) - return try await APIRequest.sendRequest(with: provider, for: request).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetGasPrice.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetGasPrice.swift deleted file mode 100755 index e902e7736..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetGasPrice.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - public func gasPrice() async throws -> BigUInt { - try await APIRequest.sendRequest(with: self.provider, for: .gasPrice).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetTransactionCount.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetTransactionCount.swift deleted file mode 100755 index 59ff77d35..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetTransactionCount.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - public func getTransactionCount(for address: EthereumAddress, onBlock: BlockNumber = .latest) async throws -> BigUInt { - let request = APIRequest.getTransactionCount(address.address, onBlock) - return try await APIRequest.sendRequest(with: provider, for: request).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetTransactionDetails.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetTransactionDetails.swift deleted file mode 100755 index 34a30a98c..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetTransactionDetails.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - public func transactionDetails(_ txHash: Data) async throws -> TransactionDetails { - let request = APIRequest.getTransactionByHash(txHash.toHexString().addHexPrefix()) - return try await APIRequest.sendRequest(with: provider, for: request).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetTransactionReceipt.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetTransactionReceipt.swift deleted file mode 100755 index a960c5791..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+GetTransactionReceipt.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - public func transactionReceipt(_ txHash: Data) async throws -> TransactionReceipt { - let request = APIRequest.getTransactionReceipt(txHash.toHexString().addHexPrefix()) - return try await APIRequest.sendRequest(with: provider, for: request).result - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+SendRawTransaction.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+SendRawTransaction.swift deleted file mode 100755 index a74bed92f..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+SendRawTransaction.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Created by Yaroslav Yashin. -// Copyright © 2022 Yaroslav Yashin. All rights reserved. -// - -import Foundation -import Core - -extension Web3.Eth { - public func send(raw data: Data) async throws -> TransactionSendingResult { - let request = APIRequest.sendRawTransaction(data.toHexString().addHexPrefix()) - let response: APIResponse = try await APIRequest.sendRequest(with: provider, for: request) - return try TransactionSendingResult(data: data, hash: response.result) - } -} - -public struct TransactionSendingResult { - public var transaction: CodableTransaction - public var hash: String -} - -fileprivate extension TransactionSendingResult { - init(data: Data, hash: Hash) throws { - guard let transaction = CodableTransaction(rawValue: data) else { throw Web3Error.dataError } - self.transaction = transaction - self.hash = hash - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+SendTransaction.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+SendTransaction.swift deleted file mode 100644 index 51f9fe86a..000000000 --- a/Sources/web3swift/EthereumAPICalls/Ethereum/Eth+SendTransaction.swift +++ /dev/null @@ -1,17 +0,0 @@ -// web3swift -// -// Created by Alex Vlasov. -// Copyright © 2018 Alex Vlasov. All rights reserved. -// - -import Foundation -import BigInt -import Core - -extension Web3.Eth { - public func send(_ transaction: CodableTransaction) async throws -> TransactionSendingResult { - let request = APIRequest.sendTransaction(transaction) - let response: APIResponse = try await APIRequest.sendRequest(with: provider, for: request) - return TransactionSendingResult(transaction: transaction, hash: response.result) - } -} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift new file mode 100644 index 000000000..3dcade8c0 --- /dev/null +++ b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth+Defaults.swift @@ -0,0 +1,158 @@ +// +// IEth+Defaults.swift +// +// Created by JeneaVranceanu on 07.12.2022. +// + +import Foundation +import BigInt +import Core + +public extension IEth { + func callTransaction(_ transaction: CodableTransaction) async throws -> Data { + let request = APIRequest.call(transaction, transaction.callOnBlock ?? .latest) + return try await APIRequest.sendRequest(with: provider, for: request).result + } +} + +public extension IEth { + func estimateGas(for transaction: CodableTransaction) async throws -> BigUInt { + try await estimateGas(for: transaction, onBlock: .latest) + } + + func estimateGas(for transaction: CodableTransaction, onBlock: BlockNumber) async throws -> BigUInt { + let request = APIRequest.estimateGas(transaction, onBlock) + return try await APIRequest.sendRequest(with: provider, for: request).result + } +} + +public extension IEth { + func transactionReceipt(_ txHash: Data) async throws -> TransactionReceipt { + let request = APIRequest.getTransactionReceipt(txHash.toHexString().addHexPrefix()) + return try await APIRequest.sendRequest(with: provider, for: request).result + } +} + +public extension IEth { + func transactionDetails(_ txHash: Data) async throws -> TransactionDetails { + let request = APIRequest.getTransactionByHash(txHash.toHexString().addHexPrefix()) + return try await APIRequest.sendRequest(with: provider, for: request).result + } +} + +public extension IEth { + func getTransactionCount(for address: EthereumAddress) async throws -> BigUInt { + try await getTransactionCount(for: address, onBlock: .latest) + } + + func getTransactionCount(for address: EthereumAddress, onBlock: BlockNumber) async throws -> BigUInt { + let request = APIRequest.getTransactionCount(address.address, onBlock) + return try await APIRequest.sendRequest(with: provider, for: request).result + } +} + +public extension IEth { + func gasPrice() async throws -> BigUInt { + try await APIRequest.sendRequest(with: self.provider, for: .gasPrice).result + } +} + +public extension IEth { + func code(for address: EthereumAddress) async throws -> Hash { + try await code(for: address, onBlock: .latest) + } + + func code(for address: EthereumAddress, onBlock: BlockNumber) async throws -> Hash { + let request = APIRequest.getCode(address.address, onBlock) + return try await APIRequest.sendRequest(with: provider, for: request).result + } +} + +public extension IEth { + func blockNumber() async throws -> BigUInt { + try await APIRequest.sendRequest(with: provider, for: .blockNumber).result + } +} + +public extension IEth { + func block(by hash: Hash) async throws -> Block { + try await block(by: hash, fullTransactions: false) + } + + func block(by hash: Hash, fullTransactions: Bool) async throws -> Block { + let request = APIRequest.getBlockByHash(hash, fullTransactions) + return try await APIRequest.sendRequest(with: provider, for: request).result + } + + func block(by number: BlockNumber) async throws -> Block { + try await block(by: number, fullTransactions: false) + } + + func block(by number: BlockNumber, fullTransactions: Bool) async throws -> Block { + let request = APIRequest.getBlockByNumber(number, fullTransactions) + return try await APIRequest.sendRequest(with: provider, for: request).result + } + + func block(by hash: Data) async throws -> Block { + try await block(by: hash, fullTransactions: false) + } + + func block(by hash: Data, fullTransactions: Bool) async throws -> Block { + let request: APIRequest = .getBlockByHash(hash.toHexString().addHexPrefix(), fullTransactions) + return try await APIRequest.sendRequest(with: provider, for: request).result + } +} + +public extension IEth { + func getBalance(for address: EthereumAddress) async throws -> BigUInt { + try await getBalance(for: address, onBlock: .latest) + } + + func getBalance(for address: EthereumAddress, onBlock: BlockNumber) async throws -> BigUInt { + let request = APIRequest.getBalance(address.address, onBlock) + return try await APIRequest.sendRequest(with: provider, for: request).result + } +} + +public extension IEth { + func ownedAccounts() async throws -> [EthereumAddress] { + if let addresses = provider.attachedKeystoreManager?.addresses { + return addresses + } + return try await APIRequest.sendRequest(with: provider, for: .getAccounts).result + } +} + +public extension IEth { + func feeHistory(blockCount: BigUInt, block: BlockNumber, percentiles: [Double]) async throws -> Oracle.FeeHistory { + let request = APIRequest.feeHistory(blockCount, block, percentiles) + return try await APIRequest.sendRequest(with: provider, for: request).result + } +} + +public extension IEth { + func send(_ transaction: CodableTransaction) async throws -> TransactionSendingResult { + let request = APIRequest.sendTransaction(transaction) + let response: APIResponse = try await APIRequest.sendRequest(with: provider, for: request) + return TransactionSendingResult(transaction: transaction, hash: response.result) + } + + func send(raw data: Data) async throws -> TransactionSendingResult { + guard let transaction = CodableTransaction(rawValue: data) else { + // FIXME: When the PR is merged add this description to dataError -> + // Description to add: + // Link to PR: + throw Web3Error.dataError + } + let request = APIRequest.sendRawTransaction(data.toHexString().addHexPrefix()) + let response: APIResponse = try await APIRequest.sendRequest(with: provider, for: request) + return TransactionSendingResult(transaction: transaction, hash: response.result) + } +} + +// MARK: - Supporting models and extensions + +public struct TransactionSendingResult { + public var transaction: CodableTransaction + public var hash: String +} diff --git a/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift new file mode 100755 index 000000000..69bbc3e5a --- /dev/null +++ b/Sources/web3swift/EthereumAPICalls/Ethereum/IEth.swift @@ -0,0 +1,34 @@ +// +// Created by Yaroslav Yashin. +// Copyright © 2022 Yaroslav Yashin. All rights reserved. +// + +import Foundation +import BigInt +import Core + +public protocol IEth { + var provider: Web3Provider { get } + func callTransaction(_ transaction: CodableTransaction) async throws -> Data + func send(_ transaction: CodableTransaction) async throws -> TransactionSendingResult + func send(raw data: Data) async throws -> TransactionSendingResult + + func estimateGas(for transaction: CodableTransaction, onBlock: BlockNumber) async throws -> BigUInt + func feeHistory(blockCount: BigUInt, block: BlockNumber, percentiles: [Double]) async throws -> Oracle.FeeHistory + func ownedAccounts() async throws -> [EthereumAddress] + func getBalance(for address: EthereumAddress, onBlock: BlockNumber) async throws -> BigUInt + + func block(by hash: Data, fullTransactions: Bool) async throws -> Block + func block(by number: BlockNumber, fullTransactions: Bool) async throws -> Block + func block(by hash: Hash, fullTransactions: Bool) async throws -> Block + func blockNumber() async throws -> BigUInt + + func code(for address: EthereumAddress, onBlock: BlockNumber) async throws -> Hash + + func gasPrice() async throws -> BigUInt + + func getTransactionCount(for address: EthereumAddress, onBlock: BlockNumber) async throws -> BigUInt + + func transactionDetails(_ txHash: Data) async throws -> TransactionDetails + func transactionReceipt(_ txHash: Data) async throws -> TransactionReceipt +} diff --git a/Sources/web3swift/Operations/WriteOperation.swift b/Sources/web3swift/Operations/WriteOperation.swift index 9c84202e7..6458ba6ff 100755 --- a/Sources/web3swift/Operations/WriteOperation.swift +++ b/Sources/web3swift/Operations/WriteOperation.swift @@ -11,25 +11,35 @@ import Core public class WriteOperation: ReadOperation { // FIXME: Rewrite this to CodableTransaction - /// Sends raw transaction for write operation. + /// Sends signed or unsigned transaction for write operation. /// - Parameters: - /// - password: Password for private key. - /// - policies: Custom policies for how to resolve (optional). Default is auto. - public func writeToChain(password: String, policies: Policies = .auto) async throws -> TransactionSendingResult { + /// - password: Password for the private key in the keystore manager attached to the provider + /// you set to `web3` passed in the initializer. + /// - policies: Determining the behaviour of how transaction attributes like gas limit and + /// nonce are resolved. Default value is ``Policies/auto``. + /// - sendRaw: If set to `true` transaction will be signed and sent using `eth_sendRawTransaction`. + /// Otherwise, no signing attempts will take place and the `eth_sendTransaction` RPC will be used instead. + /// Default value is `true`. + public func writeToChain(password: String, policies: Policies = .auto, sendRaw: Bool = true) async throws -> TransactionSendingResult { try await policyResolver.resolveAll(for: &transaction, with: policies) - if let attachedKeystoreManager = self.web3.provider.attachedKeystoreManager { - do { - try Web3Signer.signTX(transaction: &transaction, - keystore: attachedKeystoreManager, - account: transaction.from ?? transaction.sender ?? EthereumAddress.contractDeploymentAddress(), - password: password) - } catch { - throw Web3Error.inputError(desc: "Failed to locally sign a transaction") - } - guard let transactionData = transaction.encode(for: .transaction) else { throw Web3Error.dataError } - return try await web3.eth.send(raw: transactionData) + + guard sendRaw else { + return try await web3.eth.send(transaction) + } + + guard let attachedKeystoreManager = web3.provider.attachedKeystoreManager else { + throw Web3Error.inputError(desc: "Failed to locally sign a transaction. Web3 provider doesn't have keystore attached.") + } + + do { + try Web3Signer.signTX(transaction: &transaction, + keystore: attachedKeystoreManager, + account: transaction.from ?? transaction.sender ?? EthereumAddress.contractDeploymentAddress(), + password: password) + } catch { + throw Web3Error.inputError(desc: "Failed to locally sign a transaction. \(error.localizedDescription)") } - // MARK: Sending Data flow - return try await web3.eth.send(transaction) + guard let transactionData = transaction.encode(for: .transaction) else { throw Web3Error.dataError } + return try await web3.eth.send(raw: transactionData) } } diff --git a/Sources/web3swift/Web3/Web3+Instance.swift b/Sources/web3swift/Web3/Web3+Instance.swift index 1e54f5835..a9b439a56 100755 --- a/Sources/web3swift/Web3/Web3+Instance.swift +++ b/Sources/web3swift/Web3/Web3+Instance.swift @@ -20,29 +20,26 @@ public class Web3 { /// Keystore manager can be bound to Web3 instance. If some manager is bound all further account related functions, such /// as account listing, transaction signing, etc. are done locally using private keys and accounts found in a manager. public func addKeystoreManager(_ manager: KeystoreManager?) { - self.provider.attachedKeystoreManager = manager + provider.attachedKeystoreManager = manager } - var ethInstance: Web3.Eth? + var ethInstance: IEth? /// Public web3.eth.* namespace. - public var eth: Web3.Eth { - if self.ethInstance != nil { - return self.ethInstance! + public var eth: IEth { + if ethInstance != nil { + return ethInstance! } - self.ethInstance = Web3.Eth(provider: self.provider, web3: self) - return self.ethInstance! + ethInstance = Web3.Eth(provider: provider) + return ethInstance! } // FIXME: Rewrite this to CodableTransaction - public class Eth { - var provider: Web3Provider - // weak var web3: web3? - var web3: Web3 + public class Eth: IEth { + public var provider: Web3Provider - public init(provider prov: Web3Provider, web3 web3instance: Web3) { + public init(provider prov: Web3Provider) { provider = prov - web3 = web3instance } } @@ -60,7 +57,7 @@ public class Web3 { // FIXME: Rewrite this to CodableTransaction public class Personal { var provider: Web3Provider - // weak var web3: web3? + // FIXME: remove dependency on web3 instance!! var web3: Web3 public init(provider prov: Web3Provider, web3 web3instance: Web3) { provider = prov @@ -82,7 +79,7 @@ public class Web3 { // FIXME: Rewrite this to CodableTransaction public class TxPool { var provider: Web3Provider - // weak var web3: web3? + // FIXME: remove dependency on web3 instance!! var web3: Web3 public init(provider prov: Web3Provider, web3 web3instance: Web3) { provider = prov @@ -103,7 +100,7 @@ public class Web3 { public class Web3Wallet { var provider: Web3Provider - // weak var web3: web3? + // FIXME: remove dependency on web3 instance!! var web3: Web3 public init(provider prov: Web3Provider, web3 web3instance: Web3) { provider = prov @@ -125,7 +122,7 @@ public class Web3 { // FIXME: Rewrite this to CodableTransaction public class BrowserFunctions { var provider: Web3Provider - // weak var web3: web3? + // FIXME: remove dependency on web3 instance!! public var web3: Web3 public init(provider prov: Web3Provider, web3 web3instance: Web3) { provider = prov @@ -156,7 +153,7 @@ public class Web3 { } var provider: Web3Provider - // weak var web3: web3? + // FIXME: remove dependency on web3 instance!! var web3: Web3 var timer: RepeatingTimer? diff --git a/Tests/web3swiftTests/localTests/ABIElementErrorDecodingTest.swift b/Tests/web3swiftTests/localTests/ABIElementErrorDecodingTest.swift new file mode 100644 index 000000000..9b4308151 --- /dev/null +++ b/Tests/web3swiftTests/localTests/ABIElementErrorDecodingTest.swift @@ -0,0 +1,187 @@ +// +// ABIElementErrorDecodingTest.swift +// +// Created by JeneaVranceanu on 28.11.2022. +// + +import Foundation +import XCTest +import Core + +class ABIElementErrorDecodingTest: XCTestCase { + typealias EthError = ABI.Element.EthError + + /// Function with any parameters should be able to decode `require` and `revert` calls in soliditiy. + /// Note: `require(expression)` and `revert()` without a message return 0 bytes thus we cannot guarantee + /// that 0 bytes response will be interpreted correctly. + private let emptyFunction = ABI.Element.Function(name: "any", + inputs: [], + outputs: [], + constant: false, + payable: false) + private let oneOutputFunction = ABI.Element.Function(name: "any", + inputs: [], + outputs: [.init(name: "", type: .bool)], + constant: false, + payable: false) + + func testErrorRepresentation() { + XCTAssertEqual(EthError(name: "Error", inputs: []).errorDeclaration, "Error()") + XCTAssertEqual(EthError(name: "Error", inputs: [.init(name: "", type: .address)]).errorDeclaration, "Error(address)") + XCTAssertEqual(EthError(name: "Error", inputs: [.init(name: " ", type: .address)]).errorDeclaration, "Error(address)") + XCTAssertEqual(EthError(name: "Error", inputs: [.init(name: " ", type: .address), .init(name: "", type: .uint(bits: 256))]).errorDeclaration, "Error(address,uint256)") + XCTAssertEqual(EthError(name: "Error", inputs: [.init(name: "sender", type: .address), .init(name: " ", type: .uint(bits: 256))]).errorDeclaration, "Error(address sender,uint256)") + // Not all types are supported in errors, e.g. tuples and functions are not supported + let allTypesNamedAndNot: [ABI.Element.InOut] = [ + .init(name: "sender", type: .address), + .init(name: "", type: .address), + .init(name: "", type: .uint(bits: 8)), + .init(name: "", type: .uint(bits: 16)), + .init(name: "", type: .uint(bits: 32)), + .init(name: "", type: .uint(bits: 64)), + .init(name: "", type: .uint(bits: 128)), + .init(name: "", type: .uint(bits: 256)), + .init(name: "my_int_8", type: .int(bits: 8)), + .init(name: "my_int_16", type: .int(bits: 16)), + .init(name: "my_int_32", type: .int(bits: 32)), + .init(name: "my_int_64", type: .int(bits: 64)), + .init(name: "my_int_128", type: .int(bits: 128)), + .init(name: "my_int_256", type: .int(bits: 256)), + .init(name: "someFlag", type: .bool), + .init(name: "rand_bytes", type: .bytes(length: 123)), + .init(name: "", type: .dynamicBytes), + .init(name: "arrarrarray123", type: .array(type: .bool, length: 0)), + .init(name: "error_message_maybe", type: .string), + ] + XCTAssertEqual(EthError(name: "VeryCustomErrorName", + inputs: allTypesNamedAndNot).errorDeclaration, + "VeryCustomErrorName(address sender,address,uint8,uint16,uint32,uint64,uint128,uint256,int8 my_int_8,int16 my_int_16,int32 my_int_32,int64 my_int_64,int128 my_int_128,int256 my_int_256,bool someFlag,bytes123 rand_bytes,bytes,bool[] arrarrarray123,string error_message_maybe)") + } + + /// Empty Data is not decoded as a call of `revert` or `require` if function has no outputs. + /// If a function that has no outputs attempts to decode empty `revert` or `require` must return `nil` + /// because we don't know just based on the output if the call was successful or reverted. + func testDecodeEmptyErrorOnNoOutputFunction() { + 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 + } + + 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) + } + + /// 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() { + /// 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 + } + + 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.") + } + + /// Data is decoded as a call of `revert Unauthorized()`. Decoded only if custom error ABI is given. + func testDecodeRevertWithCustomError() { + /// 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 + } + + 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) + } + + /// Data is decoded as a call of `revert Unauthorized()`. Decoded only if custom error ABI is given. + /// Trying to decode as `Unauthorized(string)`. Must fail. + func testDecodeRevertWithCustomErrorFailed() { + /// 82b42900 - Unauthorized() 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 + } + + 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) + } + + /// 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() { + /// 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 + } + + 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) + + 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["0"] as? String, decodedOutput["0"] as? String) + XCTAssertEqual(errorData["message_arg"] as? String, decodedOutput["message_arg"] as? String) + } +} diff --git a/Tests/web3swiftTests/localTests/ABIEncoderTest.swift b/Tests/web3swiftTests/localTests/ABIEncoderTest.swift index 9e36c9b4d..2e39c6e58 100644 --- a/Tests/web3swiftTests/localTests/ABIEncoderTest.swift +++ b/Tests/web3swiftTests/localTests/ABIEncoderTest.swift @@ -13,7 +13,7 @@ import Core class ABIEncoderTest: XCTestCase { - func test_soliditySha3() throws { + func testSoliditySha3() throws { var hex = try ABIEncoder.soliditySha3(true).toHexString().addHexPrefix() assert(hex == "0x5fe7f977e71dba2ea1a68e21057beebb9be2ac30c6410aa38d4f3fbe41dcffd2") hex = try ABIEncoder.soliditySha3(-10).toHexString().addHexPrefix() @@ -66,7 +66,7 @@ class ABIEncoderTest: XCTestCase { assert(hex == "0xa13b31627c1ed7aaded5aecec71baf02fe123797fffd45e662eac8e06fbe4955") } - func test_soliditySha3Fail_FloatDouble() throws { + func testSoliditySha3FailFloatDouble() throws { assert((try? ABIEncoder.soliditySha3(Float(1))) == nil) assert((try? ABIEncoder.soliditySha3(Double(1))) == nil) assert((try? ABIEncoder.soliditySha3(CGFloat(1))) == nil) @@ -78,7 +78,7 @@ class ABIEncoderTest: XCTestCase { /// `[AnyObject]` is not allowed to be used directly as input for `solidtySha3`. /// `AnyObject` erases type data making it impossible to encode some types correctly, /// e.g.: Bool can be treated as Int (8/16/32/64) and 0/1 numbers can be treated as Bool. - func test_soliditySha3Fail_1() throws { + func testSoliditySha3Fail_1() throws { var didFail = false do { _ = try ABIEncoder.soliditySha3([""] as [AnyObject]) @@ -91,7 +91,7 @@ class ABIEncoderTest: XCTestCase { /// `AnyObject` is not allowed to be used directly as input for `solidtySha3`. /// `AnyObject` erases type data making it impossible to encode some types correctly, /// e.g.: Bool can be treated as Int (8/16/32/64) and 0/1 numbers can be treated as Bool. - func test_soliditySha3Fail_2() throws { + func testSoliditySha3Fail_2() throws { var didFail = false do { _ = try ABIEncoder.soliditySha3("" as AnyObject) @@ -101,7 +101,7 @@ class ABIEncoderTest: XCTestCase { XCTAssertTrue(didFail) } - func test_abiEncoding_emptyValues() { + func testAbiEncodingEmptyValues() { let zeroBytes = ABIEncoder.encode(types: [ABI.Element.InOut](), values: [AnyObject]())! XCTAssert(zeroBytes.count == 0) @@ -114,4 +114,24 @@ class ABIEncoderTest: XCTestCase { XCTAssertTrue(functionWithNoInput.methodEncoding == encodedFunction) XCTAssertTrue("0xe16b4a9b" == encodedFunction?.toHexString().addHexPrefix().lowercased()) } + + func testAbiEncodingDynamicTypes() { + var encodedValue = ABIEncoder.encode(types: [.dynamicBytes], values: [Data.fromHex("6761766f66796f726b")!] as [AnyObject])!.toHexString() + XCTAssertEqual(encodedValue, "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000096761766f66796f726b0000000000000000000000000000000000000000000000") + + encodedValue = ABIEncoder.encode(types: [.dynamicBytes], values: [Data.fromHex("731a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b")!] as [AnyObject])!.toHexString() + XCTAssertEqual(encodedValue, "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020731a3afc00d1b1e3461b955e53fc866dcf303b3eb9f4c16f89e388930f48134b") + + encodedValue = ABIEncoder.encode(types: [.dynamicBytes], values: [Data.fromHex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1")!] as [AnyObject])!.toHexString() + XCTAssertEqual(encodedValue, "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000009ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff100") + + encodedValue = ABIEncoder.encode(types: [.dynamicBytes], values: [Data.fromHex("c3a40000c3a4")!] as [AnyObject])!.toHexString() + XCTAssertEqual(encodedValue, "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006c3a40000c3a40000000000000000000000000000000000000000000000000000") + + encodedValue = ABIEncoder.encode(types: [.string], values: ["gavofyork"] as [AnyObject])!.toHexString() + XCTAssertEqual(encodedValue, "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000096761766f66796f726b0000000000000000000000000000000000000000000000") + + encodedValue = ABIEncoder.encode(types: [.string], values: ["Heeäööä👅D34ɝɣ24Єͽ-.,äü+#/"] as [AnyObject])!.toHexString() + XCTAssertEqual(encodedValue, "00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000026486565c3a4c3b6c3b6c3a4f09f9185443334c99dc9a33234d084cdbd2d2e2cc3a4c3bc2b232f0000000000000000000000000000000000000000000000000000") + } } diff --git a/Tests/web3swiftTests/localTests/AdvancedABIv2Tests.swift b/Tests/web3swiftTests/localTests/AdvancedABIv2Tests.swift index f31eaf1ac..e34aed0f8 100755 --- a/Tests/web3swiftTests/localTests/AdvancedABIv2Tests.swift +++ b/Tests/web3swiftTests/localTests/AdvancedABIv2Tests.swift @@ -25,7 +25,7 @@ class AdvancedABIv2Tests: LocalTestCase { 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) + let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) let txHash = result.hash.stripHexPrefix() Thread.sleep(forTimeInterval: 1.0) @@ -61,7 +61,7 @@ class AdvancedABIv2Tests: LocalTestCase { 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) + let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) let txHash = result.hash.stripHexPrefix() Thread.sleep(forTimeInterval: 1.0) @@ -97,7 +97,7 @@ class AdvancedABIv2Tests: LocalTestCase { 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) + let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) let txHash = result.hash.stripHexPrefix() Thread.sleep(forTimeInterval: 1.0) @@ -132,7 +132,7 @@ class AdvancedABIv2Tests: LocalTestCase { 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) + let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) let txHash = result.hash.stripHexPrefix() Thread.sleep(forTimeInterval: 1.0) @@ -168,7 +168,7 @@ class AdvancedABIv2Tests: LocalTestCase { 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) + let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) let txHash = result.hash.stripHexPrefix() Thread.sleep(forTimeInterval: 1.0) diff --git a/Tests/web3swiftTests/localTests/BasicLocalNodeTests.swift b/Tests/web3swiftTests/localTests/BasicLocalNodeTests.swift index bddfe6a1e..c099f330d 100755 --- a/Tests/web3swiftTests/localTests/BasicLocalNodeTests.swift +++ b/Tests/web3swiftTests/localTests/BasicLocalNodeTests.swift @@ -25,7 +25,7 @@ class BasicLocalNodeTests: LocalTestCase { 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) + let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) let txHash = result.hash.stripHexPrefix() while true { @@ -60,7 +60,7 @@ class BasicLocalNodeTests: LocalTestCase { print("Balance before to: " + balanceBeforeTo.description) print("Balance before from: " + balanceBeforeFrom.description) - let result = try await sendTx.writeToChain(password: "web3swift") + let result = try await sendTx.writeToChain(password: "web3swift", sendRaw: false) let txHash = Data.fromHex(result.hash.stripHexPrefix())! Thread.sleep(forTimeInterval: 1.0) diff --git a/Tests/web3swiftTests/localTests/ERC20ClassTests.swift b/Tests/web3swiftTests/localTests/ERC20ClassTests.swift index 6ae53cf5a..2bda587d8 100755 --- a/Tests/web3swiftTests/localTests/ERC20ClassTests.swift +++ b/Tests/web3swiftTests/localTests/ERC20ClassTests.swift @@ -39,7 +39,7 @@ class ERC20ClassTests: LocalTestCase { func testERC20tokenBalanceAndAllowance() async throws { let (web3, _, receipt, _) = try await TestHelpers.localDeployERC20() - let erc20token = ERC20.init(web3: web3, provider: web3.provider, address: receipt.contractAddress!) + let erc20token = ERC20(web3: web3, provider: web3.provider, address: receipt.contractAddress!) let userAddress = EthereumAddress("0xe22b8979739D724343bd002F9f432F5990879901")! diff --git a/Tests/web3swiftTests/localTests/KeystoresTests.swift b/Tests/web3swiftTests/localTests/KeystoresTests.swift index 4911c6c39..3bce14c33 100755 --- a/Tests/web3swiftTests/localTests/KeystoresTests.swift +++ b/Tests/web3swiftTests/localTests/KeystoresTests.swift @@ -16,12 +16,12 @@ class KeystoresTests: LocalTestCase { func testBIP39 () throws { var entropy = Data.fromHex("00000000000000000000000000000000")! var phrase = BIP39.generateMnemonicsFromEntropy(entropy: entropy) - XCTAssert( phrase == "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about") + XCTAssert(phrase == "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about") var seed = BIP39.seedFromMmemonics(phrase!, password: "TREZOR") XCTAssert(seed?.toHexString() == "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04") entropy = Data.fromHex("68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c")! phrase = BIP39.generateMnemonicsFromEntropy(entropy: entropy) - XCTAssert( phrase == "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length") + XCTAssert(phrase == "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length") seed = BIP39.seedFromMmemonics(phrase!, password: "TREZOR") XCTAssert(seed?.toHexString() == "64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440") } diff --git a/Tests/web3swiftTests/localTests/LocalTestCase.swift b/Tests/web3swiftTests/localTests/LocalTestCase.swift index 720d01f35..c44dab412 100644 --- a/Tests/web3swiftTests/localTests/LocalTestCase.swift +++ b/Tests/web3swiftTests/localTests/LocalTestCase.swift @@ -30,7 +30,7 @@ class LocalTestCase: XCTestCase { writeTX.transaction.value = value let policies = Policies(gasLimitPolicy: .manual(78423), gasPricePolicy: .manual(20000000000)) for _ in block..<25 { - _ = try! await writeTX.writeToChain(password: "", policies: policies) + _ = try! await writeTX.writeToChain(password: "", policies: policies, sendRaw: false) } } } diff --git a/Tests/web3swiftTests/localTests/Mocks.swift b/Tests/web3swiftTests/localTests/Mocks.swift new file mode 100644 index 000000000..de284dd2b --- /dev/null +++ b/Tests/web3swiftTests/localTests/Mocks.swift @@ -0,0 +1,23 @@ +// +// Mocks.swift +// +// Created by JeneaVranceanu on 07.12.2022. +// + +import Foundation +@testable import web3swift +@testable import Core + +class Web3EthMock: IEth { + let provider: Web3Provider + + var onCallTransaction: ((CodableTransaction) -> Data)? + + init(provider: Web3Provider) { + self.provider = provider + } + + func callTransaction(_ transaction: CodableTransaction) async throws -> Data { + onCallTransaction?(transaction) ?? Data() + } +} diff --git a/Tests/web3swiftTests/localTests/PersonalSignatureTests.swift b/Tests/web3swiftTests/localTests/PersonalSignatureTests.swift index e9b99f965..418ff7a22 100755 --- a/Tests/web3swiftTests/localTests/PersonalSignatureTests.swift +++ b/Tests/web3swiftTests/localTests/PersonalSignatureTests.swift @@ -42,7 +42,7 @@ class PersonalSignatureTests: XCTestCase { let allAddresses = try await web3.eth.ownedAccounts() deployTx.transaction.from = allAddresses[0] let policies = Policies(gasLimitPolicy: .manual(3000000)) - let deployResult = try await deployTx.writeToChain(password: "web3swift", policies: policies) + let deployResult = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) let txHash = Data.fromHex(deployResult.hash.stripHexPrefix())! Thread.sleep(forTimeInterval: 1.0) diff --git a/Tests/web3swiftTests/localTests/ST20AndSecurityTokenTests.swift b/Tests/web3swiftTests/localTests/ST20AndSecurityTokenTests.swift new file mode 100644 index 000000000..738648e7e --- /dev/null +++ b/Tests/web3swiftTests/localTests/ST20AndSecurityTokenTests.swift @@ -0,0 +1,133 @@ +// +// ST20AndSecurityTokenTests.swift +// +// Created by JeneaVranceanu on 07.12.2022. +// + +import XCTest +import BigInt +import Core + +@testable import web3swift + +class ST20AndSecurityTokenTests: XCTestCase { + + var web3: Web3! + var ethMock: Web3EthMock! + var st20token: ST20! + var securityToken: SecurityToken! + + override func setUp() async throws { + web3 = await Web3.InfuraGoerliWeb3(accessToken: Constants.infuraToken) + ethMock = Web3EthMock(provider: web3.provider) + web3.ethInstance = ethMock + st20token = ST20.init(web3: web3, provider: web3.provider, address: .contractDeploymentAddress()) + securityToken = SecurityToken.init(web3: web3, provider: web3.provider, address: .contractDeploymentAddress()) + } + + func testST20TokenPropertiesBasedOnERC20() async throws { + let expectedSymbol = "RandomTokenSymbol953" + let expectedName = "WhatA NAME - l953. Never seen again!" + let expectedDecimals = UInt8.random(in: 0...255) + + ethMock.onCallTransaction = { transaction in + guard let function = self.st20token.contract.contract.getFunctionCalled(transaction.data) else { + XCTFail("Failed to decode function call to determine what shall be returned") + return Data() + } + switch function.name { + case "symbol": + return ABIEncoder.encode(types: [.string], values: [expectedSymbol] as [AnyObject])! + case "name": + return ABIEncoder.encode(types: [.string], values: [expectedName] as [AnyObject])! + case "decimals": + return ABIEncoder.encode(types: [.uint(bits: 8)], values: [expectedDecimals] as [AnyObject])! + default: + // Unexpected function called + XCTFail("Called function '\(String(describing: function.name))' which wasn't supposed to be called.") + return Data() + } + } + + try await st20token.readProperties() + XCTAssertEqual(st20token.symbol, expectedSymbol) + XCTAssertEqual(st20token.name, expectedName) + XCTAssertEqual(st20token.decimals, expectedDecimals) + } + + func testST20TokenBalanceAndAllowance() async throws { + let expectedAllowance = BigUInt.randomInteger(lessThan: BigUInt(10000000000)) + let expectedBalance = BigUInt.randomInteger(lessThan: BigUInt(10000000000)) + + let userAddress = EthereumAddress("0xe22b8979739D724343bd002F9f432F5990879901")! + let delegate = EthereumAddress("0x2dD33957C90880bE4Ee9fd5F703110BDA2E579EC")! + + ethMock.onCallTransaction = { transaction in + guard let function = self.st20token.contract.contract.getFunctionCalled(transaction.data) else { + XCTFail("Failed to decode function call to determine what shall be returned") + return Data() + } + switch function.name { + case "balanceOf": + let address = function.decodeInputData(transaction.data)?["0"] as? EthereumAddress + XCTAssertEqual(address, userAddress) + return ABIEncoder.encode(types: [.uint(bits: 256)], values: [expectedBalance] as [AnyObject])! + case "allowance": + let transactionInput = function.decodeInputData(transaction.data) + XCTAssertEqual(transactionInput?["0"] as? EthereumAddress, userAddress) + XCTAssertEqual(transactionInput?["1"] as? EthereumAddress, delegate) + return ABIEncoder.encode(types: [.uint(bits: 256)], values: [expectedAllowance] as [AnyObject])! + default: + // Unexpected function called + XCTFail("Called function '\(String(describing: function.name))' which wasn't supposed to be called.") + return Data() + } + } + + let balance = try await st20token.getBalance(account: userAddress) + let allowance = try await st20token.getAllowance(originalOwner: userAddress, delegate: delegate) + XCTAssertEqual(balance, expectedBalance) + XCTAssertEqual(allowance, expectedAllowance) + } + + func testSecurityTokenInvestors() async throws { + let expectedNumberOfInvestors = BigUInt.randomInteger(lessThan: BigUInt(10000000000)) + ethMock.onCallTransaction = { transaction in + guard let function = self.securityToken.contract.contract.getFunctionCalled(transaction.data) else { + XCTFail("Failed to decode function call to determine what shall be returned") + return Data() + } + if function.name == "investorCount" { + return ABIEncoder.encode(types: [.uint(bits: 256)], values: [expectedNumberOfInvestors] as [AnyObject])! + } + // Unexpected function called + XCTFail("Called function '\(String(describing: function.name))' which wasn't supposed to be called.") + return Data() + } + + let investorsCount = try await securityToken.investorCount() + XCTAssertEqual(investorsCount, expectedNumberOfInvestors) + } + + func testSecurityTokenGranularity() async throws { + let expectedGranularity = BigUInt.randomInteger(lessThan: BigUInt(10000000000)) + + ethMock.onCallTransaction = { transaction in + guard let function = self.securityToken.contract.contract.getFunctionCalled(transaction.data) else { + XCTFail("Failed to decode function call to determine what shall be returned") + return Data() + } + switch function.name { + case "granularity": + return ABIEncoder.encode(types: [.uint(bits: 256)], values: [expectedGranularity] as [AnyObject])! + default: + // Unexpected function called + XCTFail("Called function '\(String(describing: function.name))' which wasn't supposed to be called.") + return Data() + } + } + + let granularity = try await securityToken.getGranularity() + XCTAssertEqual(granularity, expectedGranularity) + } +} diff --git a/Tests/web3swiftTests/localTests/TestHelpers.swift b/Tests/web3swiftTests/localTests/TestHelpers.swift index 23ad080a3..c80b4435e 100644 --- a/Tests/web3swiftTests/localTests/TestHelpers.swift +++ b/Tests/web3swiftTests/localTests/TestHelpers.swift @@ -33,7 +33,7 @@ class TestHelpers { parameters: parameters)! deployTx.transaction.from = allAddresses[0] let policies = Policies(gasLimitPolicy: .manual(3000000)) - let result = try await deployTx.writeToChain(password: "web3swift", policies: policies) + let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) let txHash = Data.fromHex(result.hash.stripHexPrefix())! Thread.sleep(forTimeInterval: 1.0) diff --git a/Tests/web3swiftTests/localTests/TransactionsTests.swift b/Tests/web3swiftTests/localTests/TransactionsTests.swift index 893e3b13c..b7e1a6886 100755 --- a/Tests/web3swiftTests/localTests/TransactionsTests.swift +++ b/Tests/web3swiftTests/localTests/TransactionsTests.swift @@ -638,7 +638,7 @@ class TransactionsTests: XCTestCase { writeTX.transaction.from = from writeTX.transaction.value = value! let policies = Policies(gasLimitPolicy: .manual(78423)) - let result = try await writeTX.writeToChain(password: "", policies: policies) + let result = try await writeTX.writeToChain(password: "", policies: policies, sendRaw: false) let txHash = Data.fromHex(result.hash.stripHexPrefix())! print("Transaction with hash ", txHash) diff --git a/Tests/web3swiftTests/localTests/UserCases.swift b/Tests/web3swiftTests/localTests/UserCases.swift index e353fb990..1f930783d 100755 --- a/Tests/web3swiftTests/localTests/UserCases.swift +++ b/Tests/web3swiftTests/localTests/UserCases.swift @@ -76,11 +76,11 @@ class UserCases: XCTestCase { let allAddresses = try await web3.eth.ownedAccounts() let contract = web3.contract(Web3.Utils.estimateGasTestABI, at: nil, abiVersion: 2)! - let parameters = [] as [AnyObject] + let parameters = [AnyObject]() 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) + let result = try await deployTx.writeToChain(password: "web3swift", policies: policies, sendRaw: false) let txHash = Data.fromHex(result.hash.stripHexPrefix())! Thread.sleep(forTimeInterval: 1.0) diff --git a/Tests/web3swiftTests/remoteTests/ST20AndSecurityTokenTests.swift b/Tests/web3swiftTests/remoteTests/ST20AndSecurityTokenTests.swift deleted file mode 100644 index be8d5b234..000000000 --- a/Tests/web3swiftTests/remoteTests/ST20AndSecurityTokenTests.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// web3swift_ST20_Tests.swift -// web3swift-iOS_Tests -// -// Created by Anton on 15/03/2019. -// Copyright © 2019 The Matter Inc. All rights reserved. -// -import XCTest -import BigInt -import web3swift -import Core - -@testable import web3swift - -// MARK: Works only with network connection -class ST20AndSecurityTokenTests: XCTestCase { - - // FIXME: Enable me back again - // Test fails because there's no such wallet on goerli chain as well as token. -// func testERC20TokenCreation() async throws { -// let web3 = await Web3.InfuraGoerliWeb3(accessToken: Constants.infuraToken) -// let w3sTokenAddress = EthereumAddress("0x33d191db2486e0d245b44fde3fae5ed667d5694b")! -// let st20token = ST20.init(web3: web3, provider: web3.provider, address: w3sTokenAddress) -// try await st20token.readProperties() -// XCTAssertEqual(st20token.symbol(), "MIMI") -// XCTAssertEqual(st20token.name(), "Mimi") -// XCTAssertEqual(st20token.decimals(), 18) -// } - - func testST20tokenBalanceAndAllowance() async throws { - let web3 = await Web3.InfuraGoerliWeb3(accessToken: Constants.infuraToken) - let w3sTokenAddress = EthereumAddress("0x2dD33957C90880bE4Ee9fd5F703110BDA2E579EC")! - let st20token = ST20.init(web3: web3, provider: web3.provider, address: w3sTokenAddress) - let userAddress = EthereumAddress("0xe22b8979739D724343bd002F9f432F5990879901")! - let balance = try await st20token.getBalance(account: userAddress) - let allowance = try await st20token.getAllowance(originalOwner: userAddress, delegate: userAddress) - XCTAssertEqual(balance, 0) - XCTAssertEqual(allowance, 0) - } - - func testSecurityTokenInvestors() async throws { - let web3 = await Web3.InfuraGoerliWeb3(accessToken: Constants.infuraToken) - let w3sTokenAddress = EthereumAddress("0x2dD33957C90880bE4Ee9fd5F703110BDA2E579EC")! - let stoken = SecurityToken.init(web3: web3, provider: web3.provider, address: w3sTokenAddress) - let investorsCount = try await stoken.investorCount() - XCTAssertEqual(investorsCount, 0) - } - - // FIXME: Enable me back again - // Test fails because there's no such wallet on goerli chain as well as token. -// func testSecurityTokenGranularity() async throws { -// let web3 = await Web3.InfuraGoerliWeb3(accessToken: Constants.infuraToken) -// let w3sTokenAddress = EthereumAddress("0x2dD33957C90880bE4Ee9fd5F703110BDA2E579EC")! -// let stoken = SecurityToken.init(web3: web3, provider: web3.provider, address: w3sTokenAddress) -// let granularity = try await stoken.getGranularity() -// let stringGranularity = String(granularity) -// XCTAssertEqual(stringGranularity, "1000000000000000000") -// } -}