From 0041b8e70d6a9df116d14204312920f2bda74096 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Mon, 16 Jan 2023 16:58:01 +0500 Subject: [PATCH 1/6] Expiry validation on request received --- .../Engine/Common/SessionEngine.swift | 11 +++++-- Sources/WalletConnectSign/Request.swift | 33 ++++++++++++++++--- .../WalletConnectSign/Sign/SignClient.swift | 4 +-- .../Types/Session/SessionType.swift | 1 + .../Types/SignReasonCode.swift | 7 ++++ Tests/WalletConnectSignTests/Stub/Stubs.swift | 2 +- 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 7f552ca32..98de13b0a 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -54,7 +54,7 @@ final class SessionEngine { guard session.hasPermission(forMethod: request.method, onChain: request.chainId) else { throw WalletConnectError.invalidPermissions } - let chainRequest = SessionType.RequestParams.Request(method: request.method, params: request.params) + let chainRequest = SessionType.RequestParams.Request(method: request.method, params: request.params, expiry: request.expiry) let sessionRequestParams = SessionType.RequestParams(request: chainRequest, chainId: request.chainId) let protocolMethod = SessionRequestProtocolMethod() let rpcRequest = RPCRequest(method: protocolMethod.method, params: sessionRequestParams) @@ -191,7 +191,9 @@ private extension SessionEngine { topic: payload.topic, method: payload.request.request.method, params: payload.request.request.params, - chainId: payload.request.chainId) + chainId: payload.request.chainId, + expiry: payload.request.request.expiry + ) guard let session = sessionStore.getSession(forTopic: topic) else { return respondError(payload: payload, reason: .noSessionForTopic, protocolMethod: protocolMethod) @@ -202,6 +204,11 @@ private extension SessionEngine { guard session.hasPermission(forMethod: request.method, onChain: request.chainId) else { return respondError(payload: payload, reason: .unauthorizedMethod(request.method), protocolMethod: protocolMethod) } + + guard !request.isExpired() else { + return respondError(payload: payload, reason: .sessionRequestExpired, protocolMethod: protocolMethod) + } + onSessionRequest?(request) } diff --git a/Sources/WalletConnectSign/Request.swift b/Sources/WalletConnectSign/Request.swift index 4820b8fc0..e41ea77de 100644 --- a/Sources/WalletConnectSign/Request.swift +++ b/Sources/WalletConnectSign/Request.swift @@ -6,20 +6,43 @@ public struct Request: Codable, Equatable { public let method: String public let params: AnyCodable public let chainId: Blockchain + public let expiry: UInt64? - internal init(id: RPCID, topic: String, method: String, params: AnyCodable, chainId: Blockchain) { + internal init(id: RPCID, topic: String, method: String, params: AnyCodable, chainId: Blockchain, expiry: UInt64?) { self.id = id self.topic = topic self.method = method self.params = params self.chainId = chainId + self.expiry = expiry } - public init(topic: String, method: String, params: AnyCodable, chainId: Blockchain) { - self.init(id: RPCID(JsonRpcID.generate()), topic: topic, method: method, params: params, chainId: chainId) + public init(topic: String, method: String, params: AnyCodable, chainId: Blockchain, expiry: UInt64? = nil) { + self.init(id: RPCID(JsonRpcID.generate()), topic: topic, method: method, params: params, chainId: chainId, expiry: expiry) } - internal init(id: RPCID, topic: String, method: String, params: C, chainId: Blockchain) where C: Codable { - self.init(id: id, topic: topic, method: method, params: AnyCodable(params), chainId: chainId) + init(id: RPCID, topic: String, method: String, params: C, chainId: Blockchain, expiry: UInt64?) where C: Codable { + self.init(id: id, topic: topic, method: method, params: AnyCodable(params), chainId: chainId, expiry: expiry) + } + + func isExpired() -> Bool { + guard let expiry = expiry else { return false } + + let expiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) + + guard + Date().distance(to: expiryDate) > Constants.maxExpiry, + Date().distance(to: expiryDate) < Constants.minExpiry + else { return true } + + return expiryDate < Date() + } +} + +private extension Request { + + struct Constants { + static let minExpiry: TimeInterval = 300 // 5 minutes + static let maxExpiry: TimeInterval = 604800 // 7 days } } diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index 6d015414e..b2f539272 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -326,7 +326,7 @@ public final class SignClient: SignClientProtocol { let pendingRequests: [Request] = history.getPending() .compactMap { guard let request = try? $0.request.params?.get(SessionType.RequestParams.self) else { return nil } - return Request(id: $0.id, topic: $0.topic, method: request.request.method, params: request.request.params, chainId: request.chainId) + return Request(id: $0.id, topic: $0.topic, method: request.request.method, params: request.request.params, chainId: request.chainId, expiry: request.request.expiry) } if let topic = topic { return pendingRequests.filter {$0.topic == topic} @@ -343,7 +343,7 @@ public final class SignClient: SignClientProtocol { let request = try? record.request.params?.get(SessionType.RequestParams.self) else { return nil } - return Request(id: record.id, topic: record.topic, method: record.request.method, params: request, chainId: request.chainId) + return Request(id: record.id, topic: record.topic, method: record.request.method, params: request, chainId: request.chainId, expiry: request.request.expiry) } /// Delete all stored data such as: pairings, sessions, keys diff --git a/Sources/WalletConnectSign/Types/Session/SessionType.swift b/Sources/WalletConnectSign/Types/Session/SessionType.swift index d6c7c87d5..2731e448c 100644 --- a/Sources/WalletConnectSign/Types/Session/SessionType.swift +++ b/Sources/WalletConnectSign/Types/Session/SessionType.swift @@ -42,6 +42,7 @@ internal enum SessionType { struct Request: Codable, Equatable { let method: String let params: AnyCodable + let expiry: UInt64? } } diff --git a/Sources/WalletConnectSign/Types/SignReasonCode.swift b/Sources/WalletConnectSign/Types/SignReasonCode.swift index 522cbfb4f..e1435dab1 100644 --- a/Sources/WalletConnectSign/Types/SignReasonCode.swift +++ b/Sources/WalletConnectSign/Types/SignReasonCode.swift @@ -37,6 +37,9 @@ enum SignReasonCode: Reason, Codable, Equatable { // 6000 case userDisconnected + // 8000 + case sessionRequestExpired + var code: Int { switch self { case .invalidMethod: return 1001 @@ -66,6 +69,8 @@ enum SignReasonCode: Reason, Codable, Equatable { case .userDisconnected: return 6000 case .noSessionForTopic: return 7001 + + case .sessionRequestExpired: return 8000 } } @@ -117,6 +122,8 @@ enum SignReasonCode: Reason, Codable, Equatable { return "Unsupported namespace key" case .userDisconnected: return "User discconnected" + case .sessionRequestExpired: + return "Session request expired or expiry param validation failed (MIN_INTERVAL: 300, MAX_INTERVAL: 604800)" } } } diff --git a/Tests/WalletConnectSignTests/Stub/Stubs.swift b/Tests/WalletConnectSignTests/Stub/Stubs.swift index 3789cdb3e..1f22303f7 100644 --- a/Tests/WalletConnectSignTests/Stub/Stubs.swift +++ b/Tests/WalletConnectSignTests/Stub/Stubs.swift @@ -70,7 +70,7 @@ extension RPCRequest { static func stubRequest(method: String, chainId: Blockchain) -> RPCRequest { let params = SessionType.RequestParams( - request: SessionType.RequestParams.Request(method: method, params: AnyCodable(EmptyCodable())), + request: SessionType.RequestParams.Request(method: method, params: AnyCodable(EmptyCodable()), expiry: nil), chainId: chainId) return RPCRequest(method: SessionRequestProtocolMethod().method, params: params) } From 87027ef74d6cf9301da8d40e9523e9a922a4e849 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 20 Jan 2023 16:35:47 +0500 Subject: [PATCH 2/6] HistoryService + Validate on respond --- .../Engine/Common/SessionEngine.swift | 31 +++++++++++++- .../Services/HistoryService.swift | 42 +++++++++++++++++++ .../WalletConnectSign/Sign/SignClient.swift | 22 +++------- .../Sign/SignClientFactory.swift | 5 ++- 4 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 Sources/WalletConnectSign/Services/HistoryService.swift diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index 98de13b0a..f06fd7179 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -4,6 +4,7 @@ import Combine final class SessionEngine { enum Errors: Error { case sessionNotFound(topic: String) + case sessionRequestExpired } var onSessionsUpdate: (([Session]) -> Void)? @@ -15,17 +16,20 @@ final class SessionEngine { private let sessionStore: WCSessionStorage private let networkingInteractor: NetworkInteracting + private let historyService: HistoryService private let kms: KeyManagementServiceProtocol private var publishers = [AnyCancellable]() private let logger: ConsoleLogging init( networkingInteractor: NetworkInteracting, + historyService: HistoryService, kms: KeyManagementServiceProtocol, sessionStore: WCSessionStorage, logger: ConsoleLogging ) { self.networkingInteractor = networkingInteractor + self.historyService = historyService self.kms = kms self.sessionStore = sessionStore self.logger = logger @@ -65,8 +69,24 @@ final class SessionEngine { guard sessionStore.hasSession(forTopic: topic) else { throw Errors.sessionNotFound(topic: topic) } - let response = RPCResponse(id: requestId, outcome: response) - try await networkingInteractor.respond(topic: topic, response: response, protocolMethod: SessionRequestProtocolMethod()) + + let protocolMethod = SessionRequestProtocolMethod() + + guard sessionRequestNotExpired(requestId: requestId) else { + try await networkingInteractor.respondError( + topic: topic, + requestId: requestId, + protocolMethod: protocolMethod, + reason: SignReasonCode.sessionRequestExpired + ) + throw Errors.sessionRequestExpired + } + + try await networkingInteractor.respond( + topic: topic, + response: RPCResponse(id: requestId, outcome: response), + protocolMethod: protocolMethod + ) } func emit(topic: String, event: SessionType.EventParams.Event, chainId: Blockchain) async throws { @@ -159,6 +179,13 @@ private extension SessionEngine { } } + func sessionRequestNotExpired(requestId: RPCID) -> Bool { + guard let request = historyService.getSessionRequest(id: requestId) + else { return false } + + return !request.isExpired() + } + func respondError(payload: SubscriptionPayload, reason: SignReasonCode, protocolMethod: ProtocolMethod) { Task(priority: .high) { do { diff --git a/Sources/WalletConnectSign/Services/HistoryService.swift b/Sources/WalletConnectSign/Services/HistoryService.swift new file mode 100644 index 000000000..394ff0c61 --- /dev/null +++ b/Sources/WalletConnectSign/Services/HistoryService.swift @@ -0,0 +1,42 @@ +import Foundation + +final class HistoryService { + + private let history: RPCHistory + + init(history: RPCHistory) { + self.history = history + } + + func getPendingRequests() -> [Request] { + return history.getPending() + .compactMap { mapRequestRecord($0) } + .filter { !$0.isExpired() } + } + + func getPendingRequests(topic: String) -> [Request] { + return getPendingRequests().filter { $0.topic == topic } + } + + public func getSessionRequest(id: RPCID) -> Request? { + guard let record = history.get(recordId: id) else { return nil } + return mapRequestRecord(record) + } +} + +private extension HistoryService { + + func mapRequestRecord(_ record: RPCHistory.Record) -> Request? { + guard let request = try? record.request.params?.get(SessionType.RequestParams.self) + else { return nil } + + return Request( + id: record.id, + topic: record.topic, + method: request.request.method, + params: request.request.params, + chainId: request.chainId, + expiry: request.request.expiry + ) + } +} diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index b2f539272..dd0614324 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -110,7 +110,7 @@ public final class SignClient: SignClientProtocol { private let nonControllerSessionStateMachine: NonControllerSessionStateMachine private let controllerSessionStateMachine: ControllerSessionStateMachine private let appProposeService: AppProposeService - private let history: RPCHistory + private let historyService: HistoryService private let cleanupService: SignCleanupService private let sessionProposalPublisherSubject = PassthroughSubject() @@ -140,7 +140,7 @@ public final class SignClient: SignClientProtocol { controllerSessionStateMachine: ControllerSessionStateMachine, appProposeService: AppProposeService, disconnectService: DisconnectService, - history: RPCHistory, + historyService: HistoryService, cleanupService: SignCleanupService, pairingClient: PairingClient ) { @@ -153,7 +153,7 @@ public final class SignClient: SignClientProtocol { self.nonControllerSessionStateMachine = nonControllerSessionStateMachine self.controllerSessionStateMachine = controllerSessionStateMachine self.appProposeService = appProposeService - self.history = history + self.historyService = historyService self.cleanupService = cleanupService self.disconnectService = disconnectService self.pairingClient = pairingClient @@ -323,27 +323,17 @@ public final class SignClient: SignClientProtocol { /// - Returns: Pending requests received from peer with `wc_sessionRequest` protocol method /// - Parameter topic: topic representing session for which you want to get pending requests. If nil, you will receive pending requests for all active sessions. public func getPendingRequests(topic: String? = nil) -> [Request] { - let pendingRequests: [Request] = history.getPending() - .compactMap { - guard let request = try? $0.request.params?.get(SessionType.RequestParams.self) else { return nil } - return Request(id: $0.id, topic: $0.topic, method: request.request.method, params: request.request.params, chainId: request.chainId, expiry: request.request.expiry) - } if let topic = topic { - return pendingRequests.filter {$0.topic == topic} + return historyService.getPendingRequests(topic: topic) } else { - return pendingRequests + return historyService.getPendingRequests() } } /// - Parameter id: id of a wc_sessionRequest jsonrpc request /// - Returns: json rpc record object for given id or nil if record for give id does not exits public func getSessionRequestRecord(id: RPCID) -> Request? { - guard - let record = history.get(recordId: id), - let request = try? record.request.params?.get(SessionType.RequestParams.self) - else { return nil } - - return Request(id: record.id, topic: record.topic, method: record.request.method, params: request, chainId: request.chainId, expiry: request.request.expiry) + return historyService.getSessionRequest(id: id) } /// Delete all stored data such as: pairings, sessions, keys diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift index ce27c1e4c..29b0a6987 100644 --- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -25,7 +25,8 @@ public struct SignClientFactory { let sessionStore = SessionStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: SignStorageIdentifiers.sessions.rawValue))) let sessionToPairingTopic = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: SignStorageIdentifiers.sessionToPairingTopic.rawValue) let proposalPayloadsStore = CodableStore>(defaults: RuntimeKeyValueStorage(), identifier: SignStorageIdentifiers.proposals.rawValue) - let sessionEngine = SessionEngine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) + let historyService = HistoryService(history: rpcHistory) + let sessionEngine = SessionEngine(networkingInteractor: networkingClient, historyService: historyService, kms: kms, sessionStore: sessionStore, logger: logger) let nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger) let approveEngine = ApproveEngine(networkingInteractor: networkingClient, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, pairingRegisterer: pairingClient, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore) @@ -47,7 +48,7 @@ public struct SignClientFactory { controllerSessionStateMachine: controllerSessionStateMachine, appProposeService: appProposerService, disconnectService: disconnectService, - history: rpcHistory, + historyService: historyService, cleanupService: cleanupService, pairingClient: pairingClient ) From 8771a95bb2ded121ce0440ccaa7a783e29a8c887 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 20 Jan 2023 20:02:42 +0500 Subject: [PATCH 3/6] TTL extension --- .../Engine/Common/SessionEngine.swift | 2 +- Sources/WalletConnectSign/Request.swift | 9 +++++++++ .../SessionRequestProtocolMethod.swift | 17 +++++++++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift index f06fd7179..562726920 100644 --- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift @@ -60,7 +60,7 @@ final class SessionEngine { } let chainRequest = SessionType.RequestParams.Request(method: request.method, params: request.params, expiry: request.expiry) let sessionRequestParams = SessionType.RequestParams(request: chainRequest, chainId: request.chainId) - let protocolMethod = SessionRequestProtocolMethod() + let protocolMethod = SessionRequestProtocolMethod(ttl: request.calculateTtl()) let rpcRequest = RPCRequest(method: protocolMethod.method, params: sessionRequestParams) try await networkingInteractor.request(rpcRequest, topic: request.topic, protocolMethod: SessionRequestProtocolMethod()) } diff --git a/Sources/WalletConnectSign/Request.swift b/Sources/WalletConnectSign/Request.swift index e41ea77de..38d5a69da 100644 --- a/Sources/WalletConnectSign/Request.swift +++ b/Sources/WalletConnectSign/Request.swift @@ -37,6 +37,15 @@ public struct Request: Codable, Equatable { return expiryDate < Date() } + + func calculateTtl() -> Int { + guard let expiry = expiry else { return SessionRequestProtocolMethod.defaultTtl } + + let expiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) + let diff = expiryDate - Date().timeIntervalSince1970 + + return Int(diff.timeIntervalSince1970) + } } private extension Request { diff --git a/Sources/WalletConnectSign/Types/ProtocolMethods/SessionRequestProtocolMethod.swift b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionRequestProtocolMethod.swift index 13d77a32e..28ee6d446 100644 --- a/Sources/WalletConnectSign/Types/ProtocolMethods/SessionRequestProtocolMethod.swift +++ b/Sources/WalletConnectSign/Types/ProtocolMethods/SessionRequestProtocolMethod.swift @@ -1,9 +1,22 @@ import Foundation struct SessionRequestProtocolMethod: ProtocolMethod { + + static let defaultTtl: Int = 300 + let method: String = "wc_sessionRequest" - let requestConfig = RelayConfig(tag: 1108, prompt: true, ttl: 300) + private let ttl: Int + + var requestConfig: RelayConfig { + RelayConfig(tag: 1108, prompt: true, ttl: ttl) + } + + var responseConfig: RelayConfig { + RelayConfig(tag: 1109, prompt: false, ttl: ttl) + } - let responseConfig = RelayConfig(tag: 1109, prompt: false, ttl: 300) + init(ttl: Int = Self.defaultTtl) { + self.ttl = ttl + } } From 89df241b4bbc8975f91062614370f152b6beefa4 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 20 Jan 2023 20:38:02 +0500 Subject: [PATCH 4/6] calculateTtl tests --- .../xcschemes/WalletConnectSignTests.xcscheme | 52 +++++++++++++++++++ Sources/WalletConnectSign/Request.swift | 13 +++-- .../SessionRequestTests.swift | 48 +++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/WalletConnectSignTests.xcscheme create mode 100644 Tests/WalletConnectSignTests/SessionRequestTests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectSignTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectSignTests.xcscheme new file mode 100644 index 000000000..2b307dc3b --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectSignTests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/WalletConnectSign/Request.swift b/Sources/WalletConnectSign/Request.swift index 38d5a69da..8999cba52 100644 --- a/Sources/WalletConnectSign/Request.swift +++ b/Sources/WalletConnectSign/Request.swift @@ -31,18 +31,23 @@ public struct Request: Codable, Equatable { let expiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) guard - Date().distance(to: expiryDate) > Constants.maxExpiry, - Date().distance(to: expiryDate) < Constants.minExpiry + Date().distance(to: expiryDate) < Constants.maxExpiry, + Date().distance(to: expiryDate) > Constants.minExpiry else { return true } return expiryDate < Date() } - func calculateTtl() -> Int { + func calculateTtl(currentDate: Date = Date()) -> Int { guard let expiry = expiry else { return SessionRequestProtocolMethod.defaultTtl } let expiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) - let diff = expiryDate - Date().timeIntervalSince1970 + let diff = expiryDate - currentDate.timeIntervalSince1970 + + guard + diff.timeIntervalSince1970 < Constants.maxExpiry, + diff.timeIntervalSince1970 > Constants.minExpiry + else { return SessionRequestProtocolMethod.defaultTtl } return Int(diff.timeIntervalSince1970) } diff --git a/Tests/WalletConnectSignTests/SessionRequestTests.swift b/Tests/WalletConnectSignTests/SessionRequestTests.swift new file mode 100644 index 000000000..99970f4f0 --- /dev/null +++ b/Tests/WalletConnectSignTests/SessionRequestTests.swift @@ -0,0 +1,48 @@ +import XCTest +@testable import WalletConnectSign + +final class SessionRequestTests: XCTestCase { + + func testRequestTtlDefault() { + let request = Request.stub() + + XCTAssertEqual(request.calculateTtl(), SessionRequestProtocolMethod.defaultTtl) + } + + func testRequestTtlExtended() { + let currentDate = Date(timeIntervalSince1970: 0) + let expiry = currentDate.advanced(by: 500) + let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) + + XCTAssertEqual(request.calculateTtl(currentDate: currentDate), 500) + } + + func testRequestTtlNotExtendedMinValidation() { + let currentDate = Date(timeIntervalSince1970: 0) + let expiry = currentDate.advanced(by: 200) + let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) + + XCTAssertEqual(request.calculateTtl(currentDate: currentDate), SessionRequestProtocolMethod.defaultTtl) + } + + func testRequestTtlNotExtendedMaxValidation() { + let currentDate = Date(timeIntervalSince1970: 0) + let expiry = currentDate.advanced(by: 700000) + let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) + + XCTAssertEqual(request.calculateTtl(currentDate: currentDate), SessionRequestProtocolMethod.defaultTtl) + } +} + +private extension Request { + + static func stub(expiry: UInt64? = nil) -> Request { + return Request( + topic: "topic", + method: "method", + params: AnyCodable("params"), + chainId: Blockchain("eip155:1")!, + expiry: expiry + ) + } +} From 25b522b73789e1d28ca4b582ee9fc50822f131eb Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 20 Jan 2023 20:50:31 +0500 Subject: [PATCH 5/6] isExpired tests --- Sources/WalletConnectSign/Request.swift | 8 ++--- .../SessionRequestTests.swift | 35 +++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Sources/WalletConnectSign/Request.swift b/Sources/WalletConnectSign/Request.swift index 8999cba52..f1a9b38b5 100644 --- a/Sources/WalletConnectSign/Request.swift +++ b/Sources/WalletConnectSign/Request.swift @@ -25,17 +25,17 @@ public struct Request: Codable, Equatable { self.init(id: id, topic: topic, method: method, params: AnyCodable(params), chainId: chainId, expiry: expiry) } - func isExpired() -> Bool { + func isExpired(currentDate: Date = Date()) -> Bool { guard let expiry = expiry else { return false } let expiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) guard - Date().distance(to: expiryDate) < Constants.maxExpiry, - Date().distance(to: expiryDate) > Constants.minExpiry + currentDate.distance(to: expiryDate) < Constants.maxExpiry, + currentDate.distance(to: expiryDate) > Constants.minExpiry else { return true } - return expiryDate < Date() + return expiryDate < currentDate } func calculateTtl(currentDate: Date = Date()) -> Int { diff --git a/Tests/WalletConnectSignTests/SessionRequestTests.swift b/Tests/WalletConnectSignTests/SessionRequestTests.swift index 99970f4f0..e89ff347d 100644 --- a/Tests/WalletConnectSignTests/SessionRequestTests.swift +++ b/Tests/WalletConnectSignTests/SessionRequestTests.swift @@ -32,6 +32,41 @@ final class SessionRequestTests: XCTestCase { XCTAssertEqual(request.calculateTtl(currentDate: currentDate), SessionRequestProtocolMethod.defaultTtl) } + + func testIsExpiredDefault() { + let request = Request.stub() + + XCTAssertFalse(request.isExpired()) + } + + func testIsExpiredTrue() { + let currentDate = Date(timeIntervalSince1970: 500) + let expiry = Date(timeIntervalSince1970: 0) + let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) + XCTAssertTrue(request.isExpired(currentDate: currentDate)) + } + + func testIsExpiredTrueMinValidation() { + let currentDate = Date(timeIntervalSince1970: 500) + let expiry = Date(timeIntervalSince1970: 600) + let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) + XCTAssertTrue(request.isExpired(currentDate: currentDate)) + } + + func testIsExpiredTrueMaxValidation() { + let currentDate = Date(timeIntervalSince1970: 500) + let expiry = Date(timeIntervalSince1970: 700000) + let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) + XCTAssertTrue(request.isExpired(currentDate: currentDate)) + } + + func testIsExpiredFalse() { + let currentDate = Date(timeIntervalSince1970: 0) + let expiry = Date(timeIntervalSince1970: 500) + let request = Request.stub(expiry: UInt64(expiry.timeIntervalSince1970)) + + XCTAssertFalse(request.isExpired(currentDate: currentDate)) + } } private extension Request { From d788d1657b4ac35608891e148a5395f804ed0a41 Mon Sep 17 00:00:00 2001 From: Artur Guseinov Date: Fri, 20 Jan 2023 21:35:24 +0500 Subject: [PATCH 6/6] testErrorOnRequestExpiry test --- .../Sign/SignClientTests.swift | 4 +- Sources/WalletConnectSign/Request.swift | 4 +- .../NetworkingInteractorMock.swift | 2 + .../SessionEngineTests.swift | 54 +++++++++++++++++++ Tests/WalletConnectSignTests/Stub/Stubs.swift | 4 +- 5 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 Tests/WalletConnectSignTests/SessionEngineTests.swift diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index a429d0884..c5a7fcab1 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -184,7 +184,7 @@ final class SignClientTests: XCTestCase { } dapp.onSessionSettled = { [unowned self] settledSession in Task(priority: .high) { - let request = Request(id: RPCID(0), topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain) + let request = Request(id: RPCID(0), topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain, expiry: nil) try await dapp.client.request(params: request) } } @@ -230,7 +230,7 @@ final class SignClientTests: XCTestCase { } dapp.onSessionSettled = { [unowned self] settledSession in Task(priority: .high) { - let request = Request(id: RPCID(0), topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain) + let request = Request(id: RPCID(0), topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain, expiry: nil) try await dapp.client.request(params: request) } } diff --git a/Sources/WalletConnectSign/Request.swift b/Sources/WalletConnectSign/Request.swift index f1a9b38b5..1cae4e0cd 100644 --- a/Sources/WalletConnectSign/Request.swift +++ b/Sources/WalletConnectSign/Request.swift @@ -31,8 +31,8 @@ public struct Request: Codable, Equatable { let expiryDate = Date(timeIntervalSince1970: TimeInterval(expiry)) guard - currentDate.distance(to: expiryDate) < Constants.maxExpiry, - currentDate.distance(to: expiryDate) > Constants.minExpiry + abs(currentDate.distance(to: expiryDate)) < Constants.maxExpiry, + abs(currentDate.distance(to: expiryDate)) > Constants.minExpiry else { return true } return expiryDate < currentDate diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift index 323c52279..4e34798ec 100644 --- a/Tests/TestingUtils/NetworkingInteractorMock.swift +++ b/Tests/TestingUtils/NetworkingInteractorMock.swift @@ -23,6 +23,7 @@ public class NetworkingInteractorMock: NetworkInteracting { var didCallRequest: Bool { requestCallCount > 0 } var onSubscribeCalled: (() -> Void)? + var onRespondError: ((Int) -> Void)? public let socketConnectionStatusPublisherSubject = PassthroughSubject() public var socketConnectionStatusPublisher: AnyPublisher { @@ -121,6 +122,7 @@ public class NetworkingInteractorMock: NetworkInteracting { public func respondError(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws { lastErrorCode = reason.code didRespondError = true + onRespondError?(reason.code) } public func requestNetworkAck(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod) async throws { diff --git a/Tests/WalletConnectSignTests/SessionEngineTests.swift b/Tests/WalletConnectSignTests/SessionEngineTests.swift new file mode 100644 index 000000000..051329ffe --- /dev/null +++ b/Tests/WalletConnectSignTests/SessionEngineTests.swift @@ -0,0 +1,54 @@ +import XCTest +@testable import WalletConnectSign +@testable import WalletConnectUtils +@testable import TestingUtils + +final class SessionEngineTests: XCTestCase { + + var networkingInteractor: NetworkingInteractorMock! + var sessionStorage: WCSessionStorageMock! + var engine: SessionEngine! + + override func setUp() { + networkingInteractor = NetworkingInteractorMock() + sessionStorage = WCSessionStorageMock() + engine = SessionEngine( + networkingInteractor: networkingInteractor, + historyService: HistoryService( + history: RPCHistory( + keyValueStore: .init( + defaults: RuntimeKeyValueStorage(), + identifier: "" + ) + ) + ), + kms: KeyManagementServiceMock(), + sessionStore: sessionStorage, + logger: ConsoleLoggerMock() + ) + } + + func testErrorOnRequestExpiry() { + let expectation = expectation(description: "TestErrorOnRequestExpiry") + + sessionStorage.setSession(WCSession.stub( + topic: "topic", + namespaces: SessionNamespace.stubDictionary() + )) + + networkingInteractor.onRespondError = { code in + XCTAssertEqual(code, 8000) + expectation.fulfill() + } + + let request = RPCRequest.stubRequest( + method: "method", + chainId: Blockchain("eip155:1")!, + expiry: UInt64(Date().timeIntervalSince1970) + ) + + networkingInteractor.requestPublisherSubject.send(("topic", request)) + + wait(for: [expectation], timeout: 0.5) + } +} diff --git a/Tests/WalletConnectSignTests/Stub/Stubs.swift b/Tests/WalletConnectSignTests/Stub/Stubs.swift index 1f22303f7..cbd649f4c 100644 --- a/Tests/WalletConnectSignTests/Stub/Stubs.swift +++ b/Tests/WalletConnectSignTests/Stub/Stubs.swift @@ -68,9 +68,9 @@ extension RPCRequest { return RPCRequest(method: SessionSettleProtocolMethod().method, params: SessionType.SettleParams.stub()) } - static func stubRequest(method: String, chainId: Blockchain) -> RPCRequest { + static func stubRequest(method: String, chainId: Blockchain, expiry: UInt64? = nil) -> RPCRequest { let params = SessionType.RequestParams( - request: SessionType.RequestParams.Request(method: method, params: AnyCodable(EmptyCodable()), expiry: nil), + request: SessionType.RequestParams.Request(method: method, params: AnyCodable(EmptyCodable()), expiry: expiry), chainId: chainId) return RPCRequest(method: SessionRequestProtocolMethod().method, params: params) }