Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
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.selectorEncoded == 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(desc: "Failed to encode method \(method) with given parameters: \(String(describing: parameters))")
}
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)
}
}
111 changes: 58 additions & 53 deletions Sources/Web3Core/EthereumABI/ABIElements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,12 @@ 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,
let data = ABIEncoder.encode(types: inputs, values: parameters) else { return nil }
return methodEncoding + data
return selectorEncoded + data
}
}

Expand Down Expand Up @@ -292,6 +292,46 @@ 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 data.count > 0,
data.count % 32 == 0,
inputs.count * 32 <= data.count,
let decoded = ABIDecoder.decode(types: inputs, data: data) else {
return nil
}

var result = [String: Any]()
for (index, input) in inputs.enumerated() {
result["\(index)"] = decoded[index]
if !input.name.isEmpty {
result[input.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 +344,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 @@ -334,77 +374,41 @@ extension ABI.Element {

extension ABI.Element.Function {
public func decodeInputData(_ rawData: Data) -> [String: Any]? {
return ABIDecoder.decodeInputData(rawData, methodEncoding: methodEncoding, inputs: inputs)
return ABIDecoder.decodeInputData(rawData, methodEncoding: selectorEncoded, 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 +457,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 Expand Up @@ -517,8 +522,8 @@ extension ABIDecoder {
/// - Returns: decoded dictionary of input arguments mapped to their indices and arguments' names if these are not empty.
/// If decoding of at least one argument fails, `rawData` size is invalid or `methodEncoding` doesn't match - `nil` is returned.
static func decodeInputData(_ rawData: Data,
methodEncoding: Data? = nil,
inputs: [ABI.Element.InOut]) -> [String: Any]? {
methodEncoding: Data? = nil,
inputs: [ABI.Element.InOut]) -> [String: Any]? {
let data: Data
let sig: Data?

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
4 changes: 3 additions & 1 deletion Sources/Web3Core/EthereumABI/Sequence+ABIExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public extension Sequence where Element == ABI.Element {
for case let .function(function) in self where function.name != nil {
appendFunction(function.name!, function)
appendFunction(function.signature, function)
appendFunction(function.methodString.addHexPrefix().lowercased(), function)
appendFunction(function.selector.addHexPrefix().lowercased(), function)

/// ABI cannot have two functions with exactly the same name and input arguments
if (functions[function.signature]?.count ?? 0) > 1 {
Expand Down 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
2 changes: 1 addition & 1 deletion Sources/Web3Core/EthereumAddress/EthereumAddress.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public struct EthereumAddress: Equatable {
}
}

/// Checksummed address with `0x` HEX prefix.
/// Checksummed address with `0x` hex prefix.
/// If the ``type`` is ``EthereumAddress/AddressType/contractDeployment`` only `0x` prefix is returned.
public var address: String {
switch self.type {
Expand Down
Loading