Skip to content

Commit ef28c1f

Browse files
Merge pull request #831 from zhangliugang/feat/decode-error
2 parents bab86ca + e6ff991 commit ef28c1f

23 files changed

+706
-298
lines changed

Sources/Web3Core/Contract/ContractProtocol.swift

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,12 @@ public protocol ContractProtocol {
143143
/// - name with arguments:`myFunction(uint256)`.
144144
/// - method signature (with or without `0x` prefix, case insensitive): `0xFFffFFff`;
145145
/// - data: non empty bytes to decode;
146-
/// - Returns: dictionary with decoded values. `nil` if decoding failed.
147-
func decodeReturnData(_ method: String, data: Data) -> [String: Any]?
146+
/// - Returns: dictionary with decoded values.
147+
/// - Throws:
148+
/// - `Web3Error.revert(String, String?)` when function call aborted by `revert(string)` and `require(expression, string)`.
149+
/// - `Web3Error.revertCustom(String, Dictionary)` when function call aborted by `revert CustomError()`.
150+
@discardableResult
151+
func decodeReturnData(_ method: String, data: Data) throws -> [String: Any]
148152

149153
/// Decode input arguments of a function.
150154
/// - Parameters:
@@ -320,13 +324,40 @@ extension DefaultContractProtocol {
320324
return bloom.test(topic: event.topic)
321325
}
322326

323-
public func decodeReturnData(_ method: String, data: Data) -> [String: Any]? {
327+
@discardableResult
328+
public func decodeReturnData(_ method: String, data: Data) throws -> [String: Any] {
324329
if method == "fallback" {
325-
return [String: Any]()
330+
return [:]
331+
}
332+
333+
guard let function = methods[method]?.first else {
334+
throw Web3Error.inputError(desc: "Make sure ABI you use contains '\(method)' method.")
335+
}
336+
337+
switch data.count % 32 {
338+
case 0:
339+
return try function.decodeReturnData(data)
340+
case 4:
341+
let selector = data[0..<4]
342+
if selector.toHexString() == "08c379a0", let reason = ABI.Element.EthError.decodeStringError(data[4...]) {
343+
throw Web3Error.revert("revert(string)` or `require(expression, string)` was executed. reason: \(reason)", reason: reason)
344+
}
345+
else if selector.toHexString() == "4e487b71", let reason = ABI.Element.EthError.decodePanicError(data[4...]) {
346+
let panicCode = String(format: "%02X", Int(reason)).addHexPrefix()
347+
throw Web3Error.revert("Error: call revert exception; VM Exception while processing transaction: reverted with panic code \(panicCode)", reason: panicCode)
348+
}
349+
else if let customError = errors[selector.toHexString().addHexPrefix().lowercased()] {
350+
if let errorArgs = customError.decodeEthError(data[4...]) {
351+
throw Web3Error.revertCustom(customError.signature, errorArgs)
352+
} else {
353+
throw Web3Error.inputError(desc: "Signature matches \(customError.errorDeclaration) but failed to be decoded.")
354+
}
355+
} else {
356+
throw Web3Error.inputError(desc: "Make sure ABI you use contains error that can match signature: 0x\(selector.toHexString())")
357+
}
358+
default:
359+
throw Web3Error.inputError(desc: "Given data has invalid bytes count.")
326360
}
327-
return methods[method]?.compactMap({ function in
328-
return function.decodeReturnData(data)
329-
}).first
330361
}
331362

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

380+
public func decodeEthError(_ data: Data) -> [String: Any]? {
381+
guard data.count >= 4,
382+
let err = errors.first(where: { $0.value.methodEncoding == data[0..<4] })?.value else {
383+
return nil
384+
}
385+
return err.decodeEthError(data[4...])
386+
}
387+
349388
public func getFunctionCalled(_ data: Data) -> ABI.Element.Function? {
350389
guard data.count >= 4 else { return nil }
351390
return methods[data[data.startIndex ..< data.startIndex + 4].toHexString().addHexPrefix()]?.first
352391
}
353392
}
393+
394+
extension DefaultContractProtocol {
395+
@discardableResult
396+
public func callStatic(_ method: String, parameters: [Any], provider: Web3Provider) async throws -> [String: Any] {
397+
guard let address = address else {
398+
throw Web3Error.inputError(desc: "RPC failed: contract is missing an address.")
399+
}
400+
guard let data = self.method(method, parameters: parameters, extraData: nil) else {
401+
throw Web3Error.dataError
402+
}
403+
let transaction = CodableTransaction(to: address, data: data)
404+
405+
let result: Data = try await APIRequest.sendRequest(with: provider, for: .call(transaction, .latest)).result
406+
return try decodeReturnData(method, data: result)
407+
}
408+
}

Sources/Web3Core/EthereumABI/ABIElements.swift

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ extension ABI.Element.Constructor {
202202
extension ABI.Element.Function {
203203

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

295+
// MARK: - Decode custom error
296+
297+
extension ABI.Element.EthError {
298+
/// Decodes `revert CustomError(_)` calls.
299+
/// - Parameters:
300+
/// - data: bytes returned by a function call that stripped error signature hash.
301+
/// - Returns: a dictionary containing decoded data mappend to indices and names of returned values or nil if decoding failed.
302+
public func decodeEthError(_ data: Data) -> [String: Any]? {
303+
guard inputs.count * 32 <= data.count,
304+
let decoded = ABIDecoder.decode(types: inputs, data: data) else {
305+
return nil
306+
}
307+
308+
var result = [String: Any]()
309+
for (index, out) in inputs.enumerated() {
310+
result["\(index)"] = decoded[index]
311+
if !out.name.isEmpty {
312+
result[out.name] = decoded[index]
313+
}
314+
}
315+
return result
316+
}
317+
318+
/// Decodes `revert(string)` or `require(expression, string)` calls.
319+
/// These calls are decomposed as `Error(string)` error.
320+
public static func decodeStringError(_ data: Data) -> String? {
321+
let decoded = ABIDecoder.decode(types: [.init(name: "", type: .string)], data: data)
322+
return decoded?.first as? String
323+
}
324+
325+
/// Decodes `Panic(uint256)` errors.
326+
/// 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
327+
public static func decodePanicError(_ data: Data) -> BigUInt? {
328+
let decoded = ABIDecoder.decode(types: [.init(name: "", type: .uint(bits: 256))], data: data)
329+
return decoded?.first as? BigUInt
330+
}
331+
}
332+
295333
// MARK: - Function input/output decoding
296334

297335
extension ABI.Element {
@@ -304,7 +342,7 @@ extension ABI.Element {
304342
case .fallback:
305343
return nil
306344
case .function(let function):
307-
return function.decodeReturnData(data)
345+
return try? function.decodeReturnData(data)
308346
case .receive:
309347
return nil
310348
case .error:
@@ -337,74 +375,38 @@ extension ABI.Element.Function {
337375
return ABIDecoder.decodeInputData(rawData, methodEncoding: methodEncoding, inputs: inputs)
338376
}
339377

340-
/// Decodes data returned by a function call. Able to decode `revert(string)`, `revert CustomError(...)` and `require(expression, string)` calls.
378+
/// Decodes data returned by a function call.
341379
/// - Parameters:
342380
/// - data: bytes returned by a function call;
343-
/// - errors: optional dictionary of known errors that could be returned by the function you called. Used to decode the error information.
344381
/// - Returns: a dictionary containing decoded data mappend to indices and names of returned values if these are not `nil`.
345-
/// If `data` is an error response returns dictionary containing all available information about that specific error. Read more for details.
382+
/// - Throws:
383+
/// - `Web3Error.processingError(desc: String)` when decode process failed.
346384
///
347385
/// Return cases:
348-
/// - when no `outputs` declared and `data` is not an error response:
386+
/// - when no `outputs` declared:
349387
/// ```swift
350-
/// ["_success": true]
388+
/// [:]
351389
/// ```
352390
/// - when `outputs` declared and decoding completed successfully:
353391
/// ```swift
354-
/// ["_success": true, "0": value_1, "1": value_2, ...]
392+
/// ["0": value_1, "1": value_2, ...]
355393
/// ```
356394
/// Additionally this dictionary will have mappings to output names if these names are specified in the ABI;
357-
/// - function call was aborted using `revert(message)` or `require(expression, message)`:
358-
/// ```swift
359-
/// ["_success": false, "_abortedByRevertOrRequire": true, "_errorMessage": message]`
360-
/// ```
361-
/// - function call was aborted using `revert CustomMessage()` and `errors` argument contains the ABI of that custom error type:
362-
/// ```swift
363-
/// ["_success": false,
364-
/// "_abortedByRevertOrRequire": true,
365-
/// "_error": error_name_and_types, // e.g. `MyCustomError(uint256, address senderAddress)`
366-
/// "0": error_arg1,
367-
/// "1": error_arg2,
368-
/// ...,
369-
/// "error_arg1_name": error_arg1, // Only named arguments will be mapped to their names, e.g. `"senderAddress": EthereumAddress`
370-
/// "error_arg2_name": error_arg2, // Otherwise, you can query them by position index.
371-
/// ...]
372-
/// ```
373-
/// - in case of any error:
374-
/// ```swift
375-
/// ["_success": false, "_failureReason": String]
376-
/// ```
377-
/// Error reasons include:
378-
/// - `outputs` declared but at least one value failed to be decoded;
379-
/// - `data.count` is less than `outputs.count * 32`;
380-
/// - `outputs` defined and `data` is empty;
381-
/// - `data` represent reverted transaction
382-
///
383-
/// How `revert(string)` and `require(expression, string)` return value is decomposed:
384-
/// - `08C379A0` function selector for `Error(string)`;
385-
/// - next 32 bytes are the data offset;
386-
/// - next 32 bytes are the error message length;
387-
/// - the next N bytes, where N >= 32, are the message bytes
388-
/// - the rest are 0 bytes padding.
389-
public func decodeReturnData(_ data: Data, errors: [String: ABI.Element.EthError]? = nil) -> [String: Any] {
390-
if let decodedError = decodeErrorResponse(data, errors: errors) {
391-
return decodedError
392-
}
393-
395+
public func decodeReturnData(_ data: Data) throws -> [String: Any] {
394396
guard !outputs.isEmpty else {
395397
NSLog("Function doesn't have any output types to decode given data.")
396-
return ["_success": true]
398+
return [:]
397399
}
398400

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

403405
// TODO: need improvement - we should be able to tell which value failed to be decoded
404406
guard let values = ABIDecoder.decode(types: outputs, data: data) else {
405-
return ["_success": false, "_failureReason": "Failed to decode at least one value."]
407+
throw Web3Error.processingError(desc: "Failed to decode at least one value.")
406408
}
407-
var returnArray: [String: Any] = ["_success": true]
409+
var returnArray: [String: Any] = [:]
408410
for i in outputs.indices {
409411
returnArray["\(i)"] = values[i]
410412
if !outputs[i].name.isEmpty {
@@ -453,6 +455,7 @@ extension ABI.Element.Function {
453455
/// // "_parsingError" is optional and is present only if decoding of custom error arguments failed
454456
/// "_parsingError": "Data matches MyCustomError(uint256, address senderAddress) but failed to be decoded."]
455457
/// ```
458+
@available(*, deprecated, message: "Use decode function from `ABI.Element.EthError` instead")
456459
public func decodeErrorResponse(_ data: Data, errors: [String: ABI.Element.EthError]? = nil) -> [String: Any]? {
457460
/// If data is empty and outputs are expected it is treated as a `require(expression)` or `revert()` call with no message.
458461
/// In solidity `require(false)` and `revert()` calls return empty error response.

Sources/Web3Core/EthereumABI/ABIParameterTypes.swift

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,31 +168,79 @@ extension ABI.Element.ParameterType: Equatable {
168168
}
169169

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

176+
/// Function selector, e.g. `"cafe1234"`. Without hex prefix `0x`.
177+
@available(*, deprecated, renamed: "selector", message: "Please, use 'selector' property instead.")
175178
public var methodString: String {
179+
return selector
180+
}
181+
182+
/// Function selector, e.g. `"cafe1234"`. Without hex prefix `0x`.
183+
public var selector: String {
176184
return String(signature.sha3(.keccak256).prefix(8))
177185
}
178186

187+
/// Function selector (e.g. `0xcafe1234`) but as raw bytes.
188+
@available(*, deprecated, renamed: "selectorEncoded", message: "Please, use 'selectorEncoded' property instead.")
179189
public var methodEncoding: Data {
180-
return signature.data(using: .ascii)!.sha3(.keccak256)[0...3]
190+
return selectorEncoded
191+
}
192+
193+
/// Function selector (e.g. `0xcafe1234`) but as raw bytes.
194+
public var selectorEncoded: Data {
195+
return Data.fromHex(selector)!
181196
}
182197
}
183198

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

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

212+
extension ABI.Element.EthError {
213+
/// String representation of an error, e.g. `TrasferFailed(address)`.
214+
public var signature: String {
215+
return "\(name)(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))"
216+
}
217+
218+
/// Error selector, e.g. `"cafe1234"`. Without hex prefix `0x`.
219+
@available(*, deprecated, renamed: "selector", message: "Please, use 'selector' property instead.")
220+
public var methodString: String {
221+
return selector
222+
}
223+
224+
/// Error selector, e.g. `"cafe1234"`. Without hex prefix `0x`.
225+
public var selector: String {
226+
return String(signature.sha3(.keccak256).prefix(8))
227+
}
228+
229+
/// Error selector (e.g. `0xcafe1234`) but as raw bytes.
230+
@available(*, deprecated, renamed: "selectorEncoded", message: "Please, use 'selectorEncoded' property instead.")
231+
public var methodEncoding: Data {
232+
return selectorEncoded
233+
}
234+
235+
/// Error selector (e.g. `0xcafe1234`) but as raw bytes.
236+
public var selectorEncoded: Data {
237+
return Data.fromHex(selector)!
238+
}
239+
}
240+
195241
extension ABI.Element.ParameterType: ABIEncoding {
242+
243+
/// Returns a valid solidity type like `address`, `uint128` or any other built-in type from Solidity.
196244
public var abiRepresentation: String {
197245
switch self {
198246
case .uint(let bits):

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: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,12 @@
88
import Foundation
99

1010
extension APIRequest {
11-
var method: REST {
11+
public var method: REST {
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)