diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/EventsTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/EventsTests.xcscheme
new file mode 100644
index 000000000..7f7753025
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/EventsTests.xcscheme
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Example/DApp/SceneDelegate.swift b/Example/DApp/SceneDelegate.swift
index 2153d26f3..5d5c3cb62 100644
--- a/Example/DApp/SceneDelegate.swift
+++ b/Example/DApp/SceneDelegate.swift
@@ -117,6 +117,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
order: 1,
mobileLink: "walletapp://",
linkMode: "https://lab.web3modal.com/wallet"
+ ),
+ .init(
+ id: "rn-sample",
+ name: "RN Sample Wallet",
+ homepage: "https://walletconnect.com/",
+ imageUrl: "https://avatars.githubusercontent.com/u/37784886?s=200&v=4",
+ order: 1,
+ mobileLink: "rn-web3wallet://",
+ linkMode: "https://lab.web3modal.com/walletkit_rn"
)
]
)
diff --git a/Example/IntegrationTests/Pairing/PairingTests.swift b/Example/IntegrationTests/Pairing/PairingTests.swift
index 2ea13ae0e..80c9e5f0a 100644
--- a/Example/IntegrationTests/Pairing/PairingTests.swift
+++ b/Example/IntegrationTests/Pairing/PairingTests.swift
@@ -42,7 +42,8 @@ final class PairingTests: XCTestCase {
logger: logger,
keyValueStorage: keyValueStorage,
keychainStorage: keychain,
- networkingClient: networkingClient)
+ networkingClient: networkingClient,
+ eventsClient: MockEventsClient())
return pairingClient
diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift
index 56262453f..31f09a9e3 100644
--- a/Example/IntegrationTests/Sign/SignClientTests.swift
+++ b/Example/IntegrationTests/Sign/SignClientTests.swift
@@ -48,7 +48,8 @@ final class SignClientTests: XCTestCase {
logger: logger,
keyValueStorage: keyValueStorage,
keychainStorage: keychain,
- networkingClient: networkingClient
+ networkingClient: networkingClient,
+ eventsClient: MockEventsClient()
)
let metadata = AppMetadata(name: name, description: "", url: "", icons: [""], redirect: try! AppMetadata.Redirect(native: "", universal: linkModeUniversalLink, linkMode: supportLinkMode))
@@ -61,7 +62,8 @@ final class SignClientTests: XCTestCase {
networkingClient: networkingClient,
iatProvider: IATProviderMock(),
projectId: InputConfig.projectId,
- crypto: DefaultCryptoProvider()
+ crypto: DefaultCryptoProvider(),
+ eventsClient: MockEventsClient()
)
let clientId = try! networkingClient.getClientId()
diff --git a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift
index 8420a3a64..5826e1f1a 100644
--- a/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift
+++ b/Example/IntegrationTests/XPlatform/Web3Wallet/XPlatformW3WTests.swift
@@ -46,7 +46,8 @@ final class XPlatformW3WTests: XCTestCase {
logger: pairingLogger,
keyValueStorage: keyValueStorage,
keychainStorage: keychain,
- networkingClient: networkingClient)
+ networkingClient: networkingClient,
+ eventsClient: MockEventsClient())
let signClient = SignClientFactory.create(
metadata: AppMetadata(name: name, description: "", url: "", icons: [""], redirect: try! AppMetadata.Redirect(native: "", universal: nil)),
@@ -57,7 +58,8 @@ final class XPlatformW3WTests: XCTestCase {
networkingClient: networkingClient,
iatProvider: DefaultIATProvider(),
projectId: InputConfig.projectId,
- crypto: DefaultCryptoProvider()
+ crypto: DefaultCryptoProvider(),
+ eventsClient: MockEventsClient()
)
w3wClient = Web3WalletClientFactory.create(
diff --git a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift
index 2eda07ec7..970353746 100644
--- a/Example/WalletApp/ApplicationLayer/ConfigurationService.swift
+++ b/Example/WalletApp/ApplicationLayer/ConfigurationService.swift
@@ -33,6 +33,7 @@ final class ConfigurationService {
Notify.instance.setLogging(level: .debug)
Sign.instance.setLogging(level: .debug)
+ Events.instance.setLogging(level: .debug)
if let clientId = try? Networking.interactor.getClientId() {
LoggingService.instance.setUpUser(account: importAccount.account.absoluteString, clientId: clientId)
diff --git a/Package.swift b/Package.swift
index b255c6b99..7e5c3133c 100644
--- a/Package.swift
+++ b/Package.swift
@@ -51,7 +51,7 @@ let package = Package(
targets: [
.target(
name: "WalletConnectSign",
- dependencies: ["WalletConnectPairing", "WalletConnectVerify", "WalletConnectSigner"],
+ dependencies: ["WalletConnectPairing", "WalletConnectVerify", "WalletConnectSigner", "Events"],
path: "Sources/WalletConnectSign",
resources: [.process("Resources/PrivacyInfo.xcprivacy")]),
.target(
@@ -84,7 +84,7 @@ let package = Package(
path: "Sources/WalletConnectKMS"),
.target(
name: "WalletConnectPairing",
- dependencies: ["WalletConnectNetworking"],
+ dependencies: ["WalletConnectNetworking", "Events"],
resources: [.process("Resources/PrivacyInfo.xcprivacy")]),
.target(
name: "WalletConnectSigner",
@@ -126,6 +126,9 @@ let package = Package(
.target(
name: "Database",
dependencies: ["WalletConnectUtils"]),
+ .target(
+ name: "Events",
+ dependencies: ["WalletConnectUtils", "WalletConnectNetworking"]),
.target(
name: "WalletConnectModal",
dependencies: ["QRCode", "WalletConnectSign"],
@@ -169,7 +172,10 @@ let package = Package(
dependencies: ["Commons", "TestingUtils"]),
.testTarget(
name: "WalletConnectModalTests",
- dependencies: ["WalletConnectModal", "TestingUtils"])
+ dependencies: ["WalletConnectModal", "TestingUtils"]),
+ .testTarget(
+ name: "EventsTests",
+ dependencies: ["Events"]),
],
swiftLanguageVersions: [.v5]
)
diff --git a/Sources/Events/Event.swift b/Sources/Events/Event.swift
new file mode 100644
index 000000000..cc8c4d3c1
--- /dev/null
+++ b/Sources/Events/Event.swift
@@ -0,0 +1,19 @@
+import Foundation
+
+struct Event: Codable {
+ let eventId: String
+ let bundleId: String
+ let timestamp: Int64
+ let props: Props
+}
+
+struct Props: Codable {
+ let event: String
+ let type: String
+ let properties: Properties?
+}
+
+struct Properties: Codable {
+ let topic: String?
+ let trace: [String]?
+}
diff --git a/Sources/Events/EventStorage.swift b/Sources/Events/EventStorage.swift
new file mode 100644
index 000000000..691ef903e
--- /dev/null
+++ b/Sources/Events/EventStorage.swift
@@ -0,0 +1,57 @@
+
+import Foundation
+
+protocol EventStorage {
+ func saveErrorEvent(_ event: Event)
+ func fetchErrorEvents() -> [Event]
+ func clearErrorEvents()
+}
+
+class UserDefaultsEventStorage: EventStorage {
+ private let errorEventsKey = "com.walletconnect.sdk.errorEvents"
+ private let maxEvents = 30
+
+ func saveErrorEvent(_ event: Event) {
+ var existingEvents = fetchErrorEvents()
+ existingEvents.append(event)
+ // Ensure we keep only the last 30 events
+ if existingEvents.count > maxEvents {
+ existingEvents = Array(existingEvents.suffix(maxEvents))
+ }
+ if let encoded = try? JSONEncoder().encode(existingEvents) {
+ UserDefaults.standard.set(encoded, forKey: errorEventsKey)
+ }
+ }
+
+ func fetchErrorEvents() -> [Event] {
+ if let data = UserDefaults.standard.data(forKey: errorEventsKey),
+ let events = try? JSONDecoder().decode([Event].self, from: data) {
+ // Return only the last 30 events
+ return Array(events.suffix(maxEvents))
+ }
+ return []
+ }
+
+ func clearErrorEvents() {
+ UserDefaults.standard.removeObject(forKey: errorEventsKey)
+ }
+}
+
+#if DEBUG
+class MockEventStorage: EventStorage {
+ private(set) var savedEvents: [Event] = []
+
+ func saveErrorEvent(_ event: Event) {
+ savedEvents.append(event)
+ }
+
+ func fetchErrorEvents() -> [Event] {
+ return savedEvents
+ }
+
+ func clearErrorEvents() {
+ savedEvents.removeAll()
+ }
+}
+#endif
+
diff --git a/Sources/Events/Events.swift b/Sources/Events/Events.swift
new file mode 100644
index 000000000..5b9d3e57a
--- /dev/null
+++ b/Sources/Events/Events.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+public class Events {
+ /// Singleton instance of EventsClient
+ public static var instance: EventsClient = {
+ return EventsClientFactory.create(
+ projectId: Networking.projectId,
+ sdkVersion: EnvironmentInfo.sdkName
+ )
+ }()
+}
diff --git a/Sources/Events/EventsClient.swift b/Sources/Events/EventsClient.swift
new file mode 100644
index 000000000..f0bcb32c3
--- /dev/null
+++ b/Sources/Events/EventsClient.swift
@@ -0,0 +1,107 @@
+import Foundation
+
+public protocol EventsClientProtocol {
+ func startTrace(topic: String)
+ func saveEvent(_ event: TraceEvent)
+ func setTopic(_ topic: String)
+ func setTelemetryEnabled(_ enabled: Bool)
+}
+
+public class EventsClient: EventsClientProtocol {
+ private let eventsCollector: EventsCollector
+ private let eventsDispatcher: EventsDispatcher
+ private let logger: ConsoleLogging
+ private var stateStorage: TelemetryStateStorage
+
+ init(
+ eventsCollector: EventsCollector,
+ eventsDispatcher: EventsDispatcher,
+ logger: ConsoleLogging,
+ stateStorage: TelemetryStateStorage
+ ) {
+ self.eventsCollector = eventsCollector
+ self.eventsDispatcher = eventsDispatcher
+ self.logger = logger
+ self.stateStorage = stateStorage
+
+ if stateStorage.telemetryEnabled {
+ Task { await sendStoredEvents() }
+ } else {
+ self.eventsCollector.storage.clearErrorEvents()
+ }
+ }
+
+ public func setLogging(level: LoggingLevel) {
+ logger.setLogging(level: level)
+ }
+
+ // Public method to start trace
+ public func startTrace(topic: String) {
+ guard stateStorage.telemetryEnabled else { return }
+ logger.debug("Will start trace with topic: \(topic)")
+ eventsCollector.startTrace(topic: topic)
+ }
+
+ public func setTopic(_ topic: String) {
+ guard stateStorage.telemetryEnabled else { return }
+ eventsCollector.setTopic(topic)
+ }
+
+ // Public method to save event
+ public func saveEvent(_ event: TraceEvent) {
+ guard stateStorage.telemetryEnabled else { return }
+ logger.debug("Will store an event: \(event)")
+ eventsCollector.saveEvent(event)
+ }
+
+ // Public method to set telemetry enabled or disabled
+ public func setTelemetryEnabled(_ enabled: Bool) {
+ stateStorage.telemetryEnabled = enabled
+ if enabled {
+ Task { await sendStoredEvents() }
+ } else {
+ eventsCollector.storage.clearErrorEvents()
+ }
+ }
+
+ private func sendStoredEvents() async {
+ guard stateStorage.telemetryEnabled else { return }
+ let events = eventsCollector.storage.fetchErrorEvents()
+ guard !events.isEmpty else { return }
+
+ logger.debug("Will send events")
+ do {
+ let success: Bool = try await eventsDispatcher.executeWithRetry(events: events)
+ if success {
+ logger.debug("Events sent successfully")
+ self.eventsCollector.storage.clearErrorEvents()
+ }
+ } catch {
+ logger.debug("Failed to send events after multiple attempts: \(error)")
+ }
+ }
+}
+
+#if DEBUG
+public class MockEventsClient: EventsClientProtocol {
+ var startTraceCalled = false
+ var saveEventCalled = false
+ var telemetryEnabled = true
+
+ public init() {}
+
+ public func startTrace(topic: String) {
+ startTraceCalled = true
+ }
+
+ public func setTopic(_ topic: String) {}
+
+ public func saveEvent(_ event: TraceEvent) {
+ saveEventCalled = true
+ }
+
+ public func setTelemetryEnabled(_ enabled: Bool) {
+ telemetryEnabled = enabled
+ }
+}
+#endif
diff --git a/Sources/Events/EventsClientFactory.swift b/Sources/Events/EventsClientFactory.swift
new file mode 100644
index 000000000..43bff4c76
--- /dev/null
+++ b/Sources/Events/EventsClientFactory.swift
@@ -0,0 +1,25 @@
+import Foundation
+
+public class EventsClientFactory {
+ static func create(
+ projectId: String,
+ sdkVersion: String,
+ storage: EventStorage = UserDefaultsEventStorage()
+ ) -> EventsClient {
+ let networkingService = NetworkingService(
+ projectId: projectId,
+ sdkVersion: sdkVersion
+ )
+ let logger = ConsoleLogger(prefix: "🧚🏻♂️", loggingLevel: .off)
+ let retryPolicy = RetryPolicy(maxAttempts: 3, initialDelay: 5, multiplier: 2)
+ let eventsDispatcher = EventsDispatcher(networkingService: networkingService, retryPolicy: retryPolicy)
+ let eventsCollector = EventsCollector(storage: storage, logger: logger)
+ return EventsClient(
+ eventsCollector: eventsCollector,
+ eventsDispatcher: eventsDispatcher,
+ logger: logger,
+ stateStorage: UserDefaultsTelemetryStateStorage()
+ )
+ }
+}
+
diff --git a/Sources/Events/EventsCollector.swift b/Sources/Events/EventsCollector.swift
new file mode 100644
index 000000000..2ace98002
--- /dev/null
+++ b/Sources/Events/EventsCollector.swift
@@ -0,0 +1,71 @@
+import Foundation
+
+// Protocol for TraceEvent
+public protocol TraceEvent: CustomStringConvertible {
+ var description: String { get }
+}
+
+// Protocol for ErrorEvent
+protocol ErrorEvent: TraceEvent {}
+
+
+class EventsCollector {
+ var trace: [String] = []
+ var topic: String?
+ let storage: EventStorage
+ private let logger: ConsoleLogging
+
+ init(
+ storage: EventStorage,
+ logger: ConsoleLogging
+ ) {
+ self.storage = storage
+ self.logger = logger
+ }
+
+ // Function to start trace with topic
+ func startTrace(topic: String) {
+ self.topic = topic
+ self.trace = []
+ }
+
+ func setTopic(_ topic: String) {
+ self.topic = topic
+ }
+
+ // Function to save event
+ func saveEvent(_ event: TraceEvent) {
+ trace.append(event.description)
+ if let errorEvent = event as? ErrorEvent {
+ saveErrorEvent(errorEvent)
+ endTrace()
+ }
+ }
+
+ // Function to end trace
+ private func endTrace() {
+ self.topic = nil
+ self.trace = []
+ }
+
+ // Private function to save error event
+ private func saveErrorEvent(_ errorEvent: ErrorEvent) {
+ let bundleId = Bundle.main.bundleIdentifier ?? "Unknown"
+ let event = Event(
+ eventId: UUID().uuidString,
+ bundleId: bundleId,
+ timestamp: Int64(Date().timeIntervalSince1970 * 1000),
+ props: Props(
+ event: "ERROR",
+ type: errorEvent.description,
+ properties: Properties(
+ topic: topic,
+ trace: trace
+ )
+ )
+ )
+ storage.saveErrorEvent(event)
+ logger.debug("Error event saved: \(event)")
+ }
+}
+
diff --git a/Sources/Events/EventsDispatcher.swift b/Sources/Events/EventsDispatcher.swift
new file mode 100644
index 000000000..0c347958b
--- /dev/null
+++ b/Sources/Events/EventsDispatcher.swift
@@ -0,0 +1,42 @@
+
+import Foundation
+
+struct RetryPolicy {
+ let maxAttempts: Int
+ let initialDelay: TimeInterval
+ let multiplier: Double
+ var delayOverride: TimeInterval? = nil
+}
+
+class EventsDispatcher {
+ private let networkingService: NetworkingServiceProtocol
+ private let retryPolicy: RetryPolicy
+
+ init(networkingService: NetworkingServiceProtocol, retryPolicy: RetryPolicy) {
+ self.networkingService = networkingService
+ self.retryPolicy = retryPolicy
+ }
+
+ func executeWithRetry(events: [Event]) async throws -> Bool {
+ var attempts = 0
+ var delay = retryPolicy.initialDelay
+
+ while attempts < retryPolicy.maxAttempts {
+ if attempts > 0 || retryPolicy.initialDelay > 0 {
+ let actualDelay = retryPolicy.delayOverride ?? delay
+ try await Task.sleep(nanoseconds: UInt64(actualDelay * Double(NSEC_PER_SEC)))
+ delay *= retryPolicy.multiplier
+ }
+
+ do {
+ return try await networkingService.sendEvents(events)
+ } catch {
+ attempts += 1
+ if attempts >= retryPolicy.maxAttempts {
+ throw error
+ }
+ }
+ }
+ throw NSError(domain: "EventsDispatcherError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Max retry attempts reached"])
+ }
+}
diff --git a/Sources/Events/EventsImports.swift b/Sources/Events/EventsImports.swift
new file mode 100644
index 000000000..5e63a68cb
--- /dev/null
+++ b/Sources/Events/EventsImports.swift
@@ -0,0 +1,5 @@
+#if !CocoaPods
+@_exported import WalletConnectUtils
+@_exported import WalletConnectNetworking
+@_exported import WalletConnectRelay
+#endif
diff --git a/Sources/Events/ExecutionTraces/PairingExecutionTraceEvents.swift b/Sources/Events/ExecutionTraces/PairingExecutionTraceEvents.swift
new file mode 100644
index 000000000..bfb809dd3
--- /dev/null
+++ b/Sources/Events/ExecutionTraces/PairingExecutionTraceEvents.swift
@@ -0,0 +1,31 @@
+
+import Foundation
+
+public enum PairingExecutionTraceEvents: String, TraceEvent {
+ case pairingUriValidationSuccess = "pairing_uri_validation_success"
+ case pairingStarted = "pairing_started"
+ case noWssConnection = "no_wss_connection"
+ case storeNewPairing = "store_new_pairing"
+ case subscribingPairingTopic = "subscribing_pairing_topic"
+ case subscribePairingTopicSuccess = "subscribe_pairing_topic_success"
+ case pairingHasPendingRequest = "pairing_has_pending_request"
+ case emitSessionProposal = "emit_session_proposal"
+
+ public var description: String {
+ return self.rawValue
+ }
+}
+
+// Enum for TraceErrorEvents
+public enum PairingTraceErrorEvents: String, ErrorEvent {
+ case noInternetConnection = "no_internet_connection"
+ case malformedPairingUri = "malformed_pairing_uri"
+ case activePairingAlreadyExists = "active_pairing_already_exists"
+ case subscribePairingTopicFailure = "subscribe_pairing_topic_failure"
+ case pairingExpired = "pairing_expired"
+ case proposalExpired = "proposal_expired"
+
+ public var description: String {
+ return self.rawValue
+ }
+}
diff --git a/Sources/Events/ExecutionTraces/SessionApproveExecutionTraceEvents.swift b/Sources/Events/ExecutionTraces/SessionApproveExecutionTraceEvents.swift
new file mode 100644
index 000000000..95cc48ca6
--- /dev/null
+++ b/Sources/Events/ExecutionTraces/SessionApproveExecutionTraceEvents.swift
@@ -0,0 +1,29 @@
+
+import Foundation
+
+public enum SessionApproveExecutionTraceEvents: String, TraceEvent {
+ case approvingSessionProposal = "approving_session_proposal"
+ case sessionNamespacesValidationStarted = "session_namespaces_validation_started"
+ case sessionNamespacesValidationSuccess = "session_namespaces_validation_success"
+ case responseApproveSent = "response_approve_sent"
+ case settleRequestSent = "settle_request_sent"
+ case sessionSettleSuccess = "session_settle_success"
+
+ public var description: String {
+ return self.rawValue
+ }
+}
+
+public enum ApproveSessionTraceErrorEvents: String, ErrorEvent {
+ case sessionNamespacesValidationFailure = "session_namespaces_validation_failure"
+ case proposalNotFound = "proposal_not_found"
+ case proposalExpired = "proposal_expired"
+ case networkNotConnected = "network_not_connected"
+ case agreementMissingOrInvalid = "agreement_missing_or_invalid"
+ case relayNotFound = "relay_not_found"
+ case sessionSettleFailure = "session_settle_failure"
+
+ public var description: String {
+ return self.rawValue
+ }
+}
diff --git a/Sources/Events/ExecutionTraces/SessionAuthenticateTraceEvents.swift b/Sources/Events/ExecutionTraces/SessionAuthenticateTraceEvents.swift
new file mode 100644
index 000000000..88b2f0696
--- /dev/null
+++ b/Sources/Events/ExecutionTraces/SessionAuthenticateTraceEvents.swift
@@ -0,0 +1,33 @@
+
+import Foundation
+
+public enum SessionAuthenticateTraceEvents: String, TraceEvent {
+ case signatureVerificationStarted = "signature_verification_started"
+ case signatureVerificationSuccess = "signature_verification_success"
+ case requestParamsRetrieved = "request_params_retrieved"
+ case agreementKeysGenerated = "agreement_keys_generated"
+ case agreementSecretSet = "agreement_secret_set"
+ case sessionKeysGenerated = "session_keys_generated"
+ case sessionSecretSet = "session_secret_set"
+ case responseParamsCreated = "response_params_created"
+ case responseSent = "response_sent"
+
+ public var description: String {
+ return self.rawValue
+ }
+}
+
+public enum SessionAuthenticateErrorEvents: String, ErrorEvent {
+ case signatureVerificationFailed = "signature_verification_failed"
+ case requestParamsRetrievalFailed = "request_params_retrieval_failed"
+ case agreementKeysGenerationFailed = "agreement_keys_generation_failed"
+ case agreementSecretSetFailed = "agreement_secret_set_failed"
+ case sessionKeysGenerationFailed = "session_keys_generation_failed"
+ case sessionSecretSetFailed = "session_secret_set_failed"
+ case sessionCreationFailed = "session_creation_failed"
+ case responseSendFailed = "response_send_failed"
+
+ public var description: String {
+ return self.rawValue
+ }
+}
diff --git a/Sources/Events/NetworkingService.swift b/Sources/Events/NetworkingService.swift
new file mode 100644
index 000000000..a8a03b620
--- /dev/null
+++ b/Sources/Events/NetworkingService.swift
@@ -0,0 +1,62 @@
+import Foundation
+
+protocol NetworkingServiceProtocol {
+ func sendEvents(_ events: [Event]) async throws -> Bool
+}
+
+class NetworkingService: NetworkingServiceProtocol {
+ private let session: URLSession
+ private let projectId: String
+ private let sdkVersion: String
+ private let apiURL = URL(string: "https://pulse.walletconnect.com/batch")!
+
+ init(session: URLSession = .shared, projectId: String, sdkVersion: String) {
+ self.session = session
+ self.projectId = projectId
+ self.sdkVersion = sdkVersion
+ }
+
+ func sendEvents(_ events: [Event]) async throws -> Bool {
+ var request = URLRequest(url: apiURL)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue(projectId, forHTTPHeaderField: "x-project-id")
+ request.setValue("events_sdk", forHTTPHeaderField: "x-sdk-type")
+ request.setValue(sdkVersion, forHTTPHeaderField: "x-sdk-version")
+
+ request.httpBody = try JSONEncoder().encode(events)
+
+ return try await withCheckedThrowingContinuation { continuation in
+ let task = session.dataTask(with: request) { data, response, error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ return
+ }
+
+ if let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) {
+ continuation.resume(returning: true)
+ } else {
+ continuation.resume(returning: false)
+ }
+ }
+
+ task.priority = URLSessionTask.lowPriority
+ task.resume()
+ }
+ }
+}
+
+#if DEBUG
+class MockNetworkingService: NetworkingServiceProtocol {
+ var shouldFail = false
+ var attemptCount = 0
+
+ func sendEvents(_ events: [Event]) async throws -> Bool {
+ attemptCount += 1
+ if shouldFail {
+ throw NSError(domain: "MockError", code: -1, userInfo: nil)
+ }
+ return true
+ }
+}
+#endif
diff --git a/Sources/Events/UserDefaultsStateStorage.swift b/Sources/Events/UserDefaultsStateStorage.swift
new file mode 100644
index 000000000..fa00eee21
--- /dev/null
+++ b/Sources/Events/UserDefaultsStateStorage.swift
@@ -0,0 +1,54 @@
+import Foundation
+
+protocol TelemetryStateStorage {
+ var telemetryEnabled: Bool { get set }
+}
+
+class UserDefaultsTelemetryStateStorage: TelemetryStateStorage {
+ private let telemetryEnabledKey = "com.walletconnect.sdk.telemetryEnabled"
+
+ var telemetryEnabled: Bool {
+ get {
+ if UserDefaults.standard.object(forKey: telemetryEnabledKey) == nil {
+ return true
+ }
+ return UserDefaults.standard.bool(forKey: telemetryEnabledKey)
+ }
+ set {
+ UserDefaults.standard.set(newValue, forKey: telemetryEnabledKey)
+ }
+ }
+
+ init() {
+ if UserDefaults.standard.object(forKey: telemetryEnabledKey) == nil {
+ // Set default value if not already set
+ UserDefaults.standard.set(true, forKey: telemetryEnabledKey)
+ }
+ }
+}
+
+#if DEBUG
+class MockUserDefaultsTelemetryStateStorage: TelemetryStateStorage {
+ private var mockStorage: [String: Any] = [:]
+ private let telemetryEnabledKey = "com.walletconnect.sdk.telemetryEnabled"
+
+ var telemetryEnabled: Bool {
+ get {
+ if mockStorage[telemetryEnabledKey] == nil {
+ return true
+ }
+ return mockStorage[telemetryEnabledKey] as? Bool ?? true
+ }
+ set {
+ mockStorage[telemetryEnabledKey] = newValue
+ }
+ }
+
+ init() {
+ // Initialize with a default value if not already set
+ if mockStorage[telemetryEnabledKey] == nil {
+ mockStorage[telemetryEnabledKey] = true
+ }
+ }
+}
+#endif
diff --git a/Sources/WalletConnectNetworking/NetworkInteracting.swift b/Sources/WalletConnectNetworking/NetworkInteracting.swift
index a79814399..e916c3f62 100644
--- a/Sources/WalletConnectNetworking/NetworkInteracting.swift
+++ b/Sources/WalletConnectNetworking/NetworkInteracting.swift
@@ -2,6 +2,7 @@ import Foundation
import Combine
public protocol NetworkInteracting {
+ var isSocketConnected: Bool { get }
var socketConnectionStatusPublisher: AnyPublisher { get }
var networkConnectionStatusPublisher: AnyPublisher { get }
var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest, decryptedPayload: Data, publishedAt: Date, derivedTopic: String?), Never> { get }
diff --git a/Sources/WalletConnectNetworking/NetworkingInteractor.swift b/Sources/WalletConnectNetworking/NetworkingInteractor.swift
index 847606edd..e7c8eb90d 100644
--- a/Sources/WalletConnectNetworking/NetworkingInteractor.swift
+++ b/Sources/WalletConnectNetworking/NetworkingInteractor.swift
@@ -27,6 +27,10 @@ public class NetworkingInteractor: NetworkInteracting {
.eraseToAnyPublisher()
}
+ public var isSocketConnected: Bool {
+ return relayClient.isSocketConnected
+ }
+
public var networkConnectionStatusPublisher: AnyPublisher
public var socketConnectionStatusPublisher: AnyPublisher
diff --git a/Sources/WalletConnectPairing/Pair.swift b/Sources/WalletConnectPairing/Pair.swift
index 08eafec7e..8924420cd 100644
--- a/Sources/WalletConnectPairing/Pair.swift
+++ b/Sources/WalletConnectPairing/Pair.swift
@@ -37,6 +37,10 @@ private extension Pair {
guard let config = Pair.config else {
fatalError("Error - you must call Pair.configure(_:) before accessing the shared instance.")
}
- return PairingClientFactory.create(networkingClient: Networking.interactor, groupIdentifier: Networking.groupIdentifier)
+ return PairingClientFactory.create(
+ networkingClient: Networking.interactor,
+ eventsClient: Events.instance,
+ groupIdentifier: Networking.groupIdentifier
+ )
}()
}
diff --git a/Sources/WalletConnectPairing/PairingClientFactory.swift b/Sources/WalletConnectPairing/PairingClientFactory.swift
index 22840bed3..26293c3ad 100644
--- a/Sources/WalletConnectPairing/PairingClientFactory.swift
+++ b/Sources/WalletConnectPairing/PairingClientFactory.swift
@@ -4,6 +4,7 @@ public struct PairingClientFactory {
public static func create(
networkingClient: NetworkingInteractor,
+ eventsClient: EventsClient,
groupIdentifier: String
) -> PairingClient {
let logger = ConsoleLogger(loggingLevel: .off)
@@ -13,20 +14,21 @@ public struct PairingClientFactory {
}
let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk", accessGroup: groupIdentifier)
- return PairingClientFactory.create(logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, networkingClient: networkingClient)
+ return PairingClientFactory.create(logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, networkingClient: networkingClient, eventsClient: eventsClient)
}
public static func create(
logger: ConsoleLogging,
keyValueStorage: KeyValueStorage,
keychainStorage: KeychainStorageProtocol,
- networkingClient: NetworkingInteractor
+ networkingClient: NetworkingInteractor,
+ eventsClient: EventsClientProtocol
) -> PairingClient {
let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: PairStorageIdentifiers.pairings.rawValue)))
let kms = KeyManagementService(keychain: keychainStorage)
let history = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage)
let appPairService = AppPairService(networkingInteractor: networkingClient, kms: kms, pairingStorage: pairingStore)
- let walletPairService = WalletPairService(networkingInteractor: networkingClient, kms: kms, pairingStorage: pairingStore, history: history, logger: logger)
+ let walletPairService = WalletPairService(networkingInteractor: networkingClient, kms: kms, pairingStorage: pairingStore, history: history, logger: logger, eventsClient: eventsClient)
let pairingRequestsSubscriber = PairingRequestsSubscriber(networkingInteractor: networkingClient, pairingStorage: pairingStore, logger: logger)
let pairingsProvider = PairingsProvider(pairingStorage: pairingStore)
let cleanupService = PairingCleanupService(pairingStore: pairingStore, kms: kms)
diff --git a/Sources/WalletConnectPairing/PairingImports.swift b/Sources/WalletConnectPairing/PairingImports.swift
index 23c1738ef..98b75f8a5 100644
--- a/Sources/WalletConnectPairing/PairingImports.swift
+++ b/Sources/WalletConnectPairing/PairingImports.swift
@@ -1,3 +1,4 @@
#if !CocoaPods
@_exported import WalletConnectNetworking
+@_exported import Events
#endif
diff --git a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift
index 2592d010d..c5607aa59 100644
--- a/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift
+++ b/Sources/WalletConnectPairing/Services/Wallet/WalletPairService.swift
@@ -8,6 +8,7 @@ actor WalletPairService {
let networkingInteractor: NetworkInteracting
let kms: KeyManagementServiceProtocol
+ private let eventsClient: EventsClientProtocol
private let pairingStorage: WCPairingStorage
private let history: RPCHistory
private let logger: ConsoleLogging
@@ -17,34 +18,50 @@ actor WalletPairService {
kms: KeyManagementServiceProtocol,
pairingStorage: WCPairingStorage,
history: RPCHistory,
- logger: ConsoleLogging
+ logger: ConsoleLogging,
+ eventsClient: EventsClientProtocol
) {
self.networkingInteractor = networkingInteractor
self.kms = kms
self.pairingStorage = pairingStorage
self.history = history
self.logger = logger
+ self.eventsClient = eventsClient
}
func pair(_ uri: WalletConnectURI) async throws {
+ eventsClient.startTrace(topic: uri.topic)
+ eventsClient.saveEvent(PairingExecutionTraceEvents.pairingStarted)
logger.debug("Pairing with uri: \(uri)")
guard try !pairingHasPendingRequest(for: uri.topic) else {
+ eventsClient.saveEvent(PairingExecutionTraceEvents.pairingHasPendingRequest)
logger.debug("Pairing with topic (\(uri.topic)) has pending request")
return
}
-
+ if !networkingInteractor.isSocketConnected {
+ eventsClient.saveEvent(PairingExecutionTraceEvents.noWssConnection)
+ }
+
let pairing = WCPairing(uri: uri)
let symKey = try SymmetricKey(hex: uri.symKey)
try kms.setSymmetricKey(symKey, for: pairing.topic)
pairingStorage.setPairing(pairing)
-
+ eventsClient.saveEvent(PairingExecutionTraceEvents.storeNewPairing)
+
let networkConnectionStatus = await resolveNetworkConnectionStatus()
guard networkConnectionStatus == .connected else {
logger.debug("Pairing failed - Network is not connected")
+ eventsClient.saveEvent(PairingTraceErrorEvents.noInternetConnection)
throw Errors.networkNotConnected
}
-
- try await networkingInteractor.subscribe(topic: pairing.topic)
+ eventsClient.saveEvent(PairingExecutionTraceEvents.subscribingPairingTopic)
+ do {
+ try await networkingInteractor.subscribe(topic: pairing.topic)
+ } catch {
+ logger.debug("Failed to subscribe to topic: \(pairing.topic)")
+ eventsClient.saveEvent(PairingTraceErrorEvents.subscribePairingTopicFailure)
+ throw error
+ }
}
}
@@ -56,6 +73,7 @@ extension WalletPairService {
}
if pairing.active {
+ eventsClient.saveEvent(PairingTraceErrorEvents.activePairingAlreadyExists)
throw Errors.pairingAlreadyExist(topic: topic)
}
@@ -67,6 +85,7 @@ extension WalletPairService {
guard !pendingRequests.isEmpty else { return false }
pendingRequests.forEach { request in
+ eventsClient.saveEvent(PairingExecutionTraceEvents.emitSessionProposal)
networkingInteractor.handleHistoryRequest(topic: topic, request: request)
}
return true
diff --git a/Sources/WalletConnectRelay/Dispatching.swift b/Sources/WalletConnectRelay/Dispatching.swift
index 81ede05d7..3af72ce97 100644
--- a/Sources/WalletConnectRelay/Dispatching.swift
+++ b/Sources/WalletConnectRelay/Dispatching.swift
@@ -3,6 +3,7 @@ import Combine
protocol Dispatching {
var onMessage: ((String) -> Void)? { get set }
+ var isSocketConnected: Bool { get }
var networkConnectionStatusPublisher: AnyPublisher { get }
var socketConnectionStatusPublisher: AnyPublisher { get }
func send(_ string: String, completion: @escaping (Error?) -> Void)
@@ -32,6 +33,10 @@ final class Dispatcher: NSObject, Dispatching {
networkMonitor.networkConnectionStatusPublisher
}
+ var isSocketConnected: Bool {
+ return networkMonitor.isConnected
+ }
+
private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.dispatcher", qos: .utility, attributes: .concurrent)
init(
diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift
index efa501499..93b33f1c7 100644
--- a/Sources/WalletConnectRelay/RelayClient.swift
+++ b/Sources/WalletConnectRelay/RelayClient.swift
@@ -22,6 +22,10 @@ public final class RelayClient {
var subscriptions: [String: String] = [:]
+ public var isSocketConnected: Bool {
+ return dispatcher.isSocketConnected
+ }
+
public var messagePublisher: AnyPublisher<(topic: String, message: String, publishedAt: Date), Never> {
messagePublisherSubject.eraseToAnyPublisher()
}
diff --git a/Sources/WalletConnectSign/Auth/Link/LinkAuthRequester.swift b/Sources/WalletConnectSign/Auth/Link/LinkAuthRequester.swift
index c18b9fb78..228baef25 100644
--- a/Sources/WalletConnectSign/Auth/Link/LinkAuthRequester.swift
+++ b/Sources/WalletConnectSign/Auth/Link/LinkAuthRequester.swift
@@ -31,7 +31,6 @@ actor LinkAuthRequester {
}
func request(params: AuthRequestParams, walletUniversalLink: String) async throws -> String {
-
guard try linkModeLinksStore.get(key: walletUniversalLink) != nil else { throw Errors.walletLinkSupportNotProven }
var params = params
diff --git a/Sources/WalletConnectSign/Auth/Services/Wallet/SessionAuthenticateResponder.swift b/Sources/WalletConnectSign/Auth/Services/Wallet/SessionAuthenticateResponder.swift
index a4b229830..dea7e0e9e 100644
--- a/Sources/WalletConnectSign/Auth/Services/Wallet/SessionAuthenticateResponder.swift
+++ b/Sources/WalletConnectSign/Auth/Services/Wallet/SessionAuthenticateResponder.swift
@@ -11,6 +11,7 @@ actor SessionAuthenticateResponder {
private let pairingRegisterer: PairingRegisterer
private let metadata: AppMetadata
private let util: ApproveSessionAuthenticateUtil
+ private let eventsClient: EventsClientProtocol
init(
networkingInteractor: NetworkInteracting,
@@ -20,7 +21,8 @@ actor SessionAuthenticateResponder {
walletErrorResponder: WalletErrorResponder,
pairingRegisterer: PairingRegisterer,
metadata: AppMetadata,
- approveSessionAuthenticateUtil: ApproveSessionAuthenticateUtil
+ approveSessionAuthenticateUtil: ApproveSessionAuthenticateUtil,
+ eventsClient: EventsClientProtocol
) {
self.networkingInteractor = networkingInteractor
self.logger = logger
@@ -30,60 +32,119 @@ actor SessionAuthenticateResponder {
self.pairingRegisterer = pairingRegisterer
self.metadata = metadata
self.util = approveSessionAuthenticateUtil
+ self.eventsClient = eventsClient
}
func respond(requestId: RPCID, auths: [Cacao]) async throws -> Session? {
- try await util.recoverAndVerifySignature(cacaos: auths)
- let (sessionAuthenticateRequestParams, pairingTopic) = try util.getsessionAuthenticateRequestParams(requestId: requestId)
- let (responseTopic, responseKeys) = try util.generateAgreementKeys(requestParams: sessionAuthenticateRequestParams)
-
-
- try kms.setAgreementSecret(responseKeys, topic: responseTopic)
+ eventsClient.saveEvent(SessionAuthenticateTraceEvents.signatureVerificationStarted)
+ do {
+ try await util.recoverAndVerifySignature(cacaos: auths)
+ eventsClient.saveEvent(SessionAuthenticateTraceEvents.signatureVerificationSuccess)
+ } catch {
+ eventsClient.saveEvent(SessionAuthenticateErrorEvents.signatureVerificationFailed)
+ throw error
+ }
+
+ let sessionAuthenticateRequestParams: SessionAuthenticateRequestParams
+ let pairingTopic: String
+
+ do {
+ (sessionAuthenticateRequestParams, pairingTopic) = try util.getsessionAuthenticateRequestParams(requestId: requestId)
+ eventsClient.saveEvent(SessionAuthenticateTraceEvents.requestParamsRetrieved)
+ } catch {
+ eventsClient.saveEvent(SessionAuthenticateErrorEvents.requestParamsRetrievalFailed)
+ throw error
+ }
+
+ let responseTopic: String
+ let responseKeys: AgreementKeys
+
+ do {
+ (responseTopic, responseKeys) = try util.generateAgreementKeys(requestParams: sessionAuthenticateRequestParams)
+ eventsClient.saveEvent(SessionAuthenticateTraceEvents.agreementKeysGenerated)
+ } catch {
+ eventsClient.saveEvent(SessionAuthenticateErrorEvents.agreementKeysGenerationFailed)
+ throw error
+ }
+
+ do {
+ try kms.setAgreementSecret(responseKeys, topic: responseTopic)
+ eventsClient.saveEvent(SessionAuthenticateTraceEvents.agreementSecretSet)
+ } catch {
+ eventsClient.saveEvent(SessionAuthenticateErrorEvents.agreementSecretSetFailed)
+ throw error
+ }
let peerParticipant = sessionAuthenticateRequestParams.requester
- let sessionSelfPubKey = try kms.createX25519KeyPair()
- let sessionSelfPubKeyHex = sessionSelfPubKey.hexRepresentation
- let sessionKeys = try kms.performKeyAgreement(selfPublicKey: sessionSelfPubKey, peerPublicKey: peerParticipant.publicKey)
+ let sessionSelfPubKey: AgreementPublicKey
+ let sessionSelfPubKeyHex: String
+ let sessionKeys: AgreementKeys
+
+ do {
+ sessionSelfPubKey = try kms.createX25519KeyPair()
+ sessionSelfPubKeyHex = sessionSelfPubKey.hexRepresentation
+ sessionKeys = try kms.performKeyAgreement(selfPublicKey: sessionSelfPubKey, peerPublicKey: peerParticipant.publicKey)
+ eventsClient.saveEvent(SessionAuthenticateTraceEvents.sessionKeysGenerated)
+ } catch {
+ eventsClient.saveEvent(SessionAuthenticateErrorEvents.sessionKeysGenerationFailed)
+ throw error
+ }
let sessionTopic = sessionKeys.derivedTopic()
- try kms.setAgreementSecret(sessionKeys, topic: sessionTopic)
+ do {
+ try kms.setAgreementSecret(sessionKeys, topic: sessionTopic)
+ eventsClient.saveEvent(SessionAuthenticateTraceEvents.sessionSecretSet)
+ } catch {
+ eventsClient.saveEvent(SessionAuthenticateErrorEvents.sessionSecretSetFailed)
+ throw error
+ }
let selfParticipant = Participant(publicKey: sessionSelfPubKeyHex, metadata: metadata)
let responseParams = SessionAuthenticateResponseParams(responder: selfParticipant, cacaos: auths)
+ eventsClient.saveEvent(SessionAuthenticateTraceEvents.responseParamsCreated)
let response = RPCResponse(id: requestId, result: responseParams)
-
- try await networkingInteractor.respond(
- topic: responseTopic,
- response: response,
- protocolMethod: SessionAuthenticatedProtocolMethod.responseApprove(),
- envelopeType: .type1(pubKey: responseKeys.publicKey.rawRepresentation)
- )
-
-
- let session = try util.createSession(
- response: responseParams,
- pairingTopic: pairingTopic,
- request: sessionAuthenticateRequestParams,
- sessionTopic: sessionTopic,
- transportType: .relay
- )
-
- pairingRegisterer.activate(
- pairingTopic: pairingTopic,
- peerMetadata: sessionAuthenticateRequestParams.requester.metadata
- )
- verifyContextStore.delete(forKey: requestId.string)
-
- return session
+ do {
+ try await networkingInteractor.respond(
+ topic: responseTopic,
+ response: response,
+ protocolMethod: SessionAuthenticatedProtocolMethod.responseApprove(),
+ envelopeType: .type1(pubKey: responseKeys.publicKey.rawRepresentation)
+ )
+ eventsClient.saveEvent(SessionAuthenticateTraceEvents.responseSent)
+ } catch {
+ eventsClient.saveEvent(SessionAuthenticateErrorEvents.responseSendFailed)
+ throw error
+ }
+
+ do {
+ let session = try util.createSession(
+ response: responseParams,
+ pairingTopic: pairingTopic,
+ request: sessionAuthenticateRequestParams,
+ sessionTopic: sessionTopic,
+ transportType: .relay
+ )
+ pairingRegisterer.activate(
+ pairingTopic: pairingTopic,
+ peerMetadata: sessionAuthenticateRequestParams.requester.metadata
+ )
+ verifyContextStore.delete(forKey: requestId.string)
+ return session
+ } catch {
+ eventsClient.saveEvent(SessionAuthenticateErrorEvents.sessionCreationFailed)
+ throw error
+ }
}
func respondError(requestId: RPCID) async throws {
- try await walletErrorResponder.respondError(AuthError.userRejeted, requestId: requestId)
+ do {
+ try await walletErrorResponder.respondError(AuthError.userRejeted, requestId: requestId)
+ } catch {
+ throw error
+ }
verifyContextStore.delete(forKey: requestId.string)
}
}
-
-
diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift
index eabb88d6b..cd127d81a 100644
--- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift
+++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift
@@ -30,6 +30,7 @@ final class ApproveEngine {
private let logger: ConsoleLogging
private let rpcHistory: RPCHistory
private let authRequestSubscribersTracking: AuthRequestSubscribersTracking
+ private let eventsClient: EventsClientProtocol
private var publishers = Set()
@@ -46,7 +47,8 @@ final class ApproveEngine {
sessionStore: WCSessionStorage,
verifyClient: VerifyClientProtocol,
rpcHistory: RPCHistory,
- authRequestSubscribersTracking: AuthRequestSubscribersTracking
+ authRequestSubscribersTracking: AuthRequestSubscribersTracking,
+ eventsClient: EventsClientProtocol
) {
self.networkingInteractor = networkingInteractor
self.proposalPayloadsStore = proposalPayloadsStore
@@ -61,50 +63,72 @@ final class ApproveEngine {
self.verifyClient = verifyClient
self.rpcHistory = rpcHistory
self.authRequestSubscribersTracking = authRequestSubscribersTracking
+ self.eventsClient = eventsClient
setupRequestSubscriptions()
setupResponseSubscriptions()
setupResponseErrorSubscriptions()
}
+
func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace], sessionProperties: [String: String]? = nil) async throws -> Session {
+ eventsClient.startTrace(topic: "")
logger.debug("Approving session proposal")
+ eventsClient.saveEvent(SessionApproveExecutionTraceEvents.approvingSessionProposal)
- guard !sessionNamespaces.isEmpty else { throw Errors.emtySessionNamespacesForbidden }
+ guard !sessionNamespaces.isEmpty else {
+ eventsClient.saveEvent(ApproveSessionTraceErrorEvents.sessionNamespacesValidationFailure)
+ throw Errors.emtySessionNamespacesForbidden
+ }
guard let payload = try proposalPayloadsStore.get(key: proposerPubKey) else {
+ eventsClient.saveEvent(ApproveSessionTraceErrorEvents.proposalNotFound)
throw Errors.proposalNotFound
}
+ let pairingTopic = payload.topic
+
+ eventsClient.setTopic(pairingTopic)
let proposal = payload.request
guard !proposal.isExpired() else {
logger.debug("Proposal has expired, topic: \(payload.topic)")
+ eventsClient.saveEvent(ApproveSessionTraceErrorEvents.proposalExpired)
proposalPayloadsStore.delete(forKey: proposerPubKey)
throw Errors.proposalExpired
}
let networkConnectionStatus = await resolveNetworkConnectionStatus()
guard networkConnectionStatus == .connected else {
+ eventsClient.saveEvent(ApproveSessionTraceErrorEvents.networkNotConnected)
throw Errors.networkNotConnected
}
- let pairingTopic = payload.topic
-
- try Namespace.validate(sessionNamespaces)
- try Namespace.validateApproved(sessionNamespaces, against: proposal.requiredNamespaces)
+ do {
+ eventsClient.saveEvent(SessionApproveExecutionTraceEvents.sessionNamespacesValidationStarted)
+ try Namespace.validate(sessionNamespaces)
+ try Namespace.validateApproved(sessionNamespaces, against: proposal.requiredNamespaces)
+ eventsClient.saveEvent(SessionApproveExecutionTraceEvents.sessionNamespacesValidationSuccess)
+ } catch {
+ eventsClient.saveEvent(ApproveSessionTraceErrorEvents.sessionNamespacesValidationFailure)
+ throw error
+ }
let selfPublicKey = try kms.createX25519KeyPair()
guard let agreementKey = try? kms.performKeyAgreement(
selfPublicKey: selfPublicKey,
peerPublicKey: proposal.proposer.publicKey
- ) else { throw Errors.agreementMissingOrInvalid }
+ ) else {
+ eventsClient.saveEvent(ApproveSessionTraceErrorEvents.agreementMissingOrInvalid)
+ throw Errors.agreementMissingOrInvalid
+ }
let sessionTopic = agreementKey.derivedTopic()
try kms.setAgreementSecret(agreementKey, topic: sessionTopic)
guard let relay = proposal.relays.first else {
+ eventsClient.saveEvent(ApproveSessionTraceErrorEvents.relayNotFound)
throw Errors.relayNotFound
}
@@ -125,21 +149,33 @@ final class ApproveEngine {
pairingTopic: pairingTopic
)
- _ = try await proposeResponseTask
- let session: WCSession = try await settleRequestTask
+ do {
+ _ = try await proposeResponseTask
+ eventsClient.saveEvent(SessionApproveExecutionTraceEvents.responseApproveSent)
+ } catch {
+ eventsClient.saveEvent(ApproveSessionTraceErrorEvents.sessionSettleFailure)
+ throw error
+ }
- sessionStore.setSession(session)
- onSessionSettle?(session.publicRepresentation())
- logger.debug("Session proposal response and settle request have been sent")
+ do {
+ let session: WCSession = try await settleRequestTask
+ sessionStore.setSession(session)
+ onSessionSettle?(session.publicRepresentation())
+ eventsClient.saveEvent(SessionApproveExecutionTraceEvents.sessionSettleSuccess)
+ logger.debug("Session proposal response and settle request have been sent")
- proposalPayloadsStore.delete(forKey: proposerPubKey)
- verifyContextStore.delete(forKey: proposerPubKey)
+ proposalPayloadsStore.delete(forKey: proposerPubKey)
+ verifyContextStore.delete(forKey: proposerPubKey)
- pairingRegisterer.activate(
- pairingTopic: payload.topic,
- peerMetadata: payload.request.proposer.metadata
- )
- return session.publicRepresentation()
+ pairingRegisterer.activate(
+ pairingTopic: payload.topic,
+ peerMetadata: payload.request.proposer.metadata
+ )
+ return session.publicRepresentation()
+ } catch {
+ eventsClient.saveEvent(ApproveSessionTraceErrorEvents.sessionSettleFailure)
+ throw error
+ }
}
func reject(proposerPubKey: String, reason: SignReasonCode) async throws {
diff --git a/Sources/WalletConnectSign/Sign/Sign.swift b/Sources/WalletConnectSign/Sign/Sign.swift
index 25f88df3e..3205485d6 100644
--- a/Sources/WalletConnectSign/Sign/Sign.swift
+++ b/Sources/WalletConnectSign/Sign/Sign.swift
@@ -29,7 +29,8 @@ public class Sign {
projectId: Networking.projectId,
crypto: config.crypto,
networkingClient: Networking.interactor,
- groupIdentifier: Networking.groupIdentifier
+ groupIdentifier: Networking.groupIdentifier,
+ eventsClient: Events.instance
)
}()
diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift
index 4acff725c..6a9989112 100644
--- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift
+++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift
@@ -17,7 +17,8 @@ public struct SignClientFactory {
projectId: String,
crypto: CryptoProvider,
networkingClient: NetworkingInteractor,
- groupIdentifier: String
+ groupIdentifier: String,
+ eventsClient: EventsClientProtocol
) -> SignClient {
let logger = ConsoleLogger(prefix: "📝", loggingLevel: .off)
@@ -37,7 +38,8 @@ public struct SignClientFactory {
networkingClient: networkingClient,
iatProvider: iatProvider,
projectId: projectId,
- crypto: crypto
+ crypto: crypto,
+ eventsClient: eventsClient
)
}
@@ -50,7 +52,8 @@ public struct SignClientFactory {
networkingClient: NetworkingInteractor,
iatProvider: IATProvider,
projectId: String,
- crypto: CryptoProvider
+ crypto: CryptoProvider,
+ eventsClient: EventsClientProtocol
) -> SignClient {
let kms = KeyManagementService(keychain: keychainStorage)
let rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage)
@@ -83,7 +86,8 @@ public struct SignClientFactory {
sessionStore: sessionStore,
verifyClient: verifyClient,
rpcHistory: rpcHistory,
- authRequestSubscribersTracking: authRequestSubscribersTracking
+ authRequestSubscribersTracking: authRequestSubscribersTracking,
+ eventsClient: eventsClient
)
let cleanupService = SignCleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionTopicToProposal: sessionTopicToProposal, networkInteractor: networkingClient, rpcHistory: rpcHistory)
let deleteSessionService = DeleteSessionService(networkingInteractor: networkingClient, kms: kms, sessionStore: sessionStore, logger: logger)
@@ -125,7 +129,7 @@ public struct SignClientFactory {
let linkAuthRequester = LinkAuthRequester(kms: kms, appMetadata: metadata, logger: logger, iatProvader: iatProvider, authResponseTopicRecordsStore: authResponseTopicRecordsStore, linkEnvelopesDispatcher: linkEnvelopesDispatcher, linkModeLinksStore: linkModeLinksStore)
let linkAuthRequestSubscriber = LinkAuthRequestSubscriber(logger: logger, kms: kms, envelopesDispatcher: linkEnvelopesDispatcher, verifyClient: verifyClient, verifyContextStore: verifyContextStore)
- let relaySessionAuthenticateResponder = SessionAuthenticateResponder(networkingInteractor: networkingClient, logger: logger, kms: kms, verifyContextStore: verifyContextStore, walletErrorResponder: walletErrorResponder, pairingRegisterer: pairingClient, metadata: metadata, approveSessionAuthenticateUtil: approveSessionAuthenticateUtil)
+ let relaySessionAuthenticateResponder = SessionAuthenticateResponder(networkingInteractor: networkingClient, logger: logger, kms: kms, verifyContextStore: verifyContextStore, walletErrorResponder: walletErrorResponder, pairingRegisterer: pairingClient, metadata: metadata, approveSessionAuthenticateUtil: approveSessionAuthenticateUtil, eventsClient: eventsClient)
let linkSessionAuthenticateResponder = LinkSessionAuthenticateResponder(linkEnvelopesDispatcher: linkEnvelopesDispatcher, logger: logger, kms: kms, metadata: metadata, approveSessionAuthenticateUtil: approveSessionAuthenticateUtil, walletErrorResponder: walletErrorResponder)
diff --git a/Sources/WalletConnectSign/Sign/SignImports.swift b/Sources/WalletConnectSign/Sign/SignImports.swift
index 91463cad3..898b4f971 100644
--- a/Sources/WalletConnectSign/Sign/SignImports.swift
+++ b/Sources/WalletConnectSign/Sign/SignImports.swift
@@ -2,4 +2,6 @@
@_exported import WalletConnectPairing
@_exported import WalletConnectSigner
@_exported import WalletConnectVerify
+@_exported import WalletConnectRelay
+@_exported import Events
#endif
diff --git a/Tests/EventsTests/EventsCollectorTests.swift b/Tests/EventsTests/EventsCollectorTests.swift
new file mode 100644
index 000000000..3dc59c7f9
--- /dev/null
+++ b/Tests/EventsTests/EventsCollectorTests.swift
@@ -0,0 +1,50 @@
+import Foundation
+import XCTest
+@testable import Events
+
+class EventsCollectorTests: XCTestCase {
+
+ var mockStorage: MockEventStorage!
+ var eventsCollector: EventsCollector!
+
+ override func setUp() {
+ super.setUp()
+ mockStorage = MockEventStorage()
+ eventsCollector = EventsCollector(storage: mockStorage, logger: ConsoleLoggerMock())
+ }
+
+ override func tearDown() {
+ eventsCollector = nil
+ mockStorage = nil
+ super.tearDown()
+ }
+
+ func testStartTrace() {
+ eventsCollector.startTrace(topic: "test_topic")
+ XCTAssertEqual(eventsCollector.topic, "test_topic")
+ XCTAssertEqual(eventsCollector.trace.count, 0)
+ }
+
+ func testSaveEvent() {
+ eventsCollector.startTrace(topic: "test_topic")
+ eventsCollector.saveEvent(PairingExecutionTraceEvents.pairingStarted)
+
+ XCTAssertEqual(eventsCollector.trace, ["pairing_started"])
+ XCTAssertEqual(mockStorage.savedEvents.count, 0)
+ }
+
+ func testSaveErrorEvent() {
+ eventsCollector.startTrace(topic: "test_topic")
+ eventsCollector.saveEvent(PairingExecutionTraceEvents.pairingStarted)
+ eventsCollector.saveEvent(PairingTraceErrorEvents.noInternetConnection)
+
+ XCTAssertEqual(mockStorage.savedEvents.count, 1)
+ let savedEvent = mockStorage.savedEvents.first
+ XCTAssertNotNil(savedEvent)
+ XCTAssertEqual(savedEvent?.props.type, "no_internet_connection")
+ XCTAssertEqual(savedEvent?.props.properties?.topic, "test_topic")
+ XCTAssertEqual(savedEvent?.props.properties?.trace, ["pairing_started", "no_internet_connection"])
+ XCTAssertNil(eventsCollector.topic)
+ XCTAssertEqual(eventsCollector.trace.count, 0)
+ }
+}
diff --git a/Tests/EventsTests/EventsDispatcherTests.swift b/Tests/EventsTests/EventsDispatcherTests.swift
new file mode 100644
index 000000000..e7b980e5a
--- /dev/null
+++ b/Tests/EventsTests/EventsDispatcherTests.swift
@@ -0,0 +1,38 @@
+import XCTest
+@testable import Events
+
+class EventsDispatcherTests: XCTestCase {
+ var mockNetworkingService: MockNetworkingService!
+ var eventsDispatcher: EventsDispatcher!
+ let events = [Event(eventId: UUID().uuidString, bundleId: "com.wallet.example", timestamp: Int64(Date().timeIntervalSince1970 * 1000), props: Props(event: "ERROR", type: "test_error", properties: Properties(topic: "test_topic", trace: ["test_trace"])))]
+
+ override func setUp() {
+ super.setUp()
+ mockNetworkingService = MockNetworkingService()
+ let retryPolicy = RetryPolicy(maxAttempts: 3, initialDelay: 1, multiplier: 1.5, delayOverride: 0.001)
+ eventsDispatcher = EventsDispatcher(networkingService: mockNetworkingService, retryPolicy: retryPolicy)
+ }
+
+ override func tearDown() {
+ eventsDispatcher = nil
+ mockNetworkingService = nil
+ super.tearDown()
+ }
+
+ func testRetrySuccess() async throws {
+ mockNetworkingService.shouldFail = true
+ do {
+ _ = try await eventsDispatcher.executeWithRetry(events: events)
+ XCTFail("Expected to throw an error")
+ } catch {
+ XCTAssertEqual(mockNetworkingService.attemptCount, 3)
+ }
+ }
+
+ func testRetryFailure() async throws {
+ mockNetworkingService.shouldFail = false
+ let result = try await eventsDispatcher.executeWithRetry(events: events)
+ XCTAssertEqual(result, true)
+ XCTAssertEqual(mockNetworkingService.attemptCount, 1)
+ }
+}
diff --git a/Tests/RelayerTests/Mocks/DispatcherMock.swift b/Tests/RelayerTests/Mocks/DispatcherMock.swift
index 2e12b6ab2..82dd12c6c 100644
--- a/Tests/RelayerTests/Mocks/DispatcherMock.swift
+++ b/Tests/RelayerTests/Mocks/DispatcherMock.swift
@@ -4,6 +4,8 @@ import Combine
@testable import WalletConnectRelay
class DispatcherMock: Dispatching {
+ var isSocketConnected: Bool = true
+
private var publishers = Set()
private let socketConnectionStatusPublisherSubject = CurrentValueSubject(.disconnected)
diff --git a/Tests/TestingUtils/NetworkingInteractorMock.swift b/Tests/TestingUtils/NetworkingInteractorMock.swift
index a1fa196ad..65b73ab0c 100644
--- a/Tests/TestingUtils/NetworkingInteractorMock.swift
+++ b/Tests/TestingUtils/NetworkingInteractorMock.swift
@@ -6,6 +6,8 @@ import WalletConnectKMS
import WalletConnectNetworking
public class NetworkingInteractorMock: NetworkInteracting {
+ public var isSocketConnected: Bool = true
+
private var publishers = Set()
diff --git a/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift b/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift
index 6def56757..1aeee96ec 100644
--- a/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift
+++ b/Tests/WalletConnectPairingTests/WalletPairServiceTests.swift
@@ -5,7 +5,7 @@ import XCTest
import WalletConnectUtils
import WalletConnectNetworking
-final class WalletPairServiceTestsTests: XCTestCase {
+final class WalletPairServiceTests: XCTestCase {
var service: WalletPairService!
var networkingInteractor: NetworkingInteractorMock!
@@ -18,7 +18,7 @@ final class WalletPairServiceTestsTests: XCTestCase {
storageMock = WCPairingStorageMock()
cryptoMock = KeyManagementServiceMock()
rpcHistory = RPCHistoryFactory.createForNetwork(keyValueStorage: RuntimeKeyValueStorage())
- service = WalletPairService(networkingInteractor: networkingInteractor, kms: cryptoMock, pairingStorage: storageMock, history: rpcHistory, logger: ConsoleLoggerMock())
+ service = WalletPairService(networkingInteractor: networkingInteractor, kms: cryptoMock, pairingStorage: storageMock, history: rpcHistory, logger: ConsoleLoggerMock(), eventsClient: MockEventsClient())
}
func testPairWhenNetworkNotConnectedThrows() async {
diff --git a/Tests/WalletConnectSignTests/AppProposalServiceTests.swift b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift
index 18a392325..ae3a4772d 100644
--- a/Tests/WalletConnectSignTests/AppProposalServiceTests.swift
+++ b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift
@@ -78,7 +78,8 @@ final class AppProposalServiceTests: XCTestCase {
sessionStore: WCSessionStorageMock(),
verifyClient: VerifyClientMock(),
rpcHistory: history,
- authRequestSubscribersTracking: AuthRequestSubscribersTracking(logger: logger)
+ authRequestSubscribersTracking: AuthRequestSubscribersTracking(logger: logger),
+ eventsClient: MockEventsClient()
)
}
diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift
index 585f7e4be..79b2f7996 100644
--- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift
+++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift
@@ -52,7 +52,8 @@ final class ApproveEngineTests: XCTestCase {
sessionStore: sessionStorageMock,
verifyClient: VerifyClientMock(),
rpcHistory: history,
- authRequestSubscribersTracking: AuthRequestSubscribersTracking(logger: ConsoleLoggerMock())
+ authRequestSubscribersTracking: AuthRequestSubscribersTracking(logger: ConsoleLoggerMock()),
+ eventsClient: MockEventsClient()
)
}