-
Notifications
You must be signed in to change notification settings - Fork 454
/
Copy pathContractProtocol.swift
executable file
·408 lines (362 loc) · 18.7 KB
/
ContractProtocol.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//
// ContractProtocol.swift
// web3swift
//
// Created by Alex Vlasov.
// Copyright © 2018 Alex Vlasov. All rights reserved.
//
import Foundation
import BigInt
/// Standard representation of a smart contract.
///
/// ## How to
/// To create a smart contract deployment transaction there is only one requirement - `bytecode`.
/// That is the compiled smart contract that is ready to be executed by EVM, or eWASM if that is a Serenity.
/// Creating a transaction is as simple as:
///
/// ```swift
/// contractInstance.deploy(bytecode: smartContractBytecode)
/// ```
///
/// One of the default implementations of `ContractProtocol` is ``EthereumContract``.
/// ```swift
/// let contract = EthereumContract(abi: [])
/// contract.deploy(bytecode: smartContractBytecode)
/// ```
///
/// ### Setting constructor arguments
/// Some smart contracts expect input arguments for a constructor that is called on contract deployment.
/// To set these input arguments you must provide `constructor` and `parameters`.
/// Constructor can be statically created if you know upfront what are the input arguments and their exact order:
///
/// ```swift
/// let inputArgsTypes: [ABI.Element.InOut] = [.init(name: "firstArgument", type: ABI.Element.ParameterType.string),
/// .init(name: "secondArgument", type: ABI.Element.ParameterType.uint(bits: 256))]
/// let constructor = ABI.Element.Constructor(inputs: inputArgsTypes, constant: false, payable: payable)
/// let constructorArguments: [Any] = ["This is the array of constructor arguments", 10_000]
///
/// contract.deploy(bytecode: smartContractBytecode,
/// constructor: constructor,
/// parameters: constructorArguments)
/// ```
///
/// Alternatively, if you have ABI string that holds meta data about the constructor you can use it instead of creating constructor manually.
/// But you must make sure the arguments for constructor call are of expected type in and correct order.
/// Example of ABI string can be found in ``Web3/Utils/erc20ABI``.
///
/// ```swift
/// let contract = EthereumContract(abiString)
/// let constructorArguments: [Any] = ["This is the array of constructor arguments", 10_000]
///
/// contract.deploy(bytecode: smartContractBytecode,
/// constructor: contract.constructor,
/// parameters: constructorArguments)
/// ```
///
/// ⚠️ If you pass in only constructor or only parameters - it will have no effect on the final transaction object.
/// Also, you have an option to set any extra bytes at the end of ``CodableTransaction/data`` attribute.
/// Alternatively you can encode constructor parameters outside of the deploy function and only set `extraData` to pass in these
/// parameters:
///
/// ```swift
/// // `encodeParameters` call returns `Data?`. Check it for nullability before calling `deploy`
/// // function to create `CodableTransaction`.
/// let encodedConstructorArguments = someConstructor.encodeParameters(arrayOfInputArguments)
/// constructor.deploy(bytecode: smartContractBytecode, extraData: encodedConstructorArguments)
/// ```
public protocol ContractProtocol {
/// Address of the referenced smart contract. Can be set later, e.g. if the contract is deploying and address is not yet known.
var address: EthereumAddress? {get set}
/// All ABI elements like: events, functions, constructors and errors.
var abi: [ABI.Element] {get}
/// Functions filtered from ``abi``.
/// Functions are mapped to:
/// - name, like `getData` that is defined in ``ABI/Element/Function/name``;
/// - name with input parameters that is a combination of ``ABI/Element/Function/name`` and
/// ``ABI/Element/Function/inputs``, e.g. `getData(bytes32)`;
/// - and 4 bytes signature `0xffffffff` (expected to be lowercased).
/// The mapping by name (e.g. `getData`) is the one most likely expected to return arrays with
/// more than one entry due to the fact that solidity allows method overloading.
var methods: [String: [ABI.Element.Function]] {get}
/// All values from ``methods``.
var allMethods: [ABI.Element.Function] {get}
/// Events filtered from ``abi`` and mapped to their unchanged ``ABI/Element/Event/name``.
var events: [String: ABI.Element.Event] {get}
/// All values from ``events``.
var allEvents: [ABI.Element.Event] {get}
/// Errors filtered from ``abi`` and mapped to their unchanged ``ABI/Element/EthError/name``.
var errors: [String: ABI.Element.EthError] {get}
/// All values from ``errors``.
var allErrors: [ABI.Element.EthError] {get}
/// Parsed from ABI or a default constructor with no input arguments.
var constructor: ABI.Element.Constructor {get}
/// Required initializer that is capable of reading ABI in JSON format.
/// - Parameters:
/// - abiString: ABI string in JSON format.
/// - at: contract added. Can be set later.
///
/// If ABI failed to be decoded `nil` will be returned. Reasons could be invalid keys and values in ABI, invalid JSON structure,
/// new Solidity keywords, types etc. that are not yet supported, etc.
init(_ abiString: String, at: EthereumAddress?) throws
/// Prepare transaction data for smart contract deployment transaction.
///
/// - Parameters:
/// - bytecode: bytecode to deploy.
/// - constructor: constructor of the smart contract bytecode is related to. Used to encode `parameters`.
/// - parameters: parameters for `constructor`.
/// - extraData: any extra data. It can be encoded input arguments for a constructor but then you should set `constructor` and
/// `parameters` to be `nil`.
/// - Returns: Encoded data for a given parameters, which is should be assigned to ``CodableTransaction.data`` property
func deploy(bytecode: Data,
constructor: ABI.Element.Constructor?,
parameters: [Any]?,
extraData: Data?) -> Data?
/// Creates function call transaction with data set as `method` encoded with given `parameters`.
/// The `method` must be part of the ABI used to init this contract.
/// - Parameters:
/// - method: method name in one of the following variants:
/// - name without arguments: `myFunction`. Use with caution! If smart contract has overloaded methods encoding might fail!
/// - name with arguments:`myFunction(uint256)`.
/// - method signature (with or without `0x` prefix, case insensitive): `0xFFffFFff`;
/// - parameters: method input arguments;
/// - extraData: additional data to append at the end of `transaction.data` field;
/// - Returns: transaction object if `method` was found and `parameters` were successfully encoded.
func method(_ method: String, parameters: [Any], extraData: Data?) -> Data?
/// Decode output data of a function.
/// - Parameters:
/// - method: method name in one of the following variants:
/// - name without arguments: `myFunction`. Use with caution! If smart contract has overloaded methods encoding might fail!
/// - name with arguments:`myFunction(uint256)`.
/// - method signature (with or without `0x` prefix, case insensitive): `0xFFffFFff`;
/// - data: non empty bytes to decode;
/// - Returns: dictionary with decoded values.
/// - Throws:
/// - `Web3Error.revert(String, String?)` when function call aborted by `revert(string)` and `require(expression, string)`.
/// - `Web3Error.revertCustom(String, Dictionary)` when function call aborted by `revert CustomError()`.
@discardableResult
func decodeReturnData(_ method: String, data: Data) throws -> [String: Any]
/// Decode input arguments of a function.
/// - Parameters:
/// - method: method name in one of the following variants:
/// - name without arguments: `myFunction`. Use with caution! If smart contract has overloaded methods encoding might fail!
/// - name with arguments:`myFunction(uint256)`.
/// - method signature (with or without `0x` prefix, case insensitive): `0xFFffFFff`;
/// - data: non empty bytes to decode;
/// - Returns: dictionary with decoded values. `nil` if decoding failed.
func decodeInputData(_ method: String, data: Data) -> [String: Any]?
/// Decode input data of a function.
/// - Parameters:
/// - data: encoded function call with first 4 bytes being function signature and the rest is input arguments, if any.
/// Empty dictionary will be return if function call doesn't accept any input arguments.
/// - Returns: dictionary with decoded input arguments. `nil` if decoding failed.
func decodeInputData(_ data: Data) -> [String: Any]?
/// Attempts to parse given event based on the data from `allEvents`, or in other words based on the given smart contract ABI.
func parseEvent(_ eventLog: EventLog) -> (eventName: String?, eventData: [String: Any]?)
/// Tests for probable presence of an event with `eventName` in a given bloom filter.
/// - Parameters:
/// - eventName: event name like `ValueReceived`.
/// - bloom: bloom filter.
/// - Returns: `true` if event is possibly present, `false` if definitely not present and `nil` if event with given name
/// is not part of the ``EthereumContract/abi``.
func testBloomForEventPresence(eventName: String, bloom: EthereumBloomFilter) -> Bool?
/// Given the transaction data searches for a match in ``ContractProtocol/methods``.
/// - Parameter data: encoded function call used in transaction data field. Must be at least 4 bytes long.
/// - Returns: function decoded from the ABI of this contract or `nil` if nothing was found.
func getFunctionCalled(_ data: Data) -> ABI.Element.Function?
}
// MARK: - Overloaded ContractProtocol's functions
extension ContractProtocol {
/// Overloading of ``ContractProtocol/deploy(bytecode:constructor:parameters:extraData:)`` to allow
/// omitting everything but `bytecode`.
///
/// See ``ContractProtocol/deploy(bytecode:constructor:parameters:extraData:)`` for details.
func deploy(_ bytecode: Data,
constructor: ABI.Element.Constructor? = nil,
parameters: [Any]? = nil,
extraData: Data? = nil) -> Data? {
deploy(bytecode: bytecode,
constructor: constructor,
parameters: parameters,
extraData: extraData)
}
/// Overloading of ``ContractProtocol/method(_:parameters:extraData:)`` to allow
/// omitting `extraData` and `parameters` if `method` does not expect any.
///
/// See ``ContractProtocol/method(_:parameters:extraData:)`` for details.
func method(_ method: String = "fallback",
parameters: [Any]? = nil,
extraData: Data? = nil) -> Data? {
self.method(method, parameters: parameters ?? [], extraData: extraData)
}
func decodeInputData(_ data: Data) -> [String: Any]? {
guard data.count >= 4 else { return nil }
let methodId = data[data.startIndex ..< data.startIndex + 4].toHexString()
let data = data[(data.startIndex + 4)...]
return decodeInputData(methodId, data: data)
}
}
/// Contains default implementations of all functions of ``ContractProtocol``.
public protocol DefaultContractProtocol: ContractProtocol {}
extension DefaultContractProtocol {
// MARK: Writing Data flow
public func deploy(bytecode: Data,
constructor: ABI.Element.Constructor?,
parameters: [Any]?,
extraData: Data?) -> Data? {
var fullData = bytecode
if let constructor = constructor,
let parameters = parameters,
!parameters.isEmpty {
guard constructor.inputs.count == parameters.count,
let encodedData = constructor.encodeParameters(parameters) else {
NSLog("Constructor encoding will fail as the number of input arguments doesn't match the number of given arguments.")
return nil
}
fullData.append(encodedData)
}
if let extraData = extraData {
fullData.append(extraData)
}
// MARK: Writing Data flow
return fullData
}
/// Call given contract method with given parameters
/// - Parameters:
/// - method: Method to call
/// - parameters: Parameters to pass to method call
/// - extraData: Any additional data that needs to be encoded
/// - Returns: preset CodableTransaction with filled date
///
/// Returned transaction have filled following priperties:
/// - to: contractAddress
/// - value: 0
/// - data: parameters + extraData
/// - params: EthereumParameters with no contract method call encoded data.
public func method(_ method: String,
parameters: [Any],
extraData: Data?) -> Data? {
// MARK: - Encoding ABI Data flow
if method == "fallback" {
return extraData ?? Data()
}
let method = Data.fromHex(method) == nil ? method : method.addHexPrefix().lowercased()
// MARK: - Encoding ABI Data flow
guard let abiMethod = methods[method]?.first(where: { $0.inputs.count == parameters.count }),
var encodedData = abiMethod.encodeParameters(parameters) else { return nil }
// Extra data just appends in the end of parameters data
if let extraData = extraData {
encodedData.append(extraData)
}
// MARK: - Encoding ABI Data flow
return encodedData
}
public func event(_ event: String, parameters: [Any]) -> [EventFilterParameters.Topic?] {
guard let event = events[event] else {
return []
}
return event.encodeParameters(parameters)
}
public func parseEvent(_ eventLog: EventLog) -> (eventName: String?, eventData: [String: Any]?) {
for (eName, ev) in self.events {
if !ev.anonymous {
if eventLog.topics[0] != ev.topic {
continue
} else {
let logTopics = eventLog.topics
let logData = eventLog.data
let parsed = ev.decodeReturnedLogs(eventLogTopics: logTopics, eventLogData: logData)
if parsed != nil {
return (eName, parsed!)
}
}
} else {
let logTopics = eventLog.topics
let logData = eventLog.data
let parsed = ev.decodeReturnedLogs(eventLogTopics: logTopics, eventLogData: logData)
if parsed != nil {
return (eName, parsed!)
}
}
}
return (nil, nil)
}
public func testBloomForEventPresence(eventName: String, bloom: EthereumBloomFilter) -> Bool? {
guard let event = events[eventName] else { return nil }
if event.anonymous {
return true
}
return bloom.test(topic: event.topic)
}
@discardableResult
public func decodeReturnData(_ method: String, data: Data) throws -> [String: Any] {
if method == "fallback" {
return [:]
}
guard let function = methods[method]?.first else {
throw Web3Error.inputError(desc: "Make sure ABI you use contains '\(method)' method.")
}
switch data.count % 32 {
case 0:
return try function.decodeReturnData(data)
case 4:
let selector = data[0..<4]
if selector.toHexString() == "08c379a0", let reason = ABI.Element.EthError.decodeStringError(data[4...]) {
throw Web3Error.revert("revert(string)` or `require(expression, string)` was executed. reason: \(reason)", reason: reason)
}
else if selector.toHexString() == "4e487b71", let reason = ABI.Element.EthError.decodePanicError(data[4...]) {
let panicCode = String(format: "%02X", Int(reason)).addHexPrefix()
throw Web3Error.revert("Error: call revert exception; VM Exception while processing transaction: reverted with panic code \(panicCode)", reason: panicCode)
}
else if let customError = errors[selector.toHexString().addHexPrefix().lowercased()] {
if let errorArgs = customError.decodeEthError(data[4...]) {
throw Web3Error.revertCustom(customError.signature, errorArgs)
} else {
throw Web3Error.inputError(desc: "Signature matches \(customError.errorDeclaration) but failed to be decoded.")
}
} else {
throw Web3Error.inputError(desc: "Make sure ABI you use contains error that can match signature: 0x\(selector.toHexString())")
}
default:
throw Web3Error.inputError(desc: "Given data has invalid bytes count.")
}
}
public func decodeInputData(_ method: String, data: Data) -> [String: Any]? {
if method == "fallback" {
return nil
}
return methods[method]?.compactMap({ function in
return function.decodeInputData(data)
}).first
}
public func decodeInputData(_ data: Data) -> [String: Any]? {
guard data.count % 32 == 4 else { return nil }
let methodSignature = data[data.startIndex ..< data.startIndex + 4].toHexString().addHexPrefix().lowercased()
guard let function = methods[methodSignature]?.first else { return nil }
return function.decodeInputData(Data(data[data.startIndex + 4 ..< data.startIndex + data.count]))
}
public func decodeEthError(_ data: Data) -> [String: Any]? {
guard data.count >= 4,
let err = errors.first(where: { $0.value.methodEncoding == data[0..<4] })?.value else {
return nil
}
return err.decodeEthError(data[4...])
}
public func getFunctionCalled(_ data: Data) -> ABI.Element.Function? {
guard data.count >= 4 else { return nil }
return methods[data[data.startIndex ..< data.startIndex + 4].toHexString().addHexPrefix()]?.first
}
}
extension DefaultContractProtocol {
@discardableResult
public func callStatic(_ method: String, parameters: [Any], provider: Web3Provider) async throws -> [String: Any] {
guard let address = address else {
throw Web3Error.inputError(desc: "RPC failed: contract is missing an address.")
}
guard let data = self.method(method, parameters: parameters, extraData: nil) else {
throw Web3Error.dataError
}
let transaction = CodableTransaction(to: address, data: data)
let result: Data = try await APIRequest.sendRequest(with: provider, for: .call(transaction, .latest)).result
return try decodeReturnData(method, data: result)
}
}