Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve decode error #831

Merged
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1fc6cd6
fix: init Web3HttpProvider without chain id
zhangliugang Apr 18, 2023
d098b87
Merge commit 'dab2667578b9d053fc82dadb89337f5cbdd94f38' into develop
zhangliugang Aug 22, 2023
edd8e72
feat: improve decode error
zhangliugang Aug 28, 2023
9a7212e
fix: checkError
zhangliugang Aug 28, 2023
25dc43c
Trim Trailing Whitespace
zhangliugang Aug 28, 2023
5c3652b
fix eth method
zhangliugang Aug 28, 2023
20bfa7a
add missing test contract bytecode
zhangliugang Aug 28, 2023
acd66fe
resolve reviews suggestions
zhangliugang Sep 2, 2023
4521eb2
fix codespell
zhangliugang Sep 2, 2023
eb32f20
update function documentation
zhangliugang Sep 3, 2023
bc6f621
chore: error message update
JeneaVranceanu Nov 26, 2023
45c8ba5
chore: code reordering
JeneaVranceanu Nov 26, 2023
e542974
chore: docs refactoring
JeneaVranceanu Nov 26, 2023
f00a88f
chore: docs refactoring
JeneaVranceanu Nov 26, 2023
9dc9713
chore: comment refactoring
JeneaVranceanu Nov 26, 2023
62addff
chore: missing space added
JeneaVranceanu Nov 26, 2023
71e8902
chore: avoiding try? in tests (try is more preffered)
JeneaVranceanu Nov 26, 2023
3cadddd
fix: isHex string extension function refactoring
JeneaVranceanu Nov 26, 2023
01e1f6a
chore: isHex string extension tests
JeneaVranceanu Nov 26, 2023
e66dd35
Merge branch 'develop' into feat/decode-error
JeneaVranceanu Nov 26, 2023
0ba7039
chore: error messages updated
JeneaVranceanu Nov 26, 2023
ab90bda
Merge pull request #1 from JeneaVranceanu/feat/decode-error
zhangliugang Nov 27, 2023
7e4fdc1
fix: code spell
zhangliugang Nov 27, 2023
d4f6678
fix: testDecodeMulticallCopy
zhangliugang Nov 27, 2023
56fc498
chore: Update Sources/Web3Core/EthereumNetwork/Request/APIRequest+Met…
JeneaVranceanu Jan 8, 2024
084a2cb
chore: Update Sources/Web3Core/EthereumNetwork/Request/APIRequest+Met…
JeneaVranceanu Jan 8, 2024
e6ff991
chore: added docs; new test for ABI.Element.Function;
JeneaVranceanu Jan 8, 2024
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
64 changes: 58 additions & 6 deletions Sources/Web3Core/Contract/ContractProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ public protocol ContractProtocol {
/// - 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]?
@discardableResult
func decodeReturnData(_ method: String, data: Data) throws -> [String: Any]

/// Decode input arguments of a function.
/// - Parameters:
Expand Down Expand Up @@ -313,13 +314,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: "Function method does not exist.")
zhangliugang marked this conversation as resolved.
Show resolved Hide resolved
}

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: "Found no matched error")
}
default:
throw Web3Error.inputError(desc: "Invalid data count")
JeneaVranceanu marked this conversation as resolved.
Show resolved Hide resolved
}
return methods[method]?.compactMap({ function in
return function.decodeReturnData(data)
}).first
}

public func decodeInputData(_ method: String, data: Data) -> [String: Any]? {
Expand All @@ -339,8 +367,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: "address field is missing")
JeneaVranceanu marked this conversation as resolved.
Show resolved Hide resolved
}
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)
}
}
51 changes: 41 additions & 10 deletions Sources/Web3Core/EthereumABI/ABIElements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,40 @@ extension ABI.Element.Event {
}
}

// MARK: - Decode custom error

extension ABI.Element.EthError {
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)` and `require(expression, string)` calls.
/// These calls are decomposed as `Error(string` error.
zhangliugang marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -232,7 +266,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 @@ -314,25 +348,21 @@ extension ABI.Element.Function {
/// - 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) {
JeneaVranceanu marked this conversation as resolved.
Show resolved Hide resolved
return decodedError
}

public func decodeReturnData(_ data: Data) throws -> [String: Any] {
JeneaVranceanu marked this conversation as resolved.
Show resolved Hide resolved
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.revert("Bytes count must be at least \(outputs.count * 32). Given \(data.count). Decoding will fail.", reason: nil)
}

// 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.revert("Failed to decode at least one value.", reason: nil)
}
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 @@ -412,6 +442,7 @@ extension ABI.Element.Function {
let errors = errors,
let customError = errors[data[data.startIndex ..< data.startIndex + 4].toHexString().stripHexPrefix()] {
var errorResponse: [String: Any] = ["_success": false, "_abortedByRevertOrRequire": true, "_error": customError.errorDeclaration]
// customError.decodeEthError(data[4...])
zhangliugang marked this conversation as resolved.
Show resolved Hide resolved

if (data.count > 32 && !customError.inputs.isEmpty),
let decodedInputs = ABIDecoder.decode(types: customError.inputs, data: Data(data[data.startIndex + 4 ..< data.startIndex + data.count])) {
Expand Down
14 changes: 14 additions & 0 deletions Sources/Web3Core/EthereumABI/ABIParameterTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,20 @@ extension ABI.Element.Event {
}
}

extension ABI.Element.EthError {
public var signature: String {
return "\(name)(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))"
}

public var methodString: String {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like this could be named selector as this is the terminology used in Solidity.
I see that methodString should be rather something like ErrorXyz(uint256,...) and signature should be 0xcafe1234. The same applies for all other types e.g. Function, Event etc.

For now, no action here is required. I'll take a look at the terminology used in Solidity and then we can think about renaming some of our variables.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

or leave the signature unchanged, add a new signatureHash for hash, mark methodEncoding and methodString as deprecated

return String(signature.sha3(.keccak256).prefix(8))
}

public var methodEncoding: Data {
return signature.data(using: .ascii)!.sha3(.keccak256)[0...3]
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the methodEncoding? Maybe I'm forgetting something but it looks unusual.
Any links to docs about it would be appreciated!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, you borrowed it from Function, I guess.
Oh, methodEncoding of Function is from 2018. We need documentation for it for sure.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Screenshot 2023-08-29 at 09 59 39

Copy link
Collaborator

Choose a reason for hiding this comment

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

So it's the same just raw bytes instead of a string.

}

extension ABI.Element.ParameterType: ABIEncoding {
public var abiRepresentation: String {
switch self {
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