Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eea9749
fix bugs
pharms-eth Nov 21, 2022
34e312a
fix: documentation update
JeneaVranceanu Nov 21, 2022
4207de8
fix: documentation update for func writeToChain
JeneaVranceanu Nov 21, 2022
ff05502
update per feedback
pharms-eth Nov 22, 2022
6b69769
attempt test fix
pharms-eth Nov 22, 2022
713c63d
working on tests
pharms-eth Nov 22, 2022
aaaabe7
restore true
pharms-eth Nov 22, 2022
9fc1508
amend tests
pharms-eth Nov 22, 2022
13bac57
adjust helpers
pharms-eth Nov 22, 2022
2213fd0
adjust test for deploy changes
pharms-eth Nov 24, 2022
5e1dd2c
fix another test for deploy changes
pharms-eth Nov 24, 2022
6247628
chore: default value of sendRaw for writeToChain is true
JeneaVranceanu Nov 25, 2022
aa32c36
chore: decodeReturnData refactoring + documentation for it
JeneaVranceanu Nov 28, 2022
a5c56ea
fix: first we try to decode data as a revert/require call
JeneaVranceanu Nov 29, 2022
3198752
chore: added trim extension function to String
JeneaVranceanu Nov 29, 2022
f2cb4b9
fix: EthError uses InOut as type for inputs; produces error declarati…
JeneaVranceanu Nov 29, 2022
6bc5100
chore: test update - added array type to error arguments
JeneaVranceanu Nov 29, 2022
7257f59
chore: EthError has default empty inputs array
JeneaVranceanu Dec 1, 2022
3151678
feat: decodeReturnData is split into decodeReturnData and decodeError…
JeneaVranceanu Dec 1, 2022
8d2490b
chore: tests for errors decode
JeneaVranceanu Dec 1, 2022
6a5ce15
fix: return message from Web3Error.valueError
JeneaVranceanu Dec 5, 2022
b9ac465
fix: removed redundant spacing
JeneaVranceanu Dec 5, 2022
60c9ca7
feat: ContractProtocol - new function to search for ABI.Element.Funct…
JeneaVranceanu Dec 6, 2022
d08a2de
chore: added tests for dynamic types encoding
JeneaVranceanu Dec 7, 2022
97ba8e9
feat: mocking network responses
JeneaVranceanu Dec 7, 2022
ff0778b
chore: Web3.Eth + IEth refactoring
JeneaVranceanu Dec 7, 2022
29fff74
fix: added initializer for the Web3EthMock
JeneaVranceanu Dec 7, 2022
e2b0f5d
fix: changed type of web3.eth to IEth and removed it's dependency on …
JeneaVranceanu Dec 7, 2022
bd2188a
chore: removed commented out case from SecurityToken test
JeneaVranceanu Dec 8, 2022
3707ca4
chore: updated FIXME comments to "// FIXME: remove dependency on web3…
JeneaVranceanu Dec 8, 2022
d036483
Merge pull request #701 from JeneaVranceanu/feat/remote-tests-mocking
yaroslavyaroslav Dec 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Sources/Core/Contract/ContractProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
236 changes: 166 additions & 70 deletions Sources/Core/EthereumABI/ABIElements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
}
}
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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..<data.count].reduce(0) { $0 + $1 } == 0) {
return ["_success": false,
"_failureReason": "`revert(string)` or `require(expression, string)` was executed.",
"_abortedByRevertOrRequire": true,
"_errorMessage": message]
}

returnArray["_success"] = true
return returnArray
if data.count >= 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..<data.count])) {
for idx in decodedInputs.indices {
errorResponse["\(idx)"] = decodedInputs[idx]
if !customError.inputs[idx].name.isEmpty {
errorResponse[customError.inputs[idx].name] = decodedInputs[idx]
}
}
} else if !customError.inputs.isEmpty {
errorResponse["_parsingError"] = "Data matches \(customError.errorDeclaration) but failed to be decoded."
}
return errorResponse
}
return nil
}
}

Expand Down
14 changes: 3 additions & 11 deletions Sources/Core/EthereumABI/ABIParsing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,9 @@ private func parseReceive(abiRecord: ABI.Record) throws -> 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)
}
Expand Down Expand Up @@ -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)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ABI.Element.EthError.Input was replaced with ABI.Element.InOut since these are two identical structures.
No need for func parseForError() throws -> ABI.Element.EthError.Input then.

}

extension ABI.Output {
Expand Down
5 changes: 5 additions & 0 deletions Sources/Core/Utility/String+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions Sources/Core/Web3Error/Web3Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!
}
}
}
14 changes: 0 additions & 14 deletions Sources/web3swift/EthereumAPICalls/Ethereum/Eth+Call.swift

This file was deleted.

Loading