Skip to content

Commit aa32c36

Browse files
chore: decodeReturnData refactoring + documentation for it
1 parent 6247628 commit aa32c36

File tree

1 file changed

+60
-57
lines changed

1 file changed

+60
-57
lines changed

Sources/Core/EthereumABI/ABIElements.swift

Lines changed: 60 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -264,72 +264,75 @@ extension ABI.Element.Function {
264264
return Core.decodeInputData(rawData, methodEncoding: methodEncoding, inputs: inputs)
265265
}
266266

267-
public func decodeReturnData(_ data: Data) -> [String: Any]? {
268-
// the response size greater than equal 100 bytes, when read function aborted by "require" statement.
269-
// if "require" statement has no message argument, the response is empty (0 byte).
270-
if data.bytes.count >= 100 {
271-
let check00_31 = BigUInt("08C379A000000000000000000000000000000000000000000000000000000000", radix: 16)!
272-
let check32_63 = BigUInt("0000002000000000000000000000000000000000000000000000000000000000", radix: 16)!
273-
274-
// check data[00-31] and data[32-63]
275-
if check00_31 == BigUInt(data[0...31]) && check32_63 == BigUInt(data[32...63]) {
276-
// data.bytes[64-67] contains the length of require message
277-
let len = (Int(data.bytes[64])<<24) | (Int(data.bytes[65])<<16) | (Int(data.bytes[66])<<8) | Int(data.bytes[67])
278-
279-
let message = String(bytes: data.bytes[68..<(68+len)], encoding: .utf8)!
280-
281-
print("read function aborted by require statement: \(message)")
282-
283-
var returnArray = [String: Any]()
284-
285-
// set infomation
286-
returnArray["_abortedByRequire"] = true
287-
returnArray["_errorMessageFromRequire"] = message
288-
289-
// set empty values
290-
for i in 0 ..< outputs.count {
291-
let name = "\(i)"
292-
returnArray[name] = outputs[i].type.emptyValue
293-
if outputs[i].name != "" {
294-
returnArray[outputs[i].name] = outputs[i].type.emptyValue
295-
}
296-
}
267+
/// Decodes data returned by a function call. Able to decode `revert("...")` calls.
268+
/// - Parameter data: bytes returned by a function call.
269+
/// - Returns: a dictionary containing returned data mappend to indices and names of returned values if these are not `nil`.
270+
/// Return cases:
271+
/// - when no `outputs` declared: returning `["_success": true]`;
272+
/// - when `outputs` declared and decoding completed successfully: returning `["_success": true, "0": value_1, "1": value_2, ...]`.
273+
/// Additionally this dictionary will have mappings to output names if these names are specified in the ABI.
274+
/// - function call was aborted using `require(some_string_error_message)`: returning `["_success": false, "_abortedByRequire": true, "_errorMessageFromRequire": error_message]`.
275+
/// - in case of any error: returning `["_success": false, "_failureReason": String]`;
276+
/// Error reasons include:
277+
/// - `outputs` declared but at least one value failed to be decoded;
278+
/// - `data.count` is less than `outputs.count * 32`;
279+
/// - `outputs` defined and `data` is empty;
280+
/// - `data` represent reverted transaction
281+
public func decodeReturnData(_ data: Data) -> [String: Any] {
282+
guard !outputs.isEmpty else {
283+
NSLog("Function doesn't have any output types to decode given data.")
284+
return ["_success": true]
285+
}
297286

298-
return returnArray
299-
}
287+
/// If data is empty and outputs are expected it is treated as a `requite(expression)` call with no message.
288+
/// In solidity `require(expression)` call, if `expresison` returns `false`, results in an empty response.
289+
if data.count == 0 && !outputs.isEmpty {
290+
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?"]
300291
}
301292

302-
var returnArray = [String: Any]()
293+
guard outputs.count * 32 <= data.count else {
294+
return ["_success": false, "_failureReason": "Bytes count must be at least \(outputs.count * 32). Given \(data.count). Decoding will fail."]
295+
}
303296

304-
// the "require" statement with no message argument will be caught here
305-
if data.count == 0 && outputs.count == 1 {
306-
let name = "0"
307-
let value = outputs[0].type.emptyValue
308-
returnArray[name] = value
309-
if outputs[0].name != "" {
310-
returnArray[outputs[0].name] = value
311-
}
312-
} else {
313-
guard outputs.count * 32 <= data.count else { return nil }
314-
315-
var i = 0
316-
guard let values = ABIDecoder.decode(types: outputs, data: data) else { return nil }
317-
for output in outputs {
318-
let name = "\(i)"
319-
returnArray[name] = values[i]
320-
if output.name != "" {
321-
returnArray[output.name] = values[i]
297+
/// How `require(expression, string)` return value is decomposed:
298+
/// - `08C379A0` function selector for Error(string);
299+
/// - next 32 bytes are the data offset;
300+
/// - next 32 bytes are the error message length;
301+
/// - the next N bytes, where N is the int value
302+
///
303+
/// Data offset must be present. Hexadecimal value of `0000...0020` is 32 in decimal. Reasoning for `BigInt(...) == 32`.
304+
if data[0..<4] == Data.fromHex("08C379A0"),
305+
data.bytes.count >= 100,
306+
BigInt(data[4..<36]) == 32,
307+
let messageLength = Int(Data(data[36..<68]).toHexString(), radix: 16),
308+
let message = String(bytes: data.bytes[68..<(68+messageLength)], encoding: .utf8) {
309+
var returnArray: [String: Any] = ["_success": false,
310+
"_failureReason": "`require` was executed.",
311+
"_abortedByRequire": true,
312+
"_errorMessageFromRequire": message]
313+
314+
// set empty values
315+
for i in outputs.indices {
316+
returnArray["\(i)"] = outputs[i].type.emptyValue
317+
if !outputs[i].name.isEmpty {
318+
returnArray[outputs[i].name] = outputs[i].type.emptyValue
322319
}
323-
i = i + 1
324320
}
325-
// set a flag to detect the request succeeded
326-
}
327321

328-
if returnArray.isEmpty && !outputs.isEmpty {
329-
return nil
322+
return returnArray
330323
}
331324

332-
returnArray["_success"] = true
325+
// TODO: need improvement - we should be able to tell which value failed to be decoded
326+
guard let values = ABIDecoder.decode(types: outputs, data: data) else {
327+
return ["_success": false, "_failureReason": "Failed to decode at least one value."]
328+
}
329+
var returnArray: [String: Any] = ["_success": true]
330+
for i in outputs.indices {
331+
returnArray["\(i)"] = values[i]
332+
if !outputs[i].name.isEmpty {
333+
returnArray[outputs[i].name] = values[i]
334+
}
335+
}
333336
return returnArray
334337
}
335338
}

0 commit comments

Comments
 (0)