diff --git a/web3sTests/Client/EthereumClientTests.swift b/web3sTests/Client/EthereumClientTests.swift index 0f86b27b..d4c1b6b8 100644 --- a/web3sTests/Client/EthereumClientTests.swift +++ b/web3sTests/Client/EthereumClientTests.swift @@ -483,7 +483,7 @@ class EthereumWebSocketClientTests: EthereumClientTests { await waitForExpectations(timeout: 5, handler: nil) XCTAssertNotEqual(subscription.id, "") - XCTAssertEqual(subscription.type, .pendingTransactions) + XCTAssertEqual(subscription.type, .newPendingTransactions) } catch { XCTFail("Expected subscription but failed \(error).") } @@ -512,6 +512,31 @@ class EthereumWebSocketClientTests: EthereumClientTests { } } + func testWebSocketLogs() async { + do { + guard let client = client as? EthereumWebSocketClient else { + XCTFail("Expected client to be EthereumWebSocketClient") + return + } + + var expectation: XCTestExpectation? = self.expectation(description: "Logs") + let type = EthereumSubscriptionType.logs(nil) + let subscription = try await client.logs { log in + print(log) + expectation?.fulfill() + expectation = nil + } + + // we need a high timeout as new block might take a while + await waitForExpectations(timeout: 2500, handler: nil) + + XCTAssertNotEqual(subscription.id, "") + XCTAssertEqual(subscription.type, type) + } catch { + XCTFail("Expected subscription but failed \(error).") + } + } + func testWebSocketSubscribe() async { do { guard let client = client as? EthereumWebSocketClient else { @@ -521,7 +546,7 @@ class EthereumWebSocketClientTests: EthereumClientTests { client.delegate = self delegateExpectation = expectation(description: "onNewPendingTransaction delegate call") - var subscription = try await client.subscribe(type: .pendingTransactions) + var subscription = try await client.subscribe(type: .newPendingTransactions) await waitForExpectations(timeout: 10) _ = try await client.unsubscribe(subscription) @@ -529,6 +554,12 @@ class EthereumWebSocketClientTests: EthereumClientTests { subscription = try await client.subscribe(type: .newBlockHeaders) await waitForExpectations(timeout: 2500) _ = try await client.unsubscribe(subscription) + + delegateExpectation = expectation(description: "onLogs delegate call") + let type = EthereumSubscriptionType.logs(nil) + subscription = try await client.subscribe(type: type) + await waitForExpectations(timeout: 2500) + _ = try await client.unsubscribe(subscription) } catch { XCTFail("Expected subscription but failed \(error).") } @@ -561,4 +592,9 @@ extension EthereumWebSocketClientTests: EthereumWebSocketClientDelegate { delegateExpectation?.fulfill() delegateExpectation = nil } + + func onLog(subscription: EthereumSubscription, log: EthereumLog) { + delegateExpectation?.fulfill() + delegateExpectation = nil + } } diff --git a/web3swift/src/Client/EthereumClientProtocol.swift b/web3swift/src/Client/EthereumClientProtocol.swift index 45a6e6e3..a281b030 100644 --- a/web3swift/src/Client/EthereumClientProtocol.swift +++ b/web3swift/src/Client/EthereumClientProtocol.swift @@ -109,6 +109,9 @@ public protocol EthereumClientProtocol: AnyObject { func newBlockHeaders(onSubscribe: @escaping (Result) -> Void, onData: @escaping (EthereumHeader) -> Void) func newBlockHeaders(onData: @escaping (EthereumHeader) -> Void) async throws -> EthereumSubscription + func logs(logsParams: LogsParams?, onSubscribe: @escaping (Result) -> Void, onData: @escaping (EthereumLog) -> Void) + func logs(logsParams: LogsParams?, onData: @escaping (EthereumLog) -> Void) async throws -> EthereumSubscription + func syncing(onSubscribe: @escaping (Result) -> Void, onData: @escaping (EthereumSyncStatus) -> Void) func syncing(onData: @escaping (EthereumSyncStatus) -> Void) async throws -> EthereumSubscription } @@ -116,6 +119,7 @@ public protocol EthereumClientProtocol: AnyObject { public protocol EthereumWebSocketClientDelegate: AnyObject { func onNewPendingTransaction(subscription: EthereumSubscription, txHash: String) func onNewBlockHeader(subscription: EthereumSubscription, header: EthereumHeader) + func onLog(subscription: EthereumSubscription, log: EthereumLog) func onSyncing(subscription: EthereumSubscription, sync: EthereumSyncStatus) func onWebSocketReconnect() } @@ -125,6 +129,8 @@ public protocol EthereumClientProtocol: AnyObject { func onNewBlockHeader(subscription: EthereumSubscription, header: EthereumHeader) {} + func onLog(subscription: EthereumSubscription, log: EthereumLog) {} + func onSyncing(subscription: EthereumSubscription, sync: EthereumSyncStatus) {} func onWebSocketReconnect() {} diff --git a/web3swift/src/Client/EthereumWebSocketClient.swift b/web3swift/src/Client/EthereumWebSocketClient.swift index 84c42ccf..534b9815 100644 --- a/web3swift/src/Client/EthereumWebSocketClient.swift +++ b/web3swift/src/Client/EthereumWebSocketClient.swift @@ -94,7 +94,7 @@ extension EthereumWebSocketClient: EthereumClientWebSocketProtocol { public func subscribe(type: EthereumSubscriptionType) async throws -> EthereumSubscription { do { - let data = try await networkProvider.send(method: "eth_subscribe", params: [type.method, type.params].compactMap { $0 }, receive: String.self) + let data = try await networkProvider.send(method: "eth_subscribe", params: type.params.compactMap { $0 }, receive: String.self) if let resDataString = data as? String { let subscription = EthereumSubscription(type: type, id: resDataString) provider.addSubscription(subscription, callback: { _ in }) @@ -123,9 +123,9 @@ public func pendingTransactions(onData: @escaping (String) -> Void) async throws -> EthereumSubscription { do { - let data = try await networkProvider.send(method: "eth_subscribe", params: [EthereumSubscriptionType.pendingTransactions.method], receive: String.self) + let data = try await networkProvider.send(method: "eth_subscribe", params: EthereumSubscriptionType.newPendingTransactions.params, receive: String.self) if let resDataString = data as? String { - let subscription = EthereumSubscription(type: .pendingTransactions, id: resDataString) + let subscription = EthereumSubscription(type: .newPendingTransactions, id: resDataString) provider.addSubscription(subscription, callback: { object in onData(object as! String) }) @@ -140,7 +140,7 @@ public func newBlockHeaders(onData: @escaping (EthereumHeader) -> Void) async throws -> EthereumSubscription { do { - let data = try await networkProvider.send(method: "eth_subscribe", params: [EthereumSubscriptionType.newBlockHeaders.method], receive: String.self) + let data = try await networkProvider.send(method: "eth_subscribe", params: EthereumSubscriptionType.newBlockHeaders.params, receive: String.self) if let resDataString = data as? String { let subscription = EthereumSubscription(type: .newBlockHeaders, id: resDataString) provider.addSubscription(subscription, callback: { object in @@ -155,9 +155,27 @@ } } + public func logs(logsParams: LogsParams? = nil, onData: @escaping (EthereumLog) -> Void) async throws -> EthereumSubscription { + do { + let type: EthereumSubscriptionType = .logs(logsParams) + let data = try await networkProvider.send(method: "eth_subscribe", params: type.params, receive: String.self) + if let resDataString = data as? String { + let subscription = EthereumSubscription(type: type, id: resDataString) + provider.addSubscription(subscription, callback: { object in + onData(object as! EthereumLog) + }) + return subscription + } else { + throw EthereumClientError.unexpectedReturnValue + } + } catch { + throw failureHandler(error) + } + } + public func syncing(onData: @escaping (EthereumSyncStatus) -> Void) async throws -> EthereumSubscription { do { - let data = try await networkProvider.send(method: "eth_subscribe", params: [EthereumSubscriptionType.syncing.method], receive: String.self) + let data = try await networkProvider.send(method: "eth_subscribe", params: EthereumSubscriptionType.syncing.params, receive: String.self) if let resDataString = data as? String { let subscription = EthereumSubscription(type: .syncing, id: resDataString) provider.addSubscription(subscription, callback: { object in @@ -218,6 +236,17 @@ } } + public func logs(logsParams: LogsParams? = nil, onSubscribe: @escaping (Result) -> Void, onData: @escaping (EthereumLog) -> Void) { + Task { + do { + let result = try await logs(logsParams: logsParams, onData: onData) + onSubscribe(.success(result)) + } catch { + failureHandler(error, completionHandler: onSubscribe) + } + } + } + public func syncing(onSubscribe: @escaping (Result) -> Void, onData: @escaping (EthereumSyncStatus) -> Void) { Task { do { diff --git a/web3swift/src/Client/Models/EthereumSubscription.swift b/web3swift/src/Client/Models/EthereumSubscription.swift index 64f3dc23..fc3e3efc 100644 --- a/web3swift/src/Client/Models/EthereumSubscription.swift +++ b/web3swift/src/Client/Models/EthereumSubscription.swift @@ -7,31 +7,56 @@ import Foundation public enum EthereumSubscriptionType: Equatable, Hashable { case newBlockHeaders - case pendingTransactions + case logs(LogsParams?) + case newPendingTransactions case syncing - var method: String { + var params: [EthereumSubscriptionParamElement] { switch self { case .newBlockHeaders: - return "newHeads" - case .pendingTransactions: - return "newPendingTransactions" + return [.method("newHeads")] + case let .logs(params): + return [.method("logs"), .logsParams(params ?? .init(address: nil, topics: nil))] + case .newPendingTransactions: + return [.method("newPendingTransactions")] case .syncing: - return "syncing" + return [.method("syncing")] } } - - struct LogParams: Encodable { - let address: [EthereumAddress] - let topics: [String?] - } - - var params: String? { - nil - } } public struct EthereumSubscription: Hashable { let type: EthereumSubscriptionType let id: String } + +public enum EthereumSubscriptionParamElement: Encodable { + case method(String) + case logsParams(LogsParams) + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .method(x): + try container.encode(x) + case let .logsParams(x): + try container.encode(x) + } + } +} + +// MARK: - ParamClass +public struct LogsParams: Codable, Equatable, Hashable { + public let address: EthereumAddress? + public let topics: [String]? + + enum CodingKeys: String, CodingKey { + case address = "address" + case topics = "topics" + } + + public init(address: EthereumAddress?, topics: [String]?) { + self.address = address + self.topics = topics + } +} diff --git a/web3swift/src/Client/NetworkProviders/WebSocketNetworkProvider.swift b/web3swift/src/Client/NetworkProviders/WebSocketNetworkProvider.swift index f4970a5c..d04634b5 100644 --- a/web3swift/src/Client/NetworkProviders/WebSocketNetworkProvider.swift +++ b/web3swift/src/Client/NetworkProviders/WebSocketNetworkProvider.swift @@ -331,7 +331,7 @@ self.delegate?.onNewBlockHeader(subscription: subscription.key, header: response.params.result) subscription.value(response.params.result) } - case .pendingTransactions: + case .newPendingTransactions: if let data = string.data(using: .utf8), let response = try? JSONDecoder().decode(JSONRPCSubscriptionResponse.self, from: data) { self.delegate?.onNewPendingTransaction(subscription: subscription.key, txHash: response.params.result) subscription.value(response.params.result) @@ -341,6 +341,11 @@ self.delegate?.onSyncing(subscription: subscription.key, sync: response.params.result) subscription.value(response.params.result) } + case .logs: + if let data = string.data(using: .utf8), let response = try? JSONDecoder().decode(JSONRPCSubscriptionResponse.self, from: data) { + self.delegate?.onLog(subscription: subscription.key, log: response.params.result) + subscription.value(response.params.result) + } } }