Skip to content

Commit 60a56e3

Browse files
Merge branch 'develop'
2 parents 74c24f4 + 6bdb9e6 commit 60a56e3

Some content is hidden

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

49 files changed

+2231
-431
lines changed

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
[![Platform](https://img.shields.io/cocoapods/p/web3swift?style=flat)](http://cocoapods.org/pods/web3swift)
1111
[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/web3swift?style=flat)](http://cocoapods.org/pods/web3swift)
1212
[![License](https://img.shields.io/cocoapods/l/web3swift.svg?style=flat)](https://github.com/web3swift-team/web3swift/blob/master/LICENSE.md)
13-
[![support](https://brianmacdonald.github.io/Ethonate/svg/eth-support-blue.svg)](https://brianmacdonald.github.io/Ethonate/address#0xe22b8979739d724343bd002f9f432f5990879901)
1413
[![Stackoverflow](https://img.shields.io/badge/stackoverflow-ask-blue.svg)](https://stackoverflow.com/questions/tagged/web3swift)
1514

1615
---

Sources/Web3Core/Contract/ContractProtocol.swift

+69-7
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:
@@ -280,6 +284,13 @@ extension DefaultContractProtocol {
280284
return encodedData
281285
}
282286

287+
public func event(_ event: String, parameters: [Any]) -> [EventFilterParameters.Topic?] {
288+
guard let event = events[event] else {
289+
return []
290+
}
291+
return event.encodeParameters(parameters)
292+
}
293+
283294
public func parseEvent(_ eventLog: EventLog) -> (eventName: String?, eventData: [String: Any]?) {
284295
for (eName, ev) in self.events {
285296
if !ev.anonymous {
@@ -313,13 +324,40 @@ extension DefaultContractProtocol {
313324
return bloom.test(topic: event.topic)
314325
}
315326

316-
public func decodeReturnData(_ method: String, data: Data) -> [String: Any]? {
327+
@discardableResult
328+
public func decodeReturnData(_ method: String, data: Data) throws -> [String: Any] {
317329
if method == "fallback" {
318-
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.")
319360
}
320-
return methods[method]?.compactMap({ function in
321-
return function.decodeReturnData(data)
322-
}).first
323361
}
324362

325363
public func decodeInputData(_ method: String, data: Data) -> [String: Any]? {
@@ -339,8 +377,32 @@ extension DefaultContractProtocol {
339377
return function.decodeInputData(Data(data[data.startIndex + 4 ..< data.startIndex + data.count]))
340378
}
341379

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+
342388
public func getFunctionCalled(_ data: Data) -> ABI.Element.Function? {
343389
guard data.count >= 4 else { return nil }
344390
return methods[data[data.startIndex ..< data.startIndex + 4].toHexString().addHexPrefix()]?.first
345391
}
346392
}
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

+125-50
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,
@@ -211,13 +211,123 @@ extension ABI.Element.Function {
211211
}
212212
}
213213

214-
// MARK: - Event logs decoding
214+
// MARK: - Event logs decoding & encoding
215215

216216
extension ABI.Element.Event {
217217
public func decodeReturnedLogs(eventLogTopics: [Data], eventLogData: Data) -> [String: Any]? {
218218
guard let eventContent = ABIDecoder.decodeLog(event: self, eventLogTopics: eventLogTopics, eventLogData: eventLogData) else { return nil }
219219
return eventContent
220220
}
221+
222+
public static func encodeTopic(input: ABI.Element.Event.Input, value: Any) -> EventFilterParameters.Topic? {
223+
switch input.type {
224+
case .string:
225+
guard let string = value as? String else {
226+
return nil
227+
}
228+
return .string(string.sha3(.keccak256).addHexPrefix())
229+
case .dynamicBytes:
230+
guard let data = ABIEncoder.convertToData(value) else {
231+
return nil
232+
}
233+
return .string(data.sha3(.keccak256).toHexString().addHexPrefix())
234+
case .bytes(length: _):
235+
guard let data = ABIEncoder.convertToData(value), let data = data.setLengthLeft(32) else {
236+
return nil
237+
}
238+
return .string(data.toHexString().addHexPrefix())
239+
case .address, .uint(bits: _), .int(bits: _), .bool:
240+
guard let encoded = ABIEncoder.encodeSingleType(type: input.type, value: value) else {
241+
return nil
242+
}
243+
return .string(encoded.toHexString().addHexPrefix())
244+
default:
245+
guard let data = try? ABIEncoder.abiEncode(value).setLengthLeft(32) else {
246+
return nil
247+
}
248+
return .string(data.toHexString().addHexPrefix())
249+
}
250+
}
251+
252+
public func encodeParameters(_ parameters: [Any?]) -> [EventFilterParameters.Topic?] {
253+
guard parameters.count <= inputs.count else {
254+
// too many arguments for fragment
255+
return []
256+
}
257+
var topics: [EventFilterParameters.Topic?] = []
258+
259+
if !anonymous {
260+
topics.append(.string(topic.toHexString().addHexPrefix()))
261+
}
262+
263+
for (i, p) in parameters.enumerated() {
264+
let input = inputs[i]
265+
if !input.indexed {
266+
// cannot filter non-indexed parameters; must be null
267+
return []
268+
}
269+
if p == nil {
270+
topics.append(nil)
271+
} else if input.type.isArray || input.type.isTuple {
272+
// filtering with tuples or arrays not supported
273+
return []
274+
} else if let p = p as? Array<Any> {
275+
topics.append(.strings(p.map { Self.encodeTopic(input: input, value: $0) }))
276+
} else {
277+
topics.append(Self.encodeTopic(input: input, value: p!))
278+
}
279+
}
280+
281+
// Trim off trailing nulls
282+
while let last = topics.last {
283+
if last == nil {
284+
topics.removeLast()
285+
} else if case .string(let string) = last, string == nil {
286+
topics.removeLast()
287+
} else {
288+
break
289+
}
290+
}
291+
return topics
292+
}
293+
}
294+
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+
}
221331
}
222332

223333
// MARK: - Function input/output decoding
@@ -232,7 +342,7 @@ extension ABI.Element {
232342
case .fallback:
233343
return nil
234344
case .function(let function):
235-
return function.decodeReturnData(data)
345+
return try? function.decodeReturnData(data)
236346
case .receive:
237347
return nil
238348
case .error:
@@ -265,74 +375,38 @@ extension ABI.Element.Function {
265375
return ABIDecoder.decodeInputData(rawData, methodEncoding: methodEncoding, inputs: inputs)
266376
}
267377

268-
/// 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.
269379
/// - Parameters:
270380
/// - 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.
272381
/// - 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.
382+
/// - Throws:
383+
/// - `Web3Error.processingError(desc: String)` when decode process failed.
274384
///
275385
/// Return cases:
276-
/// - when no `outputs` declared and `data` is not an error response:
386+
/// - when no `outputs` declared:
277387
/// ```swift
278-
/// ["_success": true]
388+
/// [:]
279389
/// ```
280390
/// - when `outputs` declared and decoding completed successfully:
281391
/// ```swift
282-
/// ["_success": true, "0": value_1, "1": value_2, ...]
392+
/// ["0": value_1, "1": value_2, ...]
283393
/// ```
284394
/// 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-
}
321-
395+
public func decodeReturnData(_ data: Data) throws -> [String: Any] {
322396
guard !outputs.isEmpty else {
323397
NSLog("Function doesn't have any output types to decode given data.")
324-
return ["_success": true]
398+
return [:]
325399
}
326400

327401
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."]
402+
throw Web3Error.processingError(desc: "Bytes count must be at least \(outputs.count * 32). Given \(data.count). Decoding will fail.")
329403
}
330404

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

0 commit comments

Comments
 (0)