Skip to content

Commit

Permalink
Merge branch 'develop' into chore/abiencoder-docs+tests
Browse files Browse the repository at this point in the history
  • Loading branch information
JeneaVranceanu authored Dec 12, 2022
2 parents 764bee0 + 0070078 commit b25fac0
Show file tree
Hide file tree
Showing 42 changed files with 920 additions and 614 deletions.
5 changes: 0 additions & 5 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
## **Summary of Changes**


Fixes # _(if applicable - add the number of issue this PR addresses)_

 

## **Test Data or Screenshots**

 

###### _By submitting this pull request, you are confirming the following:_

- I have reviewed the [Contribution Guidelines](https://github.com/web3swift-team/web3swift/blob/develop/CONTRIBUTION.md).
Expand Down
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)
}
}

extension ABI.Output {
Expand Down
10 changes: 5 additions & 5 deletions Sources/Core/Oracle/GasOracle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,11 @@ public extension Oracle {

extension Oracle {
public struct FeeHistory {
let timestamp = Date()
let baseFeePerGas: [BigUInt]
let gasUsedRatio: [Double]
let oldestBlock: BigUInt
let reward: [[BigUInt]]
public let timestamp = Date()
public let baseFeePerGas: [BigUInt]
public let gasUsedRatio: [Double]
public let oldestBlock: BigUInt
public let reward: [[BigUInt]]
}
}

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
Loading

0 comments on commit b25fac0

Please sign in to comment.