Skip to content

Commit b25fac0

Browse files
Merge branch 'develop' into chore/abiencoder-docs+tests
2 parents 764bee0 + 0070078 commit b25fac0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+920
-614
lines changed

.github/pull_request_template.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
## **Summary of Changes**
22

3-
43
Fixes # _(if applicable - add the number of issue this PR addresses)_
54

6-
 
7-
85
## **Test Data or Screenshots**
96

10-
 
11-
127
###### _By submitting this pull request, you are confirming the following:_
138

149
- I have reviewed the [Contribution Guidelines](https://github.com/web3swift-team/web3swift/blob/develop/CONTRIBUTION.md).

Sources/Core/Contract/ContractProtocol.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@ public protocol ContractProtocol {
173173
/// - Returns: `true` if event is possibly present, `false` if definitely not present and `nil` if event with given name
174174
/// is not part of the ``EthereumContract/abi``.
175175
func testBloomForEventPresence(eventName: String, bloom: EthereumBloomFilter) -> Bool?
176+
177+
/// Given the transaction data searches for a match in ``ContractProtocol/methods``.
178+
/// - Parameter data: encoded function call used in transaction data field. Must be at least 4 bytes long.
179+
/// - Returns: function decoded from the ABI of this contract or `nil` if nothing was found.
180+
func getFunctionCalled(_ data: Data) -> ABI.Element.Function?
176181
}
177182

178183
// MARK: - Overloaded ContractProtocol's functions
@@ -333,4 +338,9 @@ extension DefaultContractProtocol {
333338
guard let function = methods[methodSignature]?.first else { return nil }
334339
return function.decodeInputData(Data(data[4 ..< data.count]))
335340
}
341+
342+
public func getFunctionCalled(_ data: Data) -> ABI.Element.Function? {
343+
guard data.count >= 4 else { return nil }
344+
return methods[data[0..<4].toHexString().addHexPrefix()]?.first
345+
}
336346
}

Sources/Core/EthereumABI/ABIElements.swift

Lines changed: 166 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public extension ABI {
7777
public let type: ParameterType
7878

7979
public init(name: String, type: ParameterType) {
80-
self.name = name
80+
self.name = name.trim()
8181
self.type = type
8282
}
8383
}
@@ -91,7 +91,7 @@ public extension ABI {
9191
public let payable: Bool
9292

9393
public init(name: String?, inputs: [InOut], outputs: [InOut], constant: Bool, payable: Bool) {
94-
self.name = name
94+
self.name = name?.trim()
9595
self.inputs = inputs
9696
self.outputs = outputs
9797
self.constant = constant
@@ -103,6 +103,7 @@ public extension ABI {
103103
public let inputs: [InOut]
104104
public let constant: Bool
105105
public let payable: Bool
106+
106107
public init(inputs: [InOut], constant: Bool, payable: Bool) {
107108
self.inputs = inputs
108109
self.constant = constant
@@ -126,7 +127,7 @@ public extension ABI {
126127
public let anonymous: Bool
127128

128129
public init(name: String, inputs: [Input], anonymous: Bool) {
129-
self.name = name
130+
self.name = name.trim()
130131
self.inputs = inputs
131132
self.anonymous = anonymous
132133
}
@@ -137,7 +138,7 @@ public extension ABI {
137138
public let indexed: Bool
138139

139140
public init(name: String, type: ParameterType, indexed: Bool) {
140-
self.name = name
141+
self.name = name.trim()
141142
self.type = type
142143
self.indexed = indexed
143144
}
@@ -155,16 +156,16 @@ public extension ABI {
155156
/// Custom structured error type available since solidity 0.8.4
156157
public struct EthError {
157158
public let name: String
158-
public let inputs: [Input]
159+
public let inputs: [InOut]
159160

160-
public struct Input {
161-
public let name: String
162-
public let type: ParameterType
161+
/// e.g. `CustomError(uint32, address sender)`
162+
public var errorDeclaration: String {
163+
"\(name)(\(inputs.map { "\($0.type.abiRepresentation) \($0.name)".trim() }.joined(separator: ",")))"
164+
}
163165

164-
public init(name: String, type: ParameterType) {
165-
self.name = name
166-
self.type = type
167-
}
166+
public init(name: String, inputs: [InOut] = []) {
167+
self.name = name.trim()
168+
self.inputs = inputs
168169
}
169170
}
170171
}
@@ -202,7 +203,7 @@ extension ABI.Element.Function {
202203

203204
/// Encode parameters of a given contract method
204205
/// - Parameter parameters: Parameters to pass to Ethereum contract
205-
/// - Returns: Encoded data
206+
/// - Returns: Encoded data
206207
public func encodeParameters(_ parameters: [AnyObject]) -> Data? {
207208
guard parameters.count == inputs.count,
208209
let data = ABIEncoder.encode(types: inputs, values: parameters) else { return nil }
@@ -264,73 +265,168 @@ extension ABI.Element.Function {
264265
return Core.decodeInputData(rawData, methodEncoding: methodEncoding, inputs: inputs)
265266
}
266267

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]()
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+
///
275+
/// Return cases:
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
320+
}
284321

285-
// set infomation
286-
returnArray["_abortedByRequire"] = true
287-
returnArray["_errorMessageFromRequire"] = message
322+
guard !outputs.isEmpty else {
323+
NSLog("Function doesn't have any output types to decode given data.")
324+
return ["_success": true]
325+
}
288326

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-
}
327+
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."]
329+
}
297330

298-
return returnArray
331+
// TODO: need improvement - we should be able to tell which value failed to be decoded
332+
guard let values = ABIDecoder.decode(types: outputs, data: data) else {
333+
return ["_success": false, "_failureReason": "Failed to decode at least one value."]
334+
}
335+
var returnArray: [String: Any] = ["_success": true]
336+
for i in outputs.indices {
337+
returnArray["\(i)"] = values[i]
338+
if !outputs[i].name.isEmpty {
339+
returnArray[outputs[i].name] = values[i]
299340
}
300341
}
342+
return returnArray
343+
}
301344

302-
var returnArray = [String: Any]()
303-
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]
322-
}
323-
i = i + 1
324-
}
325-
// set a flag to detect the request succeeded
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?"]
326389
}
327390

328-
if returnArray.isEmpty {
329-
return nil
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]
330409
}
331410

332-
returnArray["_success"] = true
333-
return returnArray
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
334430
}
335431
}
336432

Sources/Core/EthereumABI/ABIParsing.swift

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,9 @@ private func parseReceive(abiRecord: ABI.Record) throws -> ABI.Element.Receive {
129129
}
130130

131131
private func parseError(abiRecord: ABI.Record) throws -> ABI.Element.EthError {
132-
let inputs = try abiRecord.inputs?.map({ (input: ABI.Input) throws -> ABI.Element.EthError.Input in
133-
let nativeInput = try input.parseForError()
134-
return nativeInput
135-
})
136-
let abiInputs = inputs ?? []
132+
let abiInputs = try abiRecord.inputs?.map({ input throws -> ABI.Element.InOut in
133+
try input.parse()
134+
}) ?? []
137135
let name = abiRecord.name ?? ""
138136
return ABI.Element.EthError(name: name, inputs: abiInputs)
139137
}
@@ -172,12 +170,6 @@ extension ABI.Input {
172170
let indexed = self.indexed == true
173171
return ABI.Element.Event.Input(name: name, type: parameterType, indexed: indexed)
174172
}
175-
176-
func parseForError() throws -> ABI.Element.EthError.Input {
177-
let name = self.name ?? ""
178-
let parameterType = try ABITypeParser.parseTypeString(self.type)
179-
return ABI.Element.EthError.Input(name: name, type: parameterType)
180-
}
181173
}
182174

183175
extension ABI.Output {

Sources/Core/Oracle/GasOracle.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,11 @@ public extension Oracle {
226226

227227
extension Oracle {
228228
public struct FeeHistory {
229-
let timestamp = Date()
230-
let baseFeePerGas: [BigUInt]
231-
let gasUsedRatio: [Double]
232-
let oldestBlock: BigUInt
233-
let reward: [[BigUInt]]
229+
public let timestamp = Date()
230+
public let baseFeePerGas: [BigUInt]
231+
public let gasUsedRatio: [Double]
232+
public let oldestBlock: BigUInt
233+
public let reward: [[BigUInt]]
234234
}
235235
}
236236

Sources/Core/Utility/String+Extension.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ extension String {
130130
return Int(s[s.startIndex].value)
131131
}
132132
}
133+
134+
/// Strips whitespaces and newlines on both ends.
135+
func trim() -> String {
136+
trimmingCharacters(in: .whitespacesAndNewlines)
137+
}
133138
}
134139

135140
extension Character {

0 commit comments

Comments
 (0)