Skip to content

Commit

Permalink
Merge pull request #831 from zhangliugang/feat/decode-error
Browse files Browse the repository at this point in the history
  • Loading branch information
yaroslavyaroslav authored Jan 9, 2024
2 parents bab86ca + e6ff991 commit ef28c1f
Show file tree
Hide file tree
Showing 23 changed files with 706 additions and 298 deletions.
69 changes: 62 additions & 7 deletions Sources/Web3Core/Contract/ContractProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,12 @@ public protocol ContractProtocol {
/// - name with arguments:`myFunction(uint256)`.
/// - method signature (with or without `0x` prefix, case insensitive): `0xFFffFFff`;
/// - data: non empty bytes to decode;
/// - Returns: dictionary with decoded values. `nil` if decoding failed.
func decodeReturnData(_ method: String, data: Data) -> [String: Any]?
/// - Returns: dictionary with decoded values.
/// - Throws:
/// - `Web3Error.revert(String, String?)` when function call aborted by `revert(string)` and `require(expression, string)`.
/// - `Web3Error.revertCustom(String, Dictionary)` when function call aborted by `revert CustomError()`.
@discardableResult
func decodeReturnData(_ method: String, data: Data) throws -> [String: Any]

/// Decode input arguments of a function.
/// - Parameters:
Expand Down Expand Up @@ -320,13 +324,40 @@ extension DefaultContractProtocol {
return bloom.test(topic: event.topic)
}

public func decodeReturnData(_ method: String, data: Data) -> [String: Any]? {
@discardableResult
public func decodeReturnData(_ method: String, data: Data) throws -> [String: Any] {
if method == "fallback" {
return [String: Any]()
return [:]
}

guard let function = methods[method]?.first else {
throw Web3Error.inputError(desc: "Make sure ABI you use contains '\(method)' method.")
}

switch data.count % 32 {
case 0:
return try function.decodeReturnData(data)
case 4:
let selector = data[0..<4]
if selector.toHexString() == "08c379a0", let reason = ABI.Element.EthError.decodeStringError(data[4...]) {
throw Web3Error.revert("revert(string)` or `require(expression, string)` was executed. reason: \(reason)", reason: reason)
}
else if selector.toHexString() == "4e487b71", let reason = ABI.Element.EthError.decodePanicError(data[4...]) {
let panicCode = String(format: "%02X", Int(reason)).addHexPrefix()
throw Web3Error.revert("Error: call revert exception; VM Exception while processing transaction: reverted with panic code \(panicCode)", reason: panicCode)
}
else if let customError = errors[selector.toHexString().addHexPrefix().lowercased()] {
if let errorArgs = customError.decodeEthError(data[4...]) {
throw Web3Error.revertCustom(customError.signature, errorArgs)
} else {
throw Web3Error.inputError(desc: "Signature matches \(customError.errorDeclaration) but failed to be decoded.")
}
} else {
throw Web3Error.inputError(desc: "Make sure ABI you use contains error that can match signature: 0x\(selector.toHexString())")
}
default:
throw Web3Error.inputError(desc: "Given data has invalid bytes count.")
}
return methods[method]?.compactMap({ function in
return function.decodeReturnData(data)
}).first
}

public func decodeInputData(_ method: String, data: Data) -> [String: Any]? {
Expand All @@ -346,8 +377,32 @@ extension DefaultContractProtocol {
return function.decodeInputData(Data(data[data.startIndex + 4 ..< data.startIndex + data.count]))
}

public func decodeEthError(_ data: Data) -> [String: Any]? {
guard data.count >= 4,
let err = errors.first(where: { $0.value.methodEncoding == data[0..<4] })?.value else {
return nil
}
return err.decodeEthError(data[4...])
}

public func getFunctionCalled(_ data: Data) -> ABI.Element.Function? {
guard data.count >= 4 else { return nil }
return methods[data[data.startIndex ..< data.startIndex + 4].toHexString().addHexPrefix()]?.first
}
}

extension DefaultContractProtocol {
@discardableResult
public func callStatic(_ method: String, parameters: [Any], provider: Web3Provider) async throws -> [String: Any] {
guard let address = address else {
throw Web3Error.inputError(desc: "RPC failed: contract is missing an address.")
}
guard let data = self.method(method, parameters: parameters, extraData: nil) else {
throw Web3Error.dataError
}
let transaction = CodableTransaction(to: address, data: data)

let result: Data = try await APIRequest.sendRequest(with: provider, for: .call(transaction, .latest)).result
return try decodeReturnData(method, data: result)
}
}
101 changes: 52 additions & 49 deletions Sources/Web3Core/EthereumABI/ABIElements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ extension ABI.Element.Constructor {
extension ABI.Element.Function {

/// Encode parameters of a given contract method
/// - Parameter parameters: Parameters to pass to Ethereum contract
/// - Parameters: Parameters to pass to Ethereum contract
/// - Returns: Encoded data
public func encodeParameters(_ parameters: [Any]) -> Data? {
guard parameters.count == inputs.count,
Expand Down Expand Up @@ -292,6 +292,44 @@ extension ABI.Element.Event {
}
}

// MARK: - Decode custom error

extension ABI.Element.EthError {
/// Decodes `revert CustomError(_)` calls.
/// - Parameters:
/// - data: bytes returned by a function call that stripped error signature hash.
/// - Returns: a dictionary containing decoded data mappend to indices and names of returned values or nil if decoding failed.
public func decodeEthError(_ data: Data) -> [String: Any]? {
guard inputs.count * 32 <= data.count,
let decoded = ABIDecoder.decode(types: inputs, data: data) else {
return nil
}

var result = [String: Any]()
for (index, out) in inputs.enumerated() {
result["\(index)"] = decoded[index]
if !out.name.isEmpty {
result[out.name] = decoded[index]
}
}
return result
}

/// Decodes `revert(string)` or `require(expression, string)` calls.
/// These calls are decomposed as `Error(string)` error.
public static func decodeStringError(_ data: Data) -> String? {
let decoded = ABIDecoder.decode(types: [.init(name: "", type: .string)], data: data)
return decoded?.first as? String
}

/// Decodes `Panic(uint256)` errors.
/// See more about panic code explain at: https://docs.soliditylang.org/en/v0.8.21/control-structures.html#panic-via-assert-and-error-via-require
public static func decodePanicError(_ data: Data) -> BigUInt? {
let decoded = ABIDecoder.decode(types: [.init(name: "", type: .uint(bits: 256))], data: data)
return decoded?.first as? BigUInt
}
}

// MARK: - Function input/output decoding

extension ABI.Element {
Expand All @@ -304,7 +342,7 @@ extension ABI.Element {
case .fallback:
return nil
case .function(let function):
return function.decodeReturnData(data)
return try? function.decodeReturnData(data)
case .receive:
return nil
case .error:
Expand Down Expand Up @@ -337,74 +375,38 @@ extension ABI.Element.Function {
return ABIDecoder.decodeInputData(rawData, methodEncoding: methodEncoding, inputs: inputs)
}

/// Decodes data returned by a function call. Able to decode `revert(string)`, `revert CustomError(...)` and `require(expression, string)` calls.
/// Decodes data returned by a function call.
/// - Parameters:
/// - data: bytes returned by a function call;
/// - errors: optional dictionary of known errors that could be returned by the function you called. Used to decode the error information.
/// - Returns: a dictionary containing decoded data mappend to indices and names of returned values if these are not `nil`.
/// If `data` is an error response returns dictionary containing all available information about that specific error. Read more for details.
/// - Throws:
/// - `Web3Error.processingError(desc: String)` when decode process failed.
///
/// Return cases:
/// - when no `outputs` declared and `data` is not an error response:
/// - when no `outputs` declared:
/// ```swift
/// ["_success": true]
/// [:]
/// ```
/// - when `outputs` declared and decoding completed successfully:
/// ```swift
/// ["_success": true, "0": value_1, "1": value_2, ...]
/// ["0": value_1, "1": value_2, ...]
/// ```
/// Additionally this dictionary will have mappings to output names if these names are specified in the ABI;
/// - function call was aborted using `revert(message)` or `require(expression, message)`:
/// ```swift
/// ["_success": false, "_abortedByRevertOrRequire": true, "_errorMessage": message]`
/// ```
/// - function call was aborted using `revert CustomMessage()` and `errors` argument contains the ABI of that custom error type:
/// ```swift
/// ["_success": false,
/// "_abortedByRevertOrRequire": true,
/// "_error": error_name_and_types, // e.g. `MyCustomError(uint256, address senderAddress)`
/// "0": error_arg1,
/// "1": error_arg2,
/// ...,
/// "error_arg1_name": error_arg1, // Only named arguments will be mapped to their names, e.g. `"senderAddress": EthereumAddress`
/// "error_arg2_name": error_arg2, // Otherwise, you can query them by position index.
/// ...]
/// ```
/// - in case of any error:
/// ```swift
/// ["_success": false, "_failureReason": String]
/// ```
/// Error reasons include:
/// - `outputs` declared but at least one value failed to be decoded;
/// - `data.count` is less than `outputs.count * 32`;
/// - `outputs` defined and `data` is empty;
/// - `data` represent reverted transaction
///
/// How `revert(string)` and `require(expression, string)` return value is decomposed:
/// - `08C379A0` function selector for `Error(string)`;
/// - next 32 bytes are the data offset;
/// - next 32 bytes are the error message length;
/// - the next N bytes, where N >= 32, are the message bytes
/// - the rest are 0 bytes padding.
public func decodeReturnData(_ data: Data, errors: [String: ABI.Element.EthError]? = nil) -> [String: Any] {
if let decodedError = decodeErrorResponse(data, errors: errors) {
return decodedError
}

public func decodeReturnData(_ data: Data) throws -> [String: Any] {
guard !outputs.isEmpty else {
NSLog("Function doesn't have any output types to decode given data.")
return ["_success": true]
return [:]
}

guard outputs.count * 32 <= data.count else {
return ["_success": false, "_failureReason": "Bytes count must be at least \(outputs.count * 32). Given \(data.count). Decoding will fail."]
throw Web3Error.processingError(desc: "Bytes count must be at least \(outputs.count * 32). Given \(data.count). Decoding will fail.")
}

// TODO: need improvement - we should be able to tell which value failed to be decoded
guard let values = ABIDecoder.decode(types: outputs, data: data) else {
return ["_success": false, "_failureReason": "Failed to decode at least one value."]
throw Web3Error.processingError(desc: "Failed to decode at least one value.")
}
var returnArray: [String: Any] = ["_success": true]
var returnArray: [String: Any] = [:]
for i in outputs.indices {
returnArray["\(i)"] = values[i]
if !outputs[i].name.isEmpty {
Expand Down Expand Up @@ -453,6 +455,7 @@ extension ABI.Element.Function {
/// // "_parsingError" is optional and is present only if decoding of custom error arguments failed
/// "_parsingError": "Data matches MyCustomError(uint256, address senderAddress) but failed to be decoded."]
/// ```
@available(*, deprecated, message: "Use decode function from `ABI.Element.EthError` instead")
public func decodeErrorResponse(_ data: Data, errors: [String: ABI.Element.EthError]? = nil) -> [String: Any]? {
/// If data is empty and outputs are expected it is treated as a `require(expression)` or `revert()` call with no message.
/// In solidity `require(false)` and `revert()` calls return empty error response.
Expand Down
50 changes: 49 additions & 1 deletion Sources/Web3Core/EthereumABI/ABIParameterTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,31 +168,79 @@ extension ABI.Element.ParameterType: Equatable {
}

extension ABI.Element.Function {
/// String representation of a function, e.g. `transfer(address,uint256)`.
public var signature: String {
return "\(name ?? "")(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))"
}

/// Function selector, e.g. `"cafe1234"`. Without hex prefix `0x`.
@available(*, deprecated, renamed: "selector", message: "Please, use 'selector' property instead.")
public var methodString: String {
return selector
}

/// Function selector, e.g. `"cafe1234"`. Without hex prefix `0x`.
public var selector: String {
return String(signature.sha3(.keccak256).prefix(8))
}

/// Function selector (e.g. `0xcafe1234`) but as raw bytes.
@available(*, deprecated, renamed: "selectorEncoded", message: "Please, use 'selectorEncoded' property instead.")
public var methodEncoding: Data {
return signature.data(using: .ascii)!.sha3(.keccak256)[0...3]
return selectorEncoded
}

/// Function selector (e.g. `0xcafe1234`) but as raw bytes.
public var selectorEncoded: Data {
return Data.fromHex(selector)!
}
}

// MARK: - Event topic
extension ABI.Element.Event {
/// String representation of an event, e.g. `ContractCreated(address)`.
public var signature: String {
return "\(name)(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))"
}

/// Hashed signature of an event, e.g. `0xcf78cf0d6f3d8371e1075c69c492ab4ec5d8cf23a1a239b6a51a1d00be7ca312`.
public var topic: Data {
return signature.data(using: .ascii)!.sha3(.keccak256)
}
}

extension ABI.Element.EthError {
/// String representation of an error, e.g. `TrasferFailed(address)`.
public var signature: String {
return "\(name)(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))"
}

/// Error selector, e.g. `"cafe1234"`. Without hex prefix `0x`.
@available(*, deprecated, renamed: "selector", message: "Please, use 'selector' property instead.")
public var methodString: String {
return selector
}

/// Error selector, e.g. `"cafe1234"`. Without hex prefix `0x`.
public var selector: String {
return String(signature.sha3(.keccak256).prefix(8))
}

/// Error selector (e.g. `0xcafe1234`) but as raw bytes.
@available(*, deprecated, renamed: "selectorEncoded", message: "Please, use 'selectorEncoded' property instead.")
public var methodEncoding: Data {
return selectorEncoded
}

/// Error selector (e.g. `0xcafe1234`) but as raw bytes.
public var selectorEncoded: Data {
return Data.fromHex(selector)!
}
}

extension ABI.Element.ParameterType: ABIEncoding {

/// Returns a valid solidity type like `address`, `uint128` or any other built-in type from Solidity.
public var abiRepresentation: String {
switch self {
case .uint(let bits):
Expand Down
2 changes: 2 additions & 0 deletions Sources/Web3Core/EthereumABI/Sequence+ABIExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public extension Sequence where Element == ABI.Element {
var errors = [String: ABI.Element.EthError]()
for case let .error(error) in self {
errors[error.name] = error
errors[error.signature] = error
errors[error.methodString.addHexPrefix().lowercased()] = error
}
return errors
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,12 @@
import Foundation

extension APIRequest {
var method: REST {
public var method: REST {
.POST
}

public var encodedBody: Data {
let request = RequestBody(method: call, params: parameters)
// this is safe to force try this here
// Because request must failed to compile if it not conformable with `Encodable` protocol
return try! JSONEncoder().encode(request)
public var encodedBody: Data {
RequestBody(method: call, params: parameters).encodedBody
}

var parameters: [RequestParameter] {
Expand Down
Loading

0 comments on commit ef28c1f

Please sign in to comment.