Skip to content

Commit 3151678

Browse files
feat: decodeReturnData is split into decodeReturnData and decodeErrorResponse
1 parent 7257f59 commit 3151678

File tree

1 file changed

+138
-46
lines changed

1 file changed

+138
-46
lines changed

Sources/Core/EthereumABI/ABIElements.swift

Lines changed: 138 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -265,60 +265,65 @@ extension ABI.Element.Function {
265265
return Core.decodeInputData(rawData, methodEncoding: methodEncoding, inputs: inputs)
266266
}
267267

268-
/// Decodes data returned by a function call. Able to decode `revert(message)`, `revert CustomError(...)` and `require(expression, message)` calls.
269-
/// - Parameter data: bytes returned by a function call.
270-
/// - Returns: a dictionary containing returned data mappend to indices and names of returned values if these are not `nil`.
268+
/// Decodes data returned by a function call. Able to decode `revert(string)`, `revert CustomError(...)` and `require(expression, string)` calls.
269+
/// - Parameters:
270+
/// - data: bytes returned by a function call;
271+
/// - errors: optional dictionary of known errors that could be returned by the function you called. Used to decode the error information.
272+
/// - Returns: a dictionary containing decoded data mappend to indices and names of returned values if these are not `nil`.
273+
/// If `data` is an error response returns dictionary containing all available information about that specific error. Read more for details.
274+
///
271275
/// Return cases:
272-
/// - when no `outputs` declared: returning `["_success": true]`;
273-
/// - when `outputs` declared and decoding completed successfully: returning `["_success": true, "0": value_1, "1": value_2, ...]`.
274-
/// Additionally this dictionary will have mappings to output names if these names are specified in the ABI.
275-
/// - function call was aborted using `require(some_string_error_message)`: returning `["_success": false, "_abortedByRequire": true, "_errorMessageFromRequire": error_message]`.
276-
/// - in case of any error: returning `["_success": false, "_failureReason": String]`;
277-
/// Error reasons include:
278-
/// - `outputs` declared but at least one value failed to be decoded;
279-
/// - `data.count` is less than `outputs.count * 32`;
280-
/// - `outputs` defined and `data` is empty;
281-
/// - `data` represent reverted transaction
282-
public func decodeReturnData(_ data: Data) -> [String: Any] {
283-
/// How `require(expression, string)` return value is decomposed:
284-
/// - `08C379A0` function selector for Error(string);
285-
/// - next 32 bytes are the data offset;
286-
/// - next 32 bytes are the error message length;
287-
/// - the next N bytes, where N is the int value
288-
///
289-
/// Data offset must be present. Hexadecimal value of `0000...0020` is 32 in decimal. Reasoning for `BigInt(...) == 32`.
290-
if data.bytes.count >= 100,
291-
data[0..<4] == Data.fromHex("08C379A0"),
292-
BigInt(data[4..<36]) == 32,
293-
let messageLength = Int(Data(data[36..<68]).toHexString(), radix: 16),
294-
let message = String(bytes: data.bytes[68..<(68+messageLength)], encoding: .utf8) {
295-
var returnArray: [String: Any] = ["_success": false,
296-
"_failureReason": "`require` was executed.",
297-
"_abortedByRequire": true,
298-
"_errorMessageFromRequire": message]
299-
300-
// set empty values
301-
for i in outputs.indices {
302-
returnArray["\(i)"] = outputs[i].type.emptyValue
303-
if !outputs[i].name.isEmpty {
304-
returnArray[outputs[i].name] = outputs[i].type.emptyValue
305-
}
306-
}
307-
308-
return returnArray
276+
/// - when no `outputs` declared and `data` is not an error response:
277+
///```swift
278+
///["_success": true]
279+
///```
280+
/// - when `outputs` declared and decoding completed successfully:
281+
///```swift
282+
///["_success": true, "0": value_1, "1": value_2, ...]
283+
///```
284+
///Additionally this dictionary will have mappings to output names if these names are specified in the ABI;
285+
/// - function call was aborted using `revert(message)` or `require(expression, message)`:
286+
///```swift
287+
///["_success": false, "_abortedByRevertOrRequire": true, "_errorMessage": message]`
288+
///```
289+
/// - function call was aborted using `revert CustomMessage()` and `errors` argument contains the ABI of that custom error type:
290+
///```swift
291+
///["_success": false,
292+
///"_abortedByRevertOrRequire": true,
293+
///"_error": error_name_and_types, // e.g. `MyCustomError(uint256, address senderAddress)`
294+
///"0": error_arg1,
295+
///"1": error_arg2,
296+
///...,
297+
///"error_arg1_name": error_arg1, // Only named arguments will be mapped to their names, e.g. `"senderAddress": EthereumAddress`
298+
///"error_arg2_name": error_arg2, // Otherwise, you can query them by position index.
299+
///...]
300+
///```
301+
///- in case of any error:
302+
///```swift
303+
///["_success": false, "_failureReason": String]
304+
///```
305+
///Error reasons include:
306+
/// - `outputs` declared but at least one value failed to be decoded;
307+
/// - `data.count` is less than `outputs.count * 32`;
308+
/// - `outputs` defined and `data` is empty;
309+
/// - `data` represent reverted transaction
310+
///
311+
/// How `revert(string)` and `require(expression, string)` return value is decomposed:
312+
/// - `08C379A0` function selector for `Error(string)`;
313+
/// - next 32 bytes are the data offset;
314+
/// - next 32 bytes are the error message length;
315+
/// - the next N bytes, where N >= 32, are the message bytes
316+
/// - 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
309320
}
310321

311322
guard !outputs.isEmpty else {
312323
NSLog("Function doesn't have any output types to decode given data.")
313324
return ["_success": true]
314325
}
315326

316-
/// If data is empty and outputs are expected it is treated as a `requite(expression)` call with no message.
317-
/// In solidity `require(expression)` call, if `expresison` returns `false`, results in an empty response.
318-
if data.count == 0 && !outputs.isEmpty {
319-
return ["_success": false, "_failureReason": "Cannot decode empty data. \(outputs.count) outputs are expected: \(outputs.map { $0.type.abiRepresentation }). Was this a result of en empty `require(expression)` call?"]
320-
}
321-
322327
guard outputs.count * 32 <= data.count else {
323328
return ["_success": false, "_failureReason": "Bytes count must be at least \(outputs.count * 32). Given \(data.count). Decoding will fail."]
324329
}
@@ -336,6 +341,93 @@ extension ABI.Element.Function {
336341
}
337342
return returnArray
338343
}
344+
345+
/// Decodes `revert(string)`, `revert CustomError(...)` and `require(expression, string)` calls.
346+
/// If `data` is empty and `outputs` are not empty it's considered that data is a result of `revert()` or `require(false)`.
347+
/// - Parameters:
348+
/// - data: returned function call data to decode;
349+
/// - errors: optional known errors that could be thrown by the function you called.
350+
/// - Returns: dictionary containing information about the error thrown by the function call.
351+
///
352+
/// What could be returned:
353+
/// - `nil` if data doesn't represent an error or it failed to be mapped to any of the `errors` or `Error(string)` types;
354+
/// - `nil` is `data.isEmpty` and `outputs.isEmpty`;
355+
/// - `data.isEmpty` and `!outputs.isEmpty`:
356+
/// ```swift
357+
/// ["_success": false,
358+
/// "_failureReason": "Cannot decode empty data. X outputs are expected: [outputs_types]. Was this a result of en empty `require(false)` or `revert()` call?"]
359+
/// ```
360+
/// - function call was aborted using `revert(message)` or `require(expression, message)`:
361+
/// ```swift
362+
/// ["_success": false, "_abortedByRevertOrRequire": true, "_errorMessage": message]`
363+
/// ```
364+
/// - function call was aborted using `revert CustomMessage()` and `errors` argument contains the ABI of that custom error type:
365+
/// ```swift
366+
/// ["_success": false,
367+
/// "_abortedByRevertOrRequire": true,
368+
/// "_error": error_name_and_types, // e.g. `MyCustomError(uint256, address senderAddress)`
369+
/// "0": error_arg1,
370+
/// "1": error_arg2,
371+
/// ...,
372+
/// "error_arg1_name": error_arg1, // Only named arguments will be mapped to their names, e.g. `"senderAddress": EthereumAddress`
373+
/// "error_arg2_name": error_arg2, // Otherwise, you can query them by position index.
374+
/// ...]
375+
///
376+
/// /// or if custo error found but decoding failed
377+
/// ["_success": false,
378+
/// "_abortedByRevertOrRequire": true,
379+
/// // "_error" can contain value like `MyCustomError(uint256, address senderAddress)`
380+
/// "_error": error_name_and_types,
381+
/// // "_parsingError" is optional and is present only if decoding of custom error arguments failed
382+
/// "_parsingError": "Data matches MyCustomError(uint256, address senderAddress) but failed to be decoded."]
383+
/// ```
384+
public func decodeErrorResponse(_ data: Data, errors: [String: ABI.Element.EthError]? = nil) -> [String: Any]? {
385+
/// If data is empty and outputs are expected it is treated as a `require(expression)` or `revert()` call with no message.
386+
/// In solidity `require(false)` and `revert()` calls return empty error response.
387+
if data.isEmpty && !outputs.isEmpty {
388+
return ["_success": false, "_failureReason": "Cannot decode empty data. \(outputs.count) outputs are expected: \(outputs.map { $0.type.abiRepresentation }). Was this a result of en empty `require(false)` or `revert()` call?"]
389+
}
390+
391+
/// Explanation of this condition:
392+
/// When `revert(string)` or `require(false, string)` are called in soliditiy they produce
393+
/// an error, specifically an instance of default `Error(string)` type.
394+
/// 1) The total number of bytes returned are at least 100.
395+
/// 2) The function selector for `Error(string)` is `08C379A0`;
396+
/// 3) Data offset must be present. Hexadecimal value of `0000...0020` is 32 in decimal. Reasoning for `BigInt(...) == 32`.
397+
/// 4) `messageLength` is used to determine where message bytes end to decode string correctly.
398+
/// 5) The rest of the `data` must be 0 bytes or empty.
399+
if data.bytes.count >= 100,
400+
Data(data[0..<4]) == Data.fromHex("08C379A0"),
401+
BigInt(data[4..<36]) == 32,
402+
let messageLength = Int(Data(data[36..<68]).toHexString(), radix: 16),
403+
let message = String(bytes: data.bytes[68..<(68+messageLength)], encoding: .utf8),
404+
(68+messageLength == data.count || data.bytes[68+messageLength..<data.count].reduce(0) { $0 + $1 } == 0) {
405+
return ["_success": false,
406+
"_failureReason": "`revert(string)` or `require(expression, string)` was executed.",
407+
"_abortedByRevertOrRequire": true,
408+
"_errorMessage": message]
409+
}
410+
411+
if data.count >= 4,
412+
let errors = errors,
413+
let customError = errors[data[0..<4].toHexString().stripHexPrefix()] {
414+
var errorResponse: [String: Any] = ["_success": false, "_abortedByRevertOrRequire": true, "_error": customError.errorDeclaration]
415+
416+
if (data.count > 32 && !customError.inputs.isEmpty),
417+
let decodedInputs = ABIDecoder.decode(types: customError.inputs, data: Data(data[4..<data.count])) {
418+
for idx in decodedInputs.indices {
419+
errorResponse["\(idx)"] = decodedInputs[idx]
420+
if !customError.inputs[idx].name.isEmpty {
421+
errorResponse[customError.inputs[idx].name] = decodedInputs[idx]
422+
}
423+
}
424+
} else if !customError.inputs.isEmpty {
425+
errorResponse["_parsingError"] = "Data matches \(customError.errorDeclaration) but failed to be decoded."
426+
}
427+
return errorResponse
428+
}
429+
return nil
430+
}
339431
}
340432

341433
extension ABI.Element.Constructor {

0 commit comments

Comments
 (0)