diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index 1d68f9305..e67e77973 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -776,7 +776,7 @@ final class SignClientTests: XCTestCase { func testEIP191SessionAuthenticated() async throws { let responseExpectation = expectation(description: "successful response delivered") - wallet.authRequestPublisher.sink { [unowned self] (request, _) in + wallet.authenticateRequestPublisher.sink { [unowned self] (request, _) in Task(priority: .high) { let signerFactory = DefaultSignerFactory() let signer = MessageSignerFactory(signerFactory: signerFactory).create() @@ -811,7 +811,7 @@ final class SignClientTests: XCTestCase { func testEIP191SessionAuthenticateEmptyMethods() async throws { let responseExpectation = expectation(description: "successful response delivered") - wallet.authRequestPublisher.sink { [unowned self] (request, _) in + wallet.authenticateRequestPublisher.sink { [unowned self] (request, _) in Task(priority: .high) { let signerFactory = DefaultSignerFactory() let signer = MessageSignerFactory(signerFactory: signerFactory).create() @@ -846,7 +846,7 @@ final class SignClientTests: XCTestCase { func testEIP191SessionAuthenticatedMultiCacao() async throws { let responseExpectation = expectation(description: "successful response delivered") - wallet.authRequestPublisher.sink { [unowned self] (request, _) in + wallet.authenticateRequestPublisher.sink { [unowned self] (request, _) in Task(priority: .high) { let signerFactory = DefaultSignerFactory() let signer = MessageSignerFactory(signerFactory: signerFactory).create() @@ -911,7 +911,7 @@ final class SignClientTests: XCTestCase { try await walletPairingClient.pair(uri: uri) - wallet.authRequestPublisher.sink { [unowned self] (request, _) in + wallet.authenticateRequestPublisher.sink { [unowned self] (request, _) in Task(priority: .high) { let signature = CacaoSignature(t: .eip1271, s: eip1271Signature) let cacao = try! wallet.buildSignedAuthObject(authPayload: request.payload, signature: signature, account: account) @@ -932,7 +932,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dapp.authenticate(AuthRequestParams.stub()) try? await walletPairingClient.pair(uri: uri) - wallet.authRequestPublisher.sink { [unowned self] (request, _) in + wallet.authenticateRequestPublisher.sink { [unowned self] (request, _) in Task(priority: .high) { let invalidSignature = CacaoSignature(t: .eip1271, s: eip1271Signature) @@ -954,7 +954,7 @@ final class SignClientTests: XCTestCase { let uri = try! await dapp.authenticate(AuthRequestParams.stub()) try? await walletPairingClient.pair(uri: uri) - wallet.authRequestPublisher.sink { [unowned self] request in + wallet.authenticateRequestPublisher.sink { [unowned self] request in Task(priority: .high) { try! await wallet.rejectSession(requestId: request.0.id) } @@ -979,7 +979,7 @@ final class SignClientTests: XCTestCase { let chain = Blockchain("eip155:1")! // sleep is needed as emitRequestIfPending() will be called on client init and then on request itself, second request would be debouced sleep(1) - wallet.authRequestPublisher.sink { [unowned self] (request, _) in + wallet.authenticateRequestPublisher.sink { [unowned self] (request, _) in Task(priority: .high) { let signerFactory = DefaultSignerFactory() let signer = MessageSignerFactory(signerFactory: signerFactory).create() @@ -1062,4 +1062,28 @@ final class SignClientTests: XCTestCase { await fulfillment(of: [fallbackExpectation], timeout: InputConfig.defaultTimeout) } + + func testFallbackToSessionProposeIfWalletIsNotSubscribingSessionAuthenticate() async throws { + let responseExpectation = expectation(description: "successful response delivered") + + let requiredNamespaces = ProposalNamespace.stubRequired() + let sessionNamespaces = SessionNamespace.make(toRespond: requiredNamespaces) + + wallet.sessionProposalPublisher.sink { [unowned self] (proposal, _) in + Task(priority: .high) { + do { _ = try await wallet.approve(proposalId: proposal.id, namespaces: sessionNamespaces) } catch { XCTFail("\(error)") } + } + }.store(in: &publishers) + + dapp.sessionSettlePublisher.sink { settledSession in + Task(priority: .high) { + responseExpectation.fulfill() + } + }.store(in: &publishers) + + let uri = try await dapp.authenticate(AuthRequestParams.stub()) + try await walletPairingClient.pair(uri: uri) + await fulfillment(of: [responseExpectation], timeout: InputConfig.defaultTimeout) + } + } diff --git a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift index 7664d62f4..6042e0366 100644 --- a/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift +++ b/Sources/WalletConnectNotify/Client/Wallet/NotifyClient.swift @@ -180,17 +180,6 @@ public class NotifyClient { } } -private extension NotifyClient { - - func makeStatement(allApps: Bool) -> String { - switch allApps { - case false: - return "I further authorize this app to send me notifications. Read more at https://walletconnect.com/notifications" - case true: - return "I further authorize this app to view and manage my notifications for ALL apps. Read more at https://walletconnect.com/notifications" - } - } -} #if targetEnvironment(simulator) extension NotifyClient { diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 33c951dc0..e2dc938ac 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -29,6 +29,7 @@ final class ApproveEngine { private let kms: KeyManagementServiceProtocol private let logger: ConsoleLogging private let rpcHistory: RPCHistory + private let authRequestSubscribersTracking: AuthRequestSubscribersTracking private var publishers = Set() @@ -44,7 +45,8 @@ final class ApproveEngine { pairingStore: WCPairingStorage, sessionStore: WCSessionStorage, verifyClient: VerifyClientProtocol, - rpcHistory: RPCHistory + rpcHistory: RPCHistory, + authRequestSubscribersTracking: AuthRequestSubscribersTracking ) { self.networkingInteractor = networkingInteractor self.proposalPayloadsStore = proposalPayloadsStore @@ -58,6 +60,7 @@ final class ApproveEngine { self.sessionStore = sessionStore self.verifyClient = verifyClient self.rpcHistory = rpcHistory + self.authRequestSubscribersTracking = authRequestSubscribersTracking setupRequestSubscriptions() setupResponseSubscriptions() @@ -217,7 +220,7 @@ private extension ApproveEngine { guard let pairing = pairingStore.getPairing(forTopic: payload.topic) else { return } if let methods = pairing.methods, methods.flatMap({ $0 }) - .contains(SessionAuthenticatedProtocolMethod().method) { + .contains(SessionAuthenticatedProtocolMethod().method), authRequestSubscribersTracking.hasSubscribers() { logger.debug("Ignoring Session Proposal") // respond with an error? return diff --git a/Sources/WalletConnectSign/Engine/Common/AuthRequestSubscribersTracking.swift b/Sources/WalletConnectSign/Engine/Common/AuthRequestSubscribersTracking.swift new file mode 100644 index 000000000..650029c7c --- /dev/null +++ b/Sources/WalletConnectSign/Engine/Common/AuthRequestSubscribersTracking.swift @@ -0,0 +1,30 @@ +import Foundation + +// purpose of this class is to allow fallback to session propose in case wallet did not subscribe to authenticateRequestPublisher and dapp request wc_sessionAuthenticate +class AuthRequestSubscribersTracking { + private var subscribersCount: Int = 0 + private let serialQueue = DispatchQueue(label: "com.walletconnect.AuthRequestSubscribersTrackingQueue") + private let logger: ConsoleLogging + + init(logger: ConsoleLogging) { + self.logger = logger + } + + func increment() { + serialQueue.sync { + subscribersCount += 1 + logger.debug("Incremented subscriber count: \(subscribersCount)") + } + } + + func decrement() { + serialQueue.sync { + subscribersCount = max(0, subscribersCount - 1) + logger.debug("Decremented subscriber count: \(subscribersCount)") + } + } + + func hasSubscribers() -> Bool { + return serialQueue.sync { subscribersCount > 0 } + } +} diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index b0fe8caf7..64ed1a1f7 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -100,8 +100,14 @@ public final class SignClient: SignClientProtocol { /// Publisher that sends authentication requests /// /// Wallet should subscribe on events in order to receive auth requests. - public var authRequestPublisher: AnyPublisher<(request: AuthenticationRequest, context: VerifyContext?), Never> { - authRequestPublisherSubject.eraseToAnyPublisher() + public var authenticateRequestPublisher: AnyPublisher<(request: AuthenticationRequest, context: VerifyContext?), Never> { + return authRequestPublisherSubject + .handleEvents(receiveSubscription: { [unowned self] _ in + authRequestSubscribersTracking.increment() + }, receiveCancel: { [unowned self] in + authRequestSubscribersTracking.decrement() + }) + .eraseToAnyPublisher() } /// Publisher that sends authentication responses @@ -175,6 +181,7 @@ public final class SignClient: SignClientProtocol { private let pingResponsePublisherSubject = PassthroughSubject() private let sessionsPublisherSubject = PassthroughSubject<[Session], Never>() private var authRequestPublisherSubject = PassthroughSubject<(request: AuthenticationRequest, context: VerifyContext?), Never>() + private let authRequestSubscribersTracking: AuthRequestSubscribersTracking private var publishers = Set() @@ -204,7 +211,8 @@ public final class SignClient: SignClientProtocol { proposalExpiryWatcher: ProposalExpiryWatcher, pendingProposalsProvider: PendingProposalsProvider, requestsExpiryWatcher: RequestsExpiryWatcher, - authResponseTopicResubscriptionService: AuthResponseTopicResubscriptionService + authResponseTopicResubscriptionService: AuthResponseTopicResubscriptionService, + authRequestSubscribersTracking: AuthRequestSubscribersTracking ) { self.logger = logger self.networkingClient = networkingClient @@ -231,6 +239,7 @@ public final class SignClient: SignClientProtocol { self.pendingProposalsProvider = pendingProposalsProvider self.requestsExpiryWatcher = requestsExpiryWatcher self.authResponseTopicResubscriptionService = authResponseTopicResubscriptionService + self.authRequestSubscribersTracking = authRequestSubscribersTracking setUpConnectionObserving() setUpEnginesCallbacks() diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift index e07c86816..bfbdcc6b9 100644 --- a/Sources/WalletConnectSign/Sign/SignClientFactory.swift +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -68,6 +68,7 @@ public struct SignClientFactory { let sessionExtendRequestSubscriber = SessionExtendRequestSubscriber(networkingInteractor: networkingClient, sessionStore: sessionStore, logger: logger) let sessionExtendResponseSubscriber = SessionExtendResponseSubscriber(networkingInteractor: networkingClient, sessionStore: sessionStore, logger: logger) let sessionTopicToProposal = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: SignStorageIdentifiers.sessionTopicToProposal.rawValue) + let authRequestSubscribersTracking = AuthRequestSubscribersTracking(logger: logger) let approveEngine = ApproveEngine( networkingInteractor: networkingClient, proposalPayloadsStore: proposalPayloadsStore, @@ -80,7 +81,8 @@ public struct SignClientFactory { pairingStore: pairingStore, sessionStore: sessionStore, verifyClient: verifyClient, - rpcHistory: rpcHistory + rpcHistory: rpcHistory, + authRequestSubscribersTracking: authRequestSubscribersTracking ) 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) @@ -135,7 +137,8 @@ public struct SignClientFactory { proposalExpiryWatcher: proposalExpiryWatcher, pendingProposalsProvider: pendingProposalsProvider, requestsExpiryWatcher: requestsExpiryWatcher, - authResponseTopicResubscriptionService: authResponseTopicResubscriptionService + authResponseTopicResubscriptionService: authResponseTopicResubscriptionService, + authRequestSubscribersTracking: authRequestSubscribersTracking ) return client } diff --git a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift index 9073fe4e6..1947e298c 100644 --- a/Sources/WalletConnectSign/Sign/SignClientProtocol.swift +++ b/Sources/WalletConnectSign/Sign/SignClientProtocol.swift @@ -12,7 +12,7 @@ public protocol SignClientProtocol { var sessionResponsePublisher: AnyPublisher { get } var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> { get } var sessionEventPublisher: AnyPublisher<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never> { get } - var authRequestPublisher: AnyPublisher<(request: AuthenticationRequest, context: VerifyContext?), Never> { get } + var authenticateRequestPublisher: AnyPublisher<(request: AuthenticationRequest, context: VerifyContext?), Never> { get } var logsPublisher: AnyPublisher {get} var sessionProposalExpirationPublisher: AnyPublisher { get } var pendingProposalsPublisher: AnyPublisher<[(proposal: Session.Proposal, context: VerifyContext?)], Never> { get } diff --git a/Sources/Web3Wallet/Web3WalletClient.swift b/Sources/Web3Wallet/Web3WalletClient.swift index 8853f8cde..bde5bc345 100644 --- a/Sources/Web3Wallet/Web3WalletClient.swift +++ b/Sources/Web3Wallet/Web3WalletClient.swift @@ -27,7 +27,7 @@ public class Web3WalletClient { /// /// Wallet should subscribe on events in order to receive auth requests. public var authenticateRequestPublisher: AnyPublisher<(request: AuthenticationRequest, context: VerifyContext?), Never> { - signClient.authRequestPublisher.eraseToAnyPublisher() + signClient.authenticateRequestPublisher.eraseToAnyPublisher() } /// Publisher that sends sessions on every sessions update diff --git a/Tests/WalletConnectSignTests/AppProposalServiceTests.swift b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift index 71b50a9ca..18a392325 100644 --- a/Tests/WalletConnectSignTests/AppProposalServiceTests.swift +++ b/Tests/WalletConnectSignTests/AppProposalServiceTests.swift @@ -77,7 +77,8 @@ final class AppProposalServiceTests: XCTestCase { pairingStore: storageMock, sessionStore: WCSessionStorageMock(), verifyClient: VerifyClientMock(), - rpcHistory: history + rpcHistory: history, + authRequestSubscribersTracking: AuthRequestSubscribersTracking(logger: logger) ) } diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index 63bf4ce0d..585f7e4be 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -51,7 +51,8 @@ final class ApproveEngineTests: XCTestCase { pairingStore: pairingStorageMock, sessionStore: sessionStorageMock, verifyClient: VerifyClientMock(), - rpcHistory: history + rpcHistory: history, + authRequestSubscribersTracking: AuthRequestSubscribersTracking(logger: ConsoleLoggerMock()) ) }