Skip to content

Commit edd8e72

Browse files
committed
feat: improve decode error
1 parent d098b87 commit edd8e72

17 files changed

+462
-145
lines changed

Sources/Web3Core/Contract/ContractProtocol.swift

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ public protocol ContractProtocol {
144144
/// - method signature (with or without `0x` prefix, case insensitive): `0xFFffFFff`;
145145
/// - data: non empty bytes to decode;
146146
/// - Returns: dictionary with decoded values. `nil` if decoding failed.
147-
func decodeReturnData(_ method: String, data: Data) -> [String: Any]?
147+
@discardableResult
148+
func decodeReturnData(_ method: String, data: Data) throws -> [String: Any]
148149

149150
/// Decode input arguments of a function.
150151
/// - Parameters:
@@ -313,13 +314,40 @@ extension DefaultContractProtocol {
313314
return bloom.test(topic: event.topic)
314315
}
315316

316-
public func decodeReturnData(_ method: String, data: Data) -> [String: Any]? {
317+
@discardableResult
318+
public func decodeReturnData(_ method: String, data: Data) throws -> [String: Any] {
317319
if method == "fallback" {
318-
return [String: Any]()
320+
return [:]
321+
}
322+
323+
guard let function = methods[method]?.first else {
324+
throw Web3Error.inputError(desc: "Function method does not exist.")
325+
}
326+
327+
switch data.count % 32 {
328+
case 0:
329+
return try function.decodeReturnData(data)
330+
case 4:
331+
let selector = data[0..<4]
332+
if selector.toHexString() == "08c379a0", let reason = ABI.Element.EthError.decodeStringError(data[4...]) {
333+
throw Web3Error.revert("revert(string)` or `require(expression, string)` was executed. reason: \(reason)", reason: reason)
334+
}
335+
else if selector.toHexString() == "4e487b71", let reason = ABI.Element.EthError.decodePanicError(data[4...]) {
336+
let panicCode = String(format: "%02X", Int(reason)).addHexPrefix()
337+
throw Web3Error.revert("Error: call revert exception; VM Exception while processing transaction: reverted with panic code \(panicCode)", reason: panicCode)
338+
}
339+
else if let customError = errors[selector.toHexString().addHexPrefix().lowercased()] {
340+
if let errorArgs = customError.decodeEthError(data[4...]) {
341+
throw Web3Error.revertCustom(customError.signature, errorArgs)
342+
} else {
343+
throw Web3Error.inputError(desc: "Signature matches \(customError.errorDeclaration) but failed to be decoded.")
344+
}
345+
} else {
346+
throw Web3Error.inputError(desc: "Found no matched error")
347+
}
348+
default:
349+
throw Web3Error.inputError(desc: "Invalid data count")
319350
}
320-
return methods[method]?.compactMap({ function in
321-
return function.decodeReturnData(data)
322-
}).first
323351
}
324352

325353
public func decodeInputData(_ method: String, data: Data) -> [String: Any]? {
@@ -339,8 +367,32 @@ extension DefaultContractProtocol {
339367
return function.decodeInputData(Data(data[data.startIndex + 4 ..< data.startIndex + data.count]))
340368
}
341369

370+
public func decodeEthError(_ data: Data) -> [String: Any]? {
371+
guard data.count >= 4,
372+
let err = errors.first(where: { $0.value.methodEncoding == data[0..<4] })?.value else {
373+
return nil
374+
}
375+
return err.decodeEthError(data[4...])
376+
}
377+
342378
public func getFunctionCalled(_ data: Data) -> ABI.Element.Function? {
343379
guard data.count >= 4 else { return nil }
344380
return methods[data[data.startIndex ..< data.startIndex + 4].toHexString().addHexPrefix()]?.first
345381
}
346382
}
383+
384+
extension DefaultContractProtocol {
385+
@discardableResult
386+
public func callStatic(_ method: String, parameters: [Any], provider: Web3Provider) async throws -> [String: Any] {
387+
guard let address = address else {
388+
throw Web3Error.inputError(desc: "address field is missing")
389+
}
390+
guard let data = self.method(method, parameters: parameters, extraData: nil) else {
391+
throw Web3Error.dataError
392+
}
393+
let transaction = CodableTransaction(to: address, data: data)
394+
395+
let result: Data = try await APIRequest.sendRequest(with: provider, for: .call(transaction, .latest)).result
396+
return try decodeReturnData(method, data: result)
397+
}
398+
}

Sources/Web3Core/EthereumABI/ABIElements.swift

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,40 @@ extension ABI.Element.Event {
220220
}
221221
}
222222

223+
// MARK: - Decode custom error
224+
225+
extension ABI.Element.EthError {
226+
public func decodeEthError(_ data: Data) -> [String: Any]? {
227+
guard inputs.count * 32 <= data.count,
228+
let decoded = ABIDecoder.decode(types: inputs, data: data) else {
229+
return nil
230+
}
231+
232+
var result = [String: Any]()
233+
for (index, out) in inputs.enumerated() {
234+
result["\(index)"] = decoded[index]
235+
if !out.name.isEmpty {
236+
result[out.name] = decoded[index]
237+
}
238+
}
239+
return result
240+
}
241+
242+
/// Decodes `revert(string)` and `require(expression, string)` calls.
243+
/// These calls are decomposed as `Error(string` error.
244+
public static func decodeStringError(_ data: Data) -> String? {
245+
let decoded = ABIDecoder.decode(types: [.init(name: "", type: .string)], data: data)
246+
return decoded?.first as? String
247+
}
248+
249+
/// Decodes `Panic(uint256)` errors.
250+
/// 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
251+
public static func decodePanicError(_ data: Data) -> BigUInt? {
252+
let decoded = ABIDecoder.decode(types: [.init(name: "", type: .uint(bits: 256))], data: data)
253+
return decoded?.first as? BigUInt
254+
}
255+
}
256+
223257
// MARK: - Function input/output decoding
224258

225259
extension ABI.Element {
@@ -232,7 +266,7 @@ extension ABI.Element {
232266
case .fallback:
233267
return nil
234268
case .function(let function):
235-
return function.decodeReturnData(data)
269+
return try? function.decodeReturnData(data)
236270
case .receive:
237271
return nil
238272
case .error:
@@ -314,25 +348,21 @@ extension ABI.Element.Function {
314348
/// - next 32 bytes are the error message length;
315349
/// - the next N bytes, where N >= 32, are the message bytes
316350
/// - the rest are 0 bytes padding.
317-
public func decodeReturnData(_ data: Data, errors: [String: ABI.Element.EthError]? = nil) -> [String: Any] {
318-
if let decodedError = decodeErrorResponse(data, errors: errors) {
319-
return decodedError
320-
}
321-
351+
public func decodeReturnData(_ data: Data) throws -> [String: Any] {
322352
guard !outputs.isEmpty else {
323353
NSLog("Function doesn't have any output types to decode given data.")
324-
return ["_success": true]
354+
return [:]
325355
}
326356

327357
guard outputs.count * 32 <= data.count else {
328-
return ["_success": false, "_failureReason": "Bytes count must be at least \(outputs.count * 32). Given \(data.count). Decoding will fail."]
358+
throw Web3Error.revert("Bytes count must be at least \(outputs.count * 32). Given \(data.count). Decoding will fail.", reason: nil)
329359
}
330360

331361
// TODO: need improvement - we should be able to tell which value failed to be decoded
332362
guard let values = ABIDecoder.decode(types: outputs, data: data) else {
333-
return ["_success": false, "_failureReason": "Failed to decode at least one value."]
363+
throw Web3Error.revert("Failed to decode at least one value.", reason: nil)
334364
}
335-
var returnArray: [String: Any] = ["_success": true]
365+
var returnArray: [String: Any] = [:]
336366
for i in outputs.indices {
337367
returnArray["\(i)"] = values[i]
338368
if !outputs[i].name.isEmpty {
@@ -412,6 +442,7 @@ extension ABI.Element.Function {
412442
let errors = errors,
413443
let customError = errors[data[data.startIndex ..< data.startIndex + 4].toHexString().stripHexPrefix()] {
414444
var errorResponse: [String: Any] = ["_success": false, "_abortedByRevertOrRequire": true, "_error": customError.errorDeclaration]
445+
// customError.decodeEthError(data[4...])
415446

416447
if (data.count > 32 && !customError.inputs.isEmpty),
417448
let decodedInputs = ABIDecoder.decode(types: customError.inputs, data: Data(data[data.startIndex + 4 ..< data.startIndex + data.count])) {

Sources/Web3Core/EthereumABI/ABIParameterTypes.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,20 @@ extension ABI.Element.Event {
192192
}
193193
}
194194

195+
extension ABI.Element.EthError {
196+
public var signature: String {
197+
return "\(name)(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))"
198+
}
199+
200+
public var methodString: String {
201+
return String(signature.sha3(.keccak256).prefix(8))
202+
}
203+
204+
public var methodEncoding: Data {
205+
return signature.data(using: .ascii)!.sha3(.keccak256)[0...3]
206+
}
207+
}
208+
195209
extension ABI.Element.ParameterType: ABIEncoding {
196210
public var abiRepresentation: String {
197211
switch self {

Sources/Web3Core/EthereumABI/Sequence+ABIExtension.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public extension Sequence where Element == ABI.Element {
5656
var errors = [String: ABI.Element.EthError]()
5757
for case let .error(error) in self {
5858
errors[error.name] = error
59+
errors[error.signature] = error
60+
errors[error.methodString.addHexPrefix().lowercased()] = error
5961
}
6062
return errors
6163
}

Sources/Web3Core/EthereumNetwork/Request/APIRequest+ComputedProperties.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@ extension APIRequest {
1212
.POST
1313
}
1414

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

2219
var parameters: [RequestParameter] {

0 commit comments

Comments
 (0)