diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift index f311cdc5b..b428a77f5 100644 --- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift +++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift @@ -171,9 +171,10 @@ public class NetworkingInteractor: NetworkInteracting { } public func respond(topic: String, response: RPCResponse, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { - try rpcHistory.resolve(response) + try rpcHistory.validate(response) let message = try serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) try await relayClient.publish(topic: topic, payload: message, tag: protocolMethod.responseConfig.tag, prompt: protocolMethod.responseConfig.prompt, ttl: protocolMethod.responseConfig.ttl) + try rpcHistory.resolve(response) } public func respondSuccess(topic: String, requestId: RPCID, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws { @@ -216,8 +217,7 @@ public class NetworkingInteractor: NetworkInteracting { private func handleResponse(topic: String, response: RPCResponse, publishedAt: Date, derivedTopic: String?) { do { - try rpcHistory.resolve(response) - let record = rpcHistory.get(recordId: response.id!)! + let record = try rpcHistory.resolve(response) responsePublisherSubject.send((topic, record.request, response, publishedAt, derivedTopic)) } catch { logger.debug("Handle json rpc response error: \(error)") diff --git a/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift b/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift index 4fc00aebe..b310586d0 100644 --- a/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift +++ b/Sources/WalletConnectUtils/RPCHistory/RPCHistory.swift @@ -1,3 +1,5 @@ +import Foundation + public final class RPCHistory { public struct Record: Codable { @@ -9,7 +11,8 @@ public final class RPCHistory { public let topic: String let origin: Origin public let request: RPCRequest - public var response: RPCResponse? + public let response: RPCResponse? + public var timestamp: Date? } enum HistoryError: Error { @@ -24,36 +27,43 @@ public final class RPCHistory { init(keyValueStore: CodableStore) { self.storage = keyValueStore + + removeOutdated() } public func get(recordId: RPCID) -> Record? { try? storage.get(key: recordId.string) } - public func set(_ request: RPCRequest, forTopic topic: String, emmitedBy origin: Record.Origin) throws { + public func set(_ request: RPCRequest, forTopic topic: String, emmitedBy origin: Record.Origin, time: TimeProvider = DefaultTimeProvider()) throws { guard let id = request.id else { throw HistoryError.unidentifiedRequest } guard get(recordId: id) == nil else { throw HistoryError.requestDuplicateNotAllowed } - let record = Record(id: id, topic: topic, origin: origin, request: request) + let record = Record(id: id, topic: topic, origin: origin, request: request, response: nil, timestamp: time.currentDate) storage.set(record, forKey: "\(record.id)") } @discardableResult public func resolve(_ response: RPCResponse) throws -> Record { + let record = try validate(response) + storage.delete(forKey: "\(record.id)") + return record + } + + @discardableResult + public func validate(_ response: RPCResponse) throws -> Record { guard let id = response.id else { throw HistoryError.unidentifiedResponse } - guard var record = get(recordId: id) else { + guard let record = get(recordId: id) else { throw HistoryError.requestMatchingResponseNotFound } guard record.response == nil else { throw HistoryError.responseDuplicateNotAllowed } - record.response = response - storage.set(record, forKey: "\(record.id)") return record } @@ -95,3 +105,23 @@ public final class RPCHistory { storage.getAll().filter { $0.response == nil } } } + +extension RPCHistory { + + func removeOutdated() { + let records = storage.getAll() + + let thirtyDays: TimeInterval = 30*86400 + + for var record in records { + if let timestamp = record.timestamp { + if timestamp.distance(to: Date()) > thirtyDays { + storage.delete(forKey: record.id.string) + } + } else { + record.timestamp = Date() + storage.set(record, forKey: "\(record.id)") + } + } + } +} diff --git a/Sources/WalletConnectUtils/TimeProvider.swift b/Sources/WalletConnectUtils/TimeProvider.swift new file mode 100644 index 000000000..86732b6f0 --- /dev/null +++ b/Sources/WalletConnectUtils/TimeProvider.swift @@ -0,0 +1,12 @@ +import Foundation + +public protocol TimeProvider { + var currentDate: Date { get } +} + +public struct DefaultTimeProvider: TimeProvider { + public init() {} + public var currentDate: Date { + return Date() + } +} diff --git a/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift b/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift index 37b05ccdd..b4023eaff 100644 --- a/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift +++ b/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift @@ -35,10 +35,8 @@ final class RPCHistoryTests: XCTestCase { try sut.set(requestB, forTopic: String.randomTopic(), emmitedBy: .local) try sut.resolve(responseA) try sut.resolve(responseB) - let recordA = sut.get(recordId: requestA.id!) - let recordB = sut.get(recordId: requestB.id!) - XCTAssertEqual(recordA?.response, responseA) - XCTAssertEqual(recordB?.response, responseB) + XCTAssertNil(sut.get(recordId: requestA.id!)) + XCTAssertNil(sut.get(recordId: requestB.id!)) } func testDelete() throws { @@ -95,7 +93,7 @@ final class RPCHistoryTests: XCTestCase { } func testResolveDuplicateResponse() throws { - let expectedError = RPCHistory.HistoryError.responseDuplicateNotAllowed + let expectedError = RPCHistory.HistoryError.requestMatchingResponseNotFound let request = RPCRequest.stub() let responseA = RPCResponse(matchingRequest: request, result: true) @@ -107,4 +105,27 @@ final class RPCHistoryTests: XCTestCase { XCTAssertEqual(expectedError, error as? RPCHistory.HistoryError) } } + + func testRemoveOutdated() throws { + let request1 = RPCRequest.stub() + let request2 = RPCRequest.stub() + + let time1 = TestTimeProvider(currentDate: .distantPast) + let time2 = TestTimeProvider(currentDate: Date()) + + try sut.set(request1, forTopic: .randomTopic(), emmitedBy: .local, time: time1) + try sut.set(request2, forTopic: .randomTopic(), emmitedBy: .local, time: time2) + + XCTAssertEqual(sut.get(recordId: request1.id!)?.request, request1) + XCTAssertEqual(sut.get(recordId: request2.id!)?.request, request2) + + sut.removeOutdated() + + XCTAssertEqual(sut.get(recordId: request1.id!)?.request, nil) + XCTAssertEqual(sut.get(recordId: request2.id!)?.request, request2) + } + + struct TestTimeProvider: TimeProvider { + var currentDate: Date + } }