diff --git a/Documentation/Usage.md b/Documentation/Usage.md index fbafaa3e2..9b513d676 100755 --- a/Documentation/Usage.md +++ b/Documentation/Usage.md @@ -37,10 +37,12 @@ - *[Infura Websocket Provider](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#infura-websocket-provider)* - [Connect to Infura endpoint](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#connect-to-infura-endpoint) - [Connect to custom endpoint with API similar to Infura WSS endpoint](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#connect-to-custom-endpoint-with-api-similar-to-infura-wss-endpoint) - - [Create a filter in the node to notify when something happened](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#create-a-filter-in-the-node-to-notify-when-something-happened) + - [Set a filter in the node to notify when something happened](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#set-a-filter-in-the-node-to-notify-when-something-happened) - [Get new pending transactions](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#get-new-pending-transactions) - [Create a new subscription over particular events](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#create-a-new-subscription-over-particular-events) - [Subscribe on new pending transactions](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#subscribe-on-new-pending-transactions) + - [Subscribe on logs](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#subscribe-on-logs) + - [Subscribe on new heads](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#subscribe-on-new-heads) - **[ENS](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#ens)** - [Registry](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#registry) - [Resolver](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#resolver) @@ -410,18 +412,28 @@ socketProvider = InfuraWebsocketProvider.connectToInfuraSocket(.Mainnet, delegat socketProvider = InfuraWebsocketProvider.connectToSocket("ws://your.endpoint", delegate: delegate) ``` -#### Create a filter in the node to notify when something happened +#### Set a filter in the node to notify when something happened To study possible filters read [Infura WSS filters documentation](https://infura.io/docs/ethereum/wss/introduction) ```swift -try! socketProvider.filter(method: , params: <[Encodable]?>) +// Getting logs +try! socketProvider.setFilterAndGetLogs(method: , params: <[Encodable]?>) +// Getting changes +try! socketProvider.setFilterAndGetChanges(method: , params: <[Encodable]?>) +``` +Or you can provide parameters in more convenient way: +```swift +// Getting logs +try! socketProvider.setFilterAndGetLogs(method: , address: , fromBlock: , toBlock: , topics: <[String]?>) +// Getting changes +try! socketProvider.setFilterAndGetChanges(method: , address: , fromBlock: , toBlock: , topics: <[String]?>) ``` #### Get new pending transactions ```swift -try! socketProvider.filter(method: .newPendingTransactionFilter) +try! socketProvider.setFilterAndGetLogs(method: .newPendingTransactionFilter) ``` #### Create a new subscription over particular events @@ -438,6 +450,18 @@ try! socketProvider.subscribe(params: <[Encodable]>) try! socketProvider.subscribeOnNewPendingTransactions() ``` +#### Subscribe on logs + +```swift +try! socketProvider.subscribeOnLogs(addresses: <[EthereumAddress]?>, topics: <[String]?>) +``` + +#### Subscribe on new heads + +```swift +try! socketProvider.subscribeOnNewHeads() +``` + ## ENS You need ENS instance for future actions: diff --git a/README.md b/README.md index c1dc86d3d..59945c6c4 100755 --- a/README.md +++ b/README.md @@ -68,10 +68,12 @@ - *[Infura Websocket Provider](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#infura-websocket-provider)* - [Connect to Infura endpoint](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#connect-to-infura-endpoint) - [Connect to custom endpoint with API similar to Infura WSS endpoint](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#connect-to-custom-endpoint-with-api-similar-to-infura-wss-endpoint) - - [Create a filter in the node to notify when something happened](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#create-a-filter-in-the-node-to-notify-when-something-happened) + - [Set a filter in the node to notify when something happened](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#set-a-filter-in-the-node-to-notify-when-something-happened) - [Get new pending transactions](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#get-new-pending-transactions) - [Create a new subscription over particular events](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#create-a-new-subscription-over-particular-events) - [Subscribe on new pending transactions](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#subscribe-on-new-pending-transactions) + - [Subscribe on logs](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#subscribe-on-logs) + - [Subscribe on new heads](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#subscribe-on-new-heads) - **[ENS](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#ens)** - [Registry](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#registry) - [Resolver](https://github.com/matter-labs/web3swift/blob/master/Documentation/Usage.md#resolver) diff --git a/Sources/web3swift/Convenience/Decodable+Extensions.swift b/Sources/web3swift/Convenience/Decodable+Extensions.swift new file mode 100644 index 000000000..59f82fb0a --- /dev/null +++ b/Sources/web3swift/Convenience/Decodable+Extensions.swift @@ -0,0 +1,146 @@ +// +// DecodingContainer+AnyCollection.swift +// AnyDecodable +// +// Created by levantAJ on 1/18/19. +// Copyright © 2019 levantAJ. All rights reserved. +// +import Foundation + +struct AnyCodingKey: CodingKey { + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + self.intValue = intValue + self.stringValue = String(intValue) + } +} + +extension KeyedDecodingContainer { + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value + /// is not convertible to the requested type. + /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry + /// for the given key. + /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for + /// the given key. + public func decode(_ type: [Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [Any] { + var values = try nestedUnkeyedContainer(forKey: key) + return try values.decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value + /// is not convertible to the requested type. + /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry + /// for the given key. + /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for + /// the given key. + public func decode(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [String: Any] { + let values = try nestedContainer(keyedBy: AnyCodingKey.self, forKey: key) + return try values.decode(type) + } + + /// Decodes a value of the given type for the given key, if present. + /// + /// This method returns `nil` if the container does not have a value + /// associated with `key`, or if the value is null. The difference between + /// these states can be distinguished with a `contains(_:)` call. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A decoded value of the requested type, or `nil` if the + /// `Decoder` does not have an entry associated with the given key, or if + /// the value is a null value. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value + /// is not convertible to the requested type. + public func decodeIfPresent(_ type: [Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [Any]? { + guard contains(key), + try decodeNil(forKey: key) == false else { return nil } + return try decode(type, forKey: key) + } + + /// Decodes a value of the given type for the given key, if present. + /// + /// This method returns `nil` if the container does not have a value + /// associated with `key`, or if the value is null. The difference between + /// these states can be distinguished with a `contains(_:)` call. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A decoded value of the requested type, or `nil` if the + /// `Decoder` does not have an entry associated with the given key, or if + /// the value is a null value. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value + /// is not convertible to the requested type. + public func decodeIfPresent(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [String: Any]? { + guard contains(key), + try decodeNil(forKey: key) == false else { return nil } + return try decode(type, forKey: key) + } +} + +private extension KeyedDecodingContainer { + func decode(_ type: [String: Any].Type) throws -> [String: Any] { + var dictionary: [String: Any] = [:] + for key in allKeys { + if try decodeNil(forKey: key) { + dictionary[key.stringValue] = NSNull() + } else if let bool = try? decode(Bool.self, forKey: key) { + dictionary[key.stringValue] = bool + } else if let string = try? decode(String.self, forKey: key) { + dictionary[key.stringValue] = string + } else if let int = try? decode(Int.self, forKey: key) { + dictionary[key.stringValue] = int + } else if let double = try? decode(Double.self, forKey: key) { + dictionary[key.stringValue] = double + } else if let dict = try? decode([String: Any].self, forKey: key) { + dictionary[key.stringValue] = dict + } else if let array = try? decode([Any].self, forKey: key) { + dictionary[key.stringValue] = array + } + } + return dictionary + } +} + +private extension UnkeyedDecodingContainer { + mutating func decode(_ type: [Any].Type) throws -> [Any] { + var elements: [Any] = [] + while !isAtEnd { + if try decodeNil() { + elements.append(NSNull()) + } else if let int = try? decode(Int.self) { + elements.append(int) + } else if let bool = try? decode(Bool.self) { + elements.append(bool) + } else if let double = try? decode(Double.self) { + elements.append(double) + } else if let string = try? decode(String.self) { + elements.append(string) + } else if let values = try? nestedContainer(keyedBy: AnyCodingKey.self), + let element = try? values.decode([String: Any].self) { + elements.append(element) + } else if var values = try? nestedUnkeyedContainer(), + let element = try? values.decode([Any].self) { + elements.append(element) + } + } + return elements + } +} diff --git a/Sources/web3swift/Convenience/Encodable+Extensions.swift b/Sources/web3swift/Convenience/Encodable+Extensions.swift new file mode 100644 index 000000000..84bad03eb --- /dev/null +++ b/Sources/web3swift/Convenience/Encodable+Extensions.swift @@ -0,0 +1,130 @@ +// +// EncodingContainer+AnyCollection.swift +// AnyDecodable +// +// Created by ShopBack on 1/19/19. +// Copyright © 2019 levantAJ. All rights reserved. +// +import Foundation + +extension KeyedEncodingContainer { + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + public mutating func encode(_ value: [String: Any], forKey key: KeyedEncodingContainer.Key) throws { + var container = nestedContainer(keyedBy: AnyCodingKey.self, forKey: key) + try container.encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + public mutating func encode(_ value: [Any], forKey key: KeyedEncodingContainer.Key) throws { + var container = nestedUnkeyedContainer(forKey: key) + try container.encode(value) + } + + /// Encodes the given value for the given key if it is not `nil`. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + public mutating func encodeIfPresent(_ value: [String: Any]?, forKey key: KeyedEncodingContainer.Key) throws { + if let value = value { + var container = nestedContainer(keyedBy: AnyCodingKey.self, forKey: key) + try container.encode(value) + } else { + try encodeNil(forKey: key) + } + } + + /// Encodes the given value for the given key if it is not `nil`. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + public mutating func encodeIfPresent(_ value: [Any]?, forKey key: KeyedEncodingContainer.Key) throws { + if let value = value { + var container = nestedUnkeyedContainer(forKey: key) + try container.encode(value) + } else { + try encodeNil(forKey: key) + } + } +} + +private extension KeyedEncodingContainer where K == AnyCodingKey { + mutating func encode(_ value: [String: Any]) throws { + for (k, v) in value { + let key = AnyCodingKey(stringValue: k)! + switch v { + case is NSNull: + try encodeNil(forKey: key) + case let string as String: + try encode(string, forKey: key) + case let int as Int: + try encode(int, forKey: key) + case let bool as Bool: + try encode(bool, forKey: key) + case let double as Double: + try encode(double, forKey: key) + case let dict as [String: Any]: + try encode(dict, forKey: key) + case let array as [Any]: + try encode(array, forKey: key) + default: + debugPrint("⚠️ Unsuported type!", v) + continue + } + } + } +} + +private extension UnkeyedEncodingContainer { + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + mutating func encode(_ value: [Any]) throws { + for v in value { + switch v { + case is NSNull: + try encodeNil() + case let string as String: + try encode(string) + case let int as Int: + try encode(int) + case let bool as Bool: + try encode(bool) + case let double as Double: + try encode(double) + case let dict as [String: Any]: + try encode(dict) + case let array as [Any]: + var values = nestedUnkeyedContainer() + try values.encode(array) + default: + debugPrint("⚠️ Unsuported type!", v) + } + } + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + mutating func encode(_ value: [String: Any]) throws { + var container = self.nestedContainer(keyedBy: AnyCodingKey.self) + try container.encode(value) + } +} diff --git a/Sources/web3swift/Web3/Web3+Eth+Websocket.swift b/Sources/web3swift/Web3/Web3+Eth+Websocket.swift index 73901a777..0c1976de3 100644 --- a/Sources/web3swift/Web3/Web3+Eth+Websocket.swift +++ b/Sources/web3swift/Web3/Web3+Eth+Websocket.swift @@ -31,7 +31,7 @@ extension web3.Eth { public func getLatestPendingTransactions(forDelegate delegate: Web3SocketDelegate) throws { let provider = try getWebsocketProvider(forDelegate: delegate) - try provider.filter(method: .newPendingTransactionFilter) + try provider.setFilterAndGetChanges(method: .newPendingTransactionFilter) } public func subscribeOnPendingTransactions(forDelegate delegate: Web3SocketDelegate) throws { diff --git a/Sources/web3swift/Web3/Web3+InfuraProviders.swift b/Sources/web3swift/Web3/Web3+InfuraProviders.swift index 7bdbd6b59..e97a81328 100755 --- a/Sources/web3swift/Web3/Web3+InfuraProviders.swift +++ b/Sources/web3swift/Web3/Web3+InfuraProviders.swift @@ -7,6 +7,26 @@ import Foundation import BigInt import Starscream +public enum BlockNumber { + case pending + case latest + case earliest + case exact(BigUInt) + + public var stringValue: String { + switch self { + case .pending: + return "pending" + case .latest: + return "latest" + case .earliest: + return "earliest" + case .exact(let number): + return String(number, radix: 16).addHexPrefix() + } + } +} + /// Custom Web3 HTTP provider of Infura nodes. public final class InfuraProvider: Web3HttpProvider { public init?(_ net:Networks, accessToken token: String? = nil, keystoreManager manager: KeystoreManager? = nil) { @@ -28,43 +48,51 @@ public final class InfuraWebsocketProvider: WebsocketProvider { public init?(_ network: Networks, delegate: Web3SocketDelegate, + projectId: String? = nil, keystoreManager manager: KeystoreManager? = nil) { guard network == Networks.Kovan || network == Networks.Rinkeby || network == Networks.Ropsten || network == Networks.Mainnet else {return nil} let networkName = network.name - let urlString = "wss://\(networkName).infura.io/ws" + let urlString = "wss://\(networkName).infura.io/ws/v3/" guard URL(string: urlString) != nil else {return nil} super.init(urlString, delegate: delegate, + projectId: projectId, keystoreManager: manager, network: network) } public init?(_ endpoint: String, delegate: Web3SocketDelegate, + projectId: String? = nil, keystoreManager manager: KeystoreManager? = nil) { guard URL(string: endpoint) != nil else {return nil} super.init(endpoint, delegate: delegate, + projectId: projectId, keystoreManager: manager) } public init?(_ endpoint: URL, delegate: Web3SocketDelegate, + projectId: String? = nil, keystoreManager manager: KeystoreManager? = nil) { super.init(endpoint, delegate: delegate, + projectId: projectId, keystoreManager: manager) } override public class func connectToSocket(_ endpoint: String, delegate: Web3SocketDelegate, + projectId: String? = nil, keystoreManager manager: KeystoreManager? = nil, network net: Networks? = nil) -> WebsocketProvider? { guard let socketProvider = InfuraWebsocketProvider(endpoint, delegate: delegate, + projectId: projectId, keystoreManager: manager) else {return nil} socketProvider.connectSocket() return socketProvider @@ -72,10 +100,12 @@ public final class InfuraWebsocketProvider: WebsocketProvider { override public class func connectToSocket(_ endpoint: URL, delegate: Web3SocketDelegate, + projectId: String? = nil, keystoreManager manager: KeystoreManager? = nil, network net: Networks? = nil) -> WebsocketProvider? { guard let socketProvider = InfuraWebsocketProvider(endpoint, delegate: delegate, + projectId: projectId, keystoreManager: manager) else {return nil} socketProvider.connectSocket() return socketProvider @@ -83,9 +113,11 @@ public final class InfuraWebsocketProvider: WebsocketProvider { public static func connectToInfuraSocket(_ network: Networks, delegate: Web3SocketDelegate, + projectId: String? = nil, keystoreManager manager: KeystoreManager? = nil) -> InfuraWebsocketProvider? { guard let socketProvider = InfuraWebsocketProvider(network, delegate: delegate, + projectId: projectId, keystoreManager: manager) else {return nil} socketProvider.connectSocket() return socketProvider @@ -95,10 +127,11 @@ public final class InfuraWebsocketProvider: WebsocketProvider { let request = JSONRPCRequestFabric.prepareRequest(method, parameters: params) let encoder = JSONEncoder() let requestData = try encoder.encode(request) + print(String(decoding: requestData, as: UTF8.self)) writeMessage(requestData) } - public func filter(method: InfuraWebsocketMethod, params: [Encodable]? = nil) throws { + public func setFilterAndGetChanges(method: InfuraWebsocketMethod, params: [Encodable]? = nil) throws { filterTimer?.invalidate() filterID = nil let params = params ?? [] @@ -110,25 +143,59 @@ public final class InfuraWebsocketProvider: WebsocketProvider { filterTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(getFilterChanges), userInfo: nil, repeats: true) } + public func setFilterAndGetChanges(method: InfuraWebsocketMethod, address: EthereumAddress? = nil, fromBlock: BlockNumber? = nil, toBlock: BlockNumber? = nil, topics: [String]? = nil) throws { + let filterParams = EventFilterParameters(fromBlock: fromBlock?.stringValue, toBlock: toBlock?.stringValue, topics: [topics], address: [address?.address]) + try setFilterAndGetChanges(method: method, params: [filterParams]) + } + + public func setFilterAndGetLogs(method: InfuraWebsocketMethod, params: [Encodable]? = nil) throws { + filterTimer?.invalidate() + filterID = nil + let params = params ?? [] + let paramsCount = params.count + guard method.requiredNumOfParameters == paramsCount || method.requiredNumOfParameters == nil else { + throw Web3Error.inputError(desc: "Wrong number of params: need - \(method.requiredNumOfParameters!), got - \(paramsCount)") + } + try writeMessage(method: method, params: params) + filterTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(getFilterLogs), userInfo: nil, repeats: true) + } + + public func setFilterAndGetLogs(method: InfuraWebsocketMethod, address: EthereumAddress? = nil, fromBlock: BlockNumber? = nil, toBlock: BlockNumber? = nil, topics: [String]? = nil) throws { + let filterParams = EventFilterParameters(fromBlock: fromBlock?.stringValue, toBlock: toBlock?.stringValue, topics: [topics], address: [address?.address]) + try setFilterAndGetLogs(method: method, params: [filterParams]) + } + @objc public func getFilterChanges() throws { - if let id = self.filterID { + if let id = filterID { filterTimer?.invalidate() let method = InfuraWebsocketMethod.getFilterChanges try writeMessage(method: method, params: [id]) } } - public func getFilterLogs() throws { - if let id = self.filterID { + @objc public func getFilterLogs() throws { + if let id = filterID { + filterTimer?.invalidate() let method = InfuraWebsocketMethod.getFilterLogs try writeMessage(method: method, params: [id]) } } + public func getFilterLogs(address: EthereumAddress? = nil, fromBlock: BlockNumber? = nil, toBlock: BlockNumber? = nil, topics: [String]? = nil) throws { + if let id = filterID { + let filterParams = EventFilterParameters(fromBlock: fromBlock?.stringValue, toBlock: toBlock?.stringValue, topics: [topics], address: [address?.address]) + let method = InfuraWebsocketMethod.getFilterLogs + try writeMessage(method: method, params: [id, filterParams]) + } + } + public func unistallFilter() throws { - if let id = self.filterID { + if let id = filterID { + filterID = nil let method = InfuraWebsocketMethod.uninstallFilter try writeMessage(method: method, params: [id]) + } else { + throw Web3Error.nodeError(desc: "No filter set") } } @@ -149,6 +216,19 @@ public final class InfuraWebsocketProvider: WebsocketProvider { try writeMessage(method: method, params: params) } + public func subscribeOnLogs(addresses: [EthereumAddress]? = nil, topics: [String]? = nil) throws { + let method = InfuraWebsocketMethod.subscribe + var stringAddresses = [String]() + if let addrs = addresses { + for addr in addrs { + stringAddresses.append(addr.address) + } + } +// let ts = topics == nil ? nil : [topics!] + let filterParams = EventFilterParameters(fromBlock: nil, toBlock: nil, topics: [topics], address: stringAddresses) + try writeMessage(method: method, params: ["logs", filterParams]) + } + public func subscribeOnNewPendingTransactions() throws { let method = InfuraWebsocketMethod.subscribe let params = ["newPendingTransactions"] diff --git a/Sources/web3swift/Web3/Web3+WebsocketProvider.swift b/Sources/web3swift/Web3/Web3+WebsocketProvider.swift index 6904a1115..d4f8884e0 100644 --- a/Sources/web3swift/Web3/Web3+WebsocketProvider.swift +++ b/Sources/web3swift/Web3/Web3+WebsocketProvider.swift @@ -15,8 +15,7 @@ public protocol IWebsocketProvider { var delegate: Web3SocketDelegate {get set} func connectSocket() throws func disconnectSocket() throws - func writeMessage(_ string: String) - func writeMessage(_ data: Data) + func writeMessage(_ message: T) } public enum InfuraWebsocketMethod: String, Encodable { @@ -113,16 +112,18 @@ public class WebsocketProvider: Web3Provider, IWebsocketProvider, WebSocketDeleg public var socket: WebSocket public var delegate: Web3SocketDelegate + private var websocketConnected: Bool = false + private var writeTimer: Timer? = nil + private var messagesStringToWrite: [String] = [] + private var messagesDataToWrite: [Data] = [] + public init?(_ endpoint: URL, delegate wsdelegate: Web3SocketDelegate, + projectId: String? = nil, keystoreManager manager: KeystoreManager? = nil, network net: Networks? = nil) { - delegate = wsdelegate - attachedKeystoreManager = manager - url = endpoint - socket = WebSocket(url: endpoint) - socket.delegate = self - let endpointString = endpoint.absoluteString + websocketConnected = false + var endpointString = endpoint.absoluteString if !(endpointString.hasPrefix("wss://") || endpointString.hasPrefix("ws://")) { return nil } @@ -146,25 +147,36 @@ public class WebsocketProvider: Web3Provider, IWebsocketProvider, WebSocketDeleg } else { network = net } + if network != nil { + endpointString += projectId ?? "4406c3acf862426c83991f1752c46dd8" + } + url = URL(string: endpointString)! + delegate = wsdelegate + attachedKeystoreManager = manager + socket = WebSocket(url: endpoint) + socket.delegate = self } public init?(_ endpoint: String, delegate wsdelegate: Web3SocketDelegate, + projectId: String? = nil, keystoreManager manager: KeystoreManager? = nil, network net: Networks? = nil) { - guard let endpointUrl = URL(string: endpoint) else {return nil} - delegate = wsdelegate - attachedKeystoreManager = manager - url = endpointUrl - socket = WebSocket(url: endpointUrl) - socket.delegate = self - let endpointString = endpointUrl.absoluteString + websocketConnected = false + var endpointString = endpoint if !(endpointString.hasPrefix("wss://") || endpointString.hasPrefix("ws://")) { return nil } if net == nil { - if endpointString.hasPrefix("wss://") && endpointString.hasSuffix(".infura.io/ws") { + if endpointString.hasPrefix("wss://") + && (endpointString.hasSuffix(".infura.io/ws/v3/") + || endpointString.hasSuffix(".infura.io/ws/v3") + || endpointString.hasSuffix(".infura.io/ws/") + || endpointString.hasSuffix(".infura.io/ws")) { let networkString = endpointString.replacingOccurrences(of: "wss://", with: "") + .replacingOccurrences(of: ".infura.io/ws/v3/", with: "") + .replacingOccurrences(of: ".infura.io/ws/v3", with: "") + .replacingOccurrences(of: ".infura.io/ws/", with: "") .replacingOccurrences(of: ".infura.io/ws", with: "") switch networkString { case "mainnet": @@ -182,22 +194,42 @@ public class WebsocketProvider: Web3Provider, IWebsocketProvider, WebSocketDeleg } else { network = net } + if network != nil { + if endpointString.hasSuffix(".infura.io/ws/v3") { endpointString += "/"} + else if endpointString.hasSuffix(".infura.io/ws/") { endpointString += "v3/"} + else if endpointString.hasSuffix(".infura.io/ws") { endpointString += "/v3/"} + endpointString += projectId ?? "4406c3acf862426c83991f1752c46dd8" + } + guard let endpointUrl = URL(string: endpointString) else {return nil} + url = endpointUrl + delegate = wsdelegate + attachedKeystoreManager = manager + socket = WebSocket(url: endpointUrl) + socket.delegate = self + } + + deinit { + writeTimer?.invalidate() } public func connectSocket() { + writeTimer?.invalidate() socket.connect() } public func disconnectSocket() { + writeTimer?.invalidate() socket.disconnect() } public class func connectToSocket(_ endpoint: String, delegate: Web3SocketDelegate, + projectId: String? = nil, keystoreManager manager: KeystoreManager? = nil, network net: Networks? = nil) -> WebsocketProvider? { guard let socketProvider = WebsocketProvider(endpoint, delegate: delegate, + projectId: projectId, keystoreManager: manager, network: net) else { return nil @@ -208,10 +240,12 @@ public class WebsocketProvider: Web3Provider, IWebsocketProvider, WebSocketDeleg public class func connectToSocket(_ endpoint: URL, delegate: Web3SocketDelegate, + projectId: String? = nil, keystoreManager manager: KeystoreManager? = nil, network net: Networks? = nil) -> WebsocketProvider? { guard let socketProvider = WebsocketProvider(endpoint, delegate: delegate, + projectId: projectId, keystoreManager: manager, network: net) else { return nil @@ -220,12 +254,34 @@ public class WebsocketProvider: Web3Provider, IWebsocketProvider, WebSocketDeleg return socketProvider } - public func writeMessage(_ string: String) { - socket.write(string: string) + public func writeMessage(_ message: T) { + var sMessage: String? = nil + var dMessage: Data? = nil + if !(message.self is String) && !(message.self is Data) { + sMessage = "\(message)" + } else if message.self is String { + sMessage = message as? String + } else if message.self is Data { + dMessage = message as? Data + } + if sMessage != nil { + self.messagesStringToWrite.append(sMessage!) + } else if dMessage != nil { + self.messagesDataToWrite.append(dMessage!) + } + writeTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(performWriteOperations), userInfo: nil, repeats: true) } - public func writeMessage(_ data: Data) { - socket.write(data: data) + @objc private func performWriteOperations() { + if websocketConnected { + writeTimer?.invalidate() + for s in messagesStringToWrite { + socket.write(string: s) + } + for d in messagesDataToWrite { + socket.write(data: d) + } + } } public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { @@ -240,10 +296,12 @@ public class WebsocketProvider: Web3Provider, IWebsocketProvider, WebSocketDeleg public func websocketDidConnect(socket: WebSocketClient) { print("websocket is connected") + websocketConnected = true } public func websocketDidDisconnect(socket: WebSocketClient, error: Error?) { print("websocket is disconnected with \(error?.localizedDescription ?? "no error")") + websocketConnected = false } public func websocketDidReceivePong(socket: WebSocketClient, data: Data?) { diff --git a/Tests/web3swiftTests/web3swift_Websockets_Tests.swift b/Tests/web3swiftTests/web3swift_Websockets_Tests.swift index 54f04e61d..a59523a40 100644 --- a/Tests/web3swiftTests/web3swift_Websockets_Tests.swift +++ b/Tests/web3swiftTests/web3swift_Websockets_Tests.swift @@ -13,21 +13,26 @@ import Starscream class SpyDelegate: Web3SocketDelegate { // Setting .None is unnecessary, but helps with clarity imho - var somethingWithDelegateResult: String? = nil + var somethingWithDelegateResult: Any? = nil // Async test code needs to fulfill the XCTestExpecation used for the test // when all the async operations have been completed. For this reason we need // to store a reference to the expectation var asyncExpectation: XCTestExpectation? + var fulfilled = false func received(message: Any) { + somethingWithDelegateResult = message guard let expectation = asyncExpectation else { XCTFail("SpyDelegate was not setup correctly. Missing XCTExpectation reference") return } - - print(message as! String) - expectation.fulfill() + print(message) + if !fulfilled { + print("fullfilled") + fulfilled = true + expectation.fulfill() + } } func gotError(error: Error) { @@ -44,19 +49,93 @@ class web3swift_websocket_Tests: XCTestCase { guard let socketProvider = InfuraWebsocketProvider.connectToInfuraSocket(.Mainnet, delegate: spyDelegate) else { return XCTFail() } + self.socketProvider = socketProvider + spyDelegate.asyncExpectation = expectation(description: "Delegate called") + try! self.socketProvider!.subscribeOnNewPendingTransactions() +// DispatchQueue.main.asyncAfter(deadline: .now()+5) { [unowned self] in +// try! self.socketProvider!.subscribeOnNewPendingTransactions() +// } + waitForExpectations(timeout: 1000) { error in + if let error = error { + XCTFail("waitForExpectationsWithTimeout errored: \(error)") + } + + guard self.spyDelegate.somethingWithDelegateResult != nil else { + XCTFail("Expected delegate to be called") + return + } + + XCTAssert(true) + } + } + + func testSubscribeOnLogs() { + guard let socketProvider = InfuraWebsocketProvider.connectToInfuraSocket(.Mainnet, delegate: spyDelegate) else { + return XCTFail() + } + self.socketProvider = socketProvider + spyDelegate.asyncExpectation = expectation(description: "Delegate called") + try! self.socketProvider!.subscribeOnLogs(addresses: [EthereumAddress("0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359")!], topics: nil) + // DispatchQueue.main.asyncAfter(deadline: .now()+5) { [unowned self] in + // try! self.socketProvider!.subscribeOnNewPendingTransactions() + // } + waitForExpectations(timeout: 1000) { error in + if let error = error { + XCTFail("waitForExpectationsWithTimeout errored: \(error)") + } + + guard self.spyDelegate.somethingWithDelegateResult != nil else { + XCTFail("Expected delegate to be called") + return + } + + XCTAssert(true) + } + } + + func testSubscribeOnNewHeads() { + guard let socketProvider = InfuraWebsocketProvider.connectToInfuraSocket(.Mainnet, delegate: spyDelegate) else { + return XCTFail() + } + self.socketProvider = socketProvider + spyDelegate.asyncExpectation = expectation(description: "Delegate called") + try! self.socketProvider!.subscribeOnNewHeads() + // DispatchQueue.main.asyncAfter(deadline: .now()+5) { [unowned self] in + // try! self.socketProvider!.subscribeOnNewPendingTransactions() + // } + waitForExpectations(timeout: 1000) { error in + if let error = error { + XCTFail("waitForExpectationsWithTimeout errored: \(error)") + } + + guard self.spyDelegate.somethingWithDelegateResult != nil else { + XCTFail("Expected delegate to be called") + return + } + + XCTAssert(true) + } + } + + func testFilter() { + guard let socketProvider = InfuraWebsocketProvider.connectToInfuraSocket(.Mainnet, delegate: spyDelegate) else { + return XCTFail() + } + self.socketProvider = socketProvider spyDelegate.asyncExpectation = expectation(description: "Delegate called") - try! socketProvider.filter(method: .newPendingTransactionFilter) - - waitForExpectations(timeout: 100) { error in + try! self.socketProvider?.setFilterAndGetLogs(method: .newFilter, address: EthereumAddress("0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359")!, fromBlock: .earliest, toBlock: .latest, topics: nil) + // DispatchQueue.main.asyncAfter(deadline: .now()+5) { [unowned self] in + // try! self.socketProvider!.subscribeOnNewPendingTransactions() + // } + waitForExpectations(timeout: 1000) { error in if let error = error { XCTFail("waitForExpectationsWithTimeout errored: \(error)") } - guard let result = self.spyDelegate.somethingWithDelegateResult else { + guard self.spyDelegate.somethingWithDelegateResult != nil else { XCTFail("Expected delegate to be called") return } - print(result) XCTAssert(true) } diff --git a/web3swift.xcodeproj/project.pbxproj b/web3swift.xcodeproj/project.pbxproj index 6f4274fbc..9a3a150a0 100755 --- a/web3swift.xcodeproj/project.pbxproj +++ b/web3swift.xcodeproj/project.pbxproj @@ -53,6 +53,8 @@ 13C3392521B6C62400F33F5E /* secp256k1_ec_mult_static_context.h in Headers */ = {isa = PBXBuildFile; fileRef = 13C338F621B6C62400F33F5E /* secp256k1_ec_mult_static_context.h */; }; 13C3392621B6C62400F33F5E /* scratch.h in Headers */ = {isa = PBXBuildFile; fileRef = 13C338F721B6C62400F33F5E /* scratch.h */; }; 13C3392821B6C68900F33F5E /* secp256k1.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13C3388E21B6C2DD00F33F5E /* secp256k1.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 3A7EA35E2280EA9A005120C2 /* Encodable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7EA35D2280EA9A005120C2 /* Encodable+Extensions.swift */; }; + 3A7EA3602280EB27005120C2 /* Decodable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7EA35F2280EB27005120C2 /* Decodable+Extensions.swift */; }; 3AA8151C2276E42F00F5DB52 /* EventFiltering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA815172276E42F00F5DB52 /* EventFiltering.swift */; }; 3AA8151D2276E42F00F5DB52 /* ComparisonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA815182276E42F00F5DB52 /* ComparisonExtensions.swift */; }; 3AA8151E2276E42F00F5DB52 /* EthereumFilterEncodingExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA815192276E42F00F5DB52 /* EthereumFilterEncodingExtensions.swift */; }; @@ -242,6 +244,8 @@ 13CE02B021FC846800CE7148 /* RELEASE_GUIDE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = RELEASE_GUIDE.md; sourceTree = ""; }; 13CE02B121FC846900CE7148 /* BUILD_GUIDE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = BUILD_GUIDE.md; sourceTree = ""; }; 13CE02B421FC849400CE7148 /* Web3swift.pod.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Web3swift.pod.podspec; sourceTree = ""; }; + 3A7EA35D2280EA9A005120C2 /* Encodable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+Extensions.swift"; sourceTree = ""; }; + 3A7EA35F2280EB27005120C2 /* Decodable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decodable+Extensions.swift"; sourceTree = ""; }; 3AA815172276E42F00F5DB52 /* EventFiltering.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventFiltering.swift; sourceTree = ""; }; 3AA815182276E42F00F5DB52 /* ComparisonExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComparisonExtensions.swift; sourceTree = ""; }; 3AA815192276E42F00F5DB52 /* EthereumFilterEncodingExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EthereumFilterEncodingExtensions.swift; sourceTree = ""; }; @@ -716,6 +720,8 @@ 3AA815642276E44100F5DB52 /* BigUInt+Extensions.swift */, 3AA815652276E44100F5DB52 /* CryptoExtensions.swift */, 3AA815662276E44100F5DB52 /* String+Extension.swift */, + 3A7EA35D2280EA9A005120C2 /* Encodable+Extensions.swift */, + 3A7EA35F2280EB27005120C2 /* Decodable+Extensions.swift */, 3AA815672276E44100F5DB52 /* NSRegularExpressionExtension.swift */, 3AA815682276E44100F5DB52 /* Dictionary+Extension.swift */, 3AA815692276E44100F5DB52 /* Data+Extension.swift */, @@ -1154,6 +1160,7 @@ buildActionMask = 2147483647; files = ( 3AA815CF2276E44100F5DB52 /* Web3+Eth+Websocket.swift in Sources */, + 3A7EA35E2280EA9A005120C2 /* Encodable+Extensions.swift in Sources */, 3AA815D22276E44100F5DB52 /* Web3+JSONRPC.swift in Sources */, 3AA815F82276E44100F5DB52 /* Promise+Web3+Contract+GetIndexedEvents.swift in Sources */, 3AA815E42276E44100F5DB52 /* Data+Extension.swift in Sources */, @@ -1219,6 +1226,7 @@ 3AA815DA2276E44100F5DB52 /* Web3+Eth.swift in Sources */, 3AA816082276E44100F5DB52 /* Web3+ERC1644.swift in Sources */, 3AA815AB2276E44100F5DB52 /* ETHRegistrarController.swift in Sources */, + 3A7EA3602280EB27005120C2 /* Decodable+Extensions.swift in Sources */, 3AA816022276E44100F5DB52 /* Web3+ERC1633.swift in Sources */, 3AA815CB2276E44100F5DB52 /* Web3.swift in Sources */, 3AA815B62276E44100F5DB52 /* BIP39+WordLists.swift in Sources */,